No team members configured. Please add team members in the configuration tab.
';
}
workloadData.forEach(member => {
const percentage = member.capacity > 0 ? (member.currentLoad / member.capacity) * 100 : 0;
let barColor = 'bg-green-500';
if (percentage > 85) barColor = 'bg-red-500';
else if (percentage > 60) barColor = 'bg-yellow-500';
const taskItems = member.assignedTasks.map(t => `
${t.desc} (${t.hours}h)`).join('');
list.innerHTML += `
${member.name}
${member.currentLoad} / ${member.capacity} hrs
${taskItems || '- No tasks assigned.
'}
`;
});
renderChart(workloadData);
};
const renderTeamConfig = () => {
const list = document.getElementById('teamMemberList');
list.innerHTML = '';
team.forEach(member => {
list.innerHTML += `
${member.name}
Capacity: ${member.capacity} hrs/week
Skills: ${member.skills.join(', ')}
`;
});
};
const renderChart = (workloadData) => {
if (workloadChart) workloadChart.destroy();
const ctx = document.getElementById('workloadChart').getContext('2d');
workloadChart = new Chart(ctx, {
type: 'bar',
data: {
labels: workloadData.map(m => m.name),
datasets: [{
label: 'Current Workload (Hours)',
data: workloadData.map(m => m.currentLoad),
backgroundColor: '#3B82F6'
}, {
label: 'Capacity (Hours)',
data: workloadData.map(m => m.capacity),
backgroundColor: '#E5E7EB'
}]
},
options: { scales: { x: { stacked: false }, y: { stacked: false, beginAtZero: true } } }
});
};
// --- MODAL & FORM LOGIC ---
app.showTaskModal = () => {
const select = document.getElementById('taskAssigneeSelect');
select.innerHTML = '
';
team.forEach(m => select.innerHTML += `
`);
document.getElementById('taskModal').classList.remove('hidden');
};
app.showMemberModal = (id = null) => {
const modal = document.getElementById('memberModal');
const title = document.getElementById('memberModalTitle');
const idInput = document.getElementById('memberIdInput');
const nameInput = document.getElementById('memberNameInput');
const capacityInput = document.getElementById('memberCapacityInput');
const skillsInput = document.getElementById('memberSkillsInput');
if (id) {
const member = team.find(m => m.id === id);
title.textContent = 'Edit Team Member';
idInput.value = member.id;
nameInput.value = member.name;
capacityInput.value = member.capacity;
skillsInput.value = member.skills.join(', ');
} else {
title.textContent = 'Add Team Member';
idInput.value = '';
nameInput.value = '';
capacityInput.value = 40;
skillsInput.value = '';
}
modal.classList.remove('hidden');
};
app.hideModal = (modalId) => document.getElementById(modalId).classList.add('hidden');
// --- CORE LOGIC ---
app.saveTask = () => {
const desc = document.getElementById('taskDescInput').value;
const hours = parseFloat(document.getElementById('taskHoursInput').value);
const skill = document.getElementById('taskSkillInput').value.trim();
let assigneeId = parseInt(document.getElementById('taskAssigneeSelect').value);
if (!desc || !hours) { alert('Description and hours are required.'); return; }
if (!assigneeId) { // Auto-assign logic
const availableMembers = team
.map(member => ({
...member,
currentLoad: tasks.filter(t => t.assigneeId === member.id).reduce((sum, t) => sum + t.hours, 0)
}))
.filter(member => (member.currentLoad + hours) <= member.capacity && (!skill || member.skills.map(s => s.toLowerCase()).includes(skill.toLowerCase())))
.sort((a, b) => (a.currentLoad / a.capacity) - (b.currentLoad / b.capacity)); // Assign to least busy
if (availableMembers.length > 0) {
assigneeId = availableMembers[0].id;
} else {
alert('No suitable team member found with available capacity or required skill. Please assign manually or adjust task.');
return;
}
}
tasks.push({ id: Date.now(), desc, hours, skill, assigneeId });
saveData();
renderWorkloadDashboard();
app.hideModal('taskModal');
};
app.saveMember = () => {
const id = document.getElementById('memberIdInput').value;
const name = document.getElementById('memberNameInput').value;
if (!name.trim()) { alert('Name is required.'); return; }
const memberData = {
name,
capacity: parseInt(document.getElementById('memberCapacityInput').value) || 40,
skills: document.getElementById('memberSkillsInput').value.split(',').map(s => s.trim()).filter(Boolean)
};
if (id) {
const member = team.find(m => m.id == id);
Object.assign(member, memberData);
} else {
team.push({ id: Date.now(), ...memberData });
}
saveData();
renderTeamConfig();
app.hideModal('memberModal');
};
app.deleteMember = (id) => {
if (confirm('Are you sure? This will unassign all their tasks.')) {
team = team.filter(m => m.id !== id);
tasks.forEach(task => { if (task.assigneeId === id) task.assigneeId = null; });
saveData();
renderTeamConfig();
renderWorkloadDashboard();
}
};
// --- UTILS & HELPERS ---
app.changeTab = (tabIndex) => {
document.querySelectorAll('[id^="tabContent-"]').forEach(el => el.classList.add('hidden'));
document.querySelectorAll('[id^="tab-"]').forEach(el => el.classList.replace('tab-active', 'tab-inactive'));
document.getElementById(`tabContent-${tabIndex}`).classList.remove('hidden');
document.getElementById(`tab-${tabIndex}`).classList.replace('tab-inactive', 'tab-active');
};
app.downloadReportPDF = () => {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
doc.setFontSize(20);
doc.text('Team Workload Report', 105, 15, { align: 'center' });
const head = [['Team Member', 'Skills', 'Capacity (h)', 'Current Load (h)', 'Utilization']];
const body = team.map(member => {
const currentLoad = tasks.filter(t => t.assigneeId === member.id).reduce((sum, t) => sum + t.hours, 0);
const utilization = member.capacity > 0 ? `${((currentLoad / member.capacity) * 100).toFixed(1)}%` : 'N/A';
return [member.name, member.skills.join(', '), member.capacity, currentLoad, utilization];
});
doc.autoTable({ head, body, startY: 25 });
doc.save('workload_report.pdf');
};
// --- INITIALIZATION ---
loadData();
renderWorkloadDashboard();
renderTeamConfig();
});