Online Secure Office Department-Wide Workload Balancer
Add a New Task
Configure Team
Current Team Members:
Hours: ${task.hours}
`; // Populate the reassign dropdown for this task const reassignSelect = taskCard.querySelector('.reassign-select'); teamAndUnassigned.forEach(optionMember => { const option = document.createElement('option'); option.value = optionMember.id === null ? '' : optionMember.id; option.textContent = optionMember.name; if (optionMember.id === task.assigneeId) { option.selected = true; } reassignSelect.appendChild(option); }); taskListDiv.appendChild(taskCard); }); } dashboardContainer.appendChild(memberColumn); }); } /** * Renders the list of team members in the Team Configuration tab. */ function renderTeamConfig() { if (!teamList) return; teamList.innerHTML = ''; // Clear existing list state.team.forEach(member => { const li = document.createElement('li'); li.className = 'flex justify-between items-center bg-gray-100 p-3 rounded-lg'; li.innerHTML = ` ${member.name} `; teamList.appendChild(li); }); } /** * Populates the assignee dropdown in the Task Input form. */ function renderTaskAssigneeDropdown() { if (!taskAssigneeSelect) return; taskAssigneeSelect.innerHTML = ''; state.team.forEach(member => { const option = document.createElement('option'); option.value = member.id; option.textContent = member.name; taskAssigneeSelect.appendChild(option); }); } // --- EVENT HANDLERS --- // /** * Handles the submission of the team member form. * @param {Event} e - The form submission event. */ function handleAddMember(e) { e.preventDefault(); const name = memberNameInput.value.trim(); if (name) { const newMember = { id: Date.now(), // Simple unique ID name: name, }; state.team.push(newMember); memberNameInput.value = ''; renderAll(); } } /** * Handles clicks on the team list for removing members. * @param {Event} e - The click event. */ function handleTeamListClick(e) { if (e.target.classList.contains('remove-member-btn')) { const memberId = parseInt(e.target.dataset.id, 10); // Remove the team member state.team = state.team.filter(member => member.id !== memberId); // Reassign tasks of the removed member to "Unassigned" state.tasks.forEach(task => { if (task.assigneeId === memberId) { task.assigneeId = null; } }); renderAll(); } } /** * Handles the submission of the new task form. * @param {Event} e - The form submission event. */ function handleAddTask(e) { e.preventDefault(); const name = taskNameInput.value.trim(); const hours = parseFloat(taskHoursInput.value); const assigneeId = taskAssigneeSelect.value ? parseInt(taskAssigneeSelect.value, 10) : null; if (name && hours > 0) { const newTask = { id: Date.now(), name, hours, assigneeId, }; state.tasks.push(newTask); taskForm.reset(); renderAll(); showTab('dashboard'); // Switch to dashboard to see the new task } } /** * Handles the change event for reassigning tasks from the dashboard. * @param {Event} e - The change event. */ function handleReassignTask(e) { if (e.target.classList.contains('reassign-select')) { const taskId = parseInt(e.target.dataset.taskId, 10); const newAssigneeId = e.target.value ? parseInt(e.target.value, 10) : null; const taskToUpdate = state.tasks.find(task => task.id === taskId); if (taskToUpdate) { taskToUpdate.assigneeId = newAssigneeId; renderDashboard(); // Only need to re-render the dashboard } } } // --- UI & NAVIGATION LOGIC --- // /** * Shows a specific tab and hides others. * @param {string} tabId - The ID of the tab to show ('dashboard', 'task-input', 'team-config'). */ window.showTab = (tabId) => { state.currentTab = tabId; // Hide all tab contents Object.values(tabContents).forEach(content => content.classList.add('hidden')); // Deactivate all tab buttons Object.values(tabButtons).forEach(button => button.classList.replace('tab-active', 'tab-inactive')); // Show the selected tab content if (tabContents[tabId]) { tabContents[tabId].classList.remove('hidden'); } // Activate the selected tab button if (tabButtons[tabId]) { tabButtons[tabId].classList.replace('tab-inactive', 'tab-active'); } updateNavButtons(); }; /** * Updates the visibility and state of the 'Previous' and 'Next' navigation buttons. */ function updateNavButtons() { const tabOrder = ['dashboard', 'task-input', 'team-config']; const currentIndex = tabOrder.indexOf(state.currentTab); prevBtn.disabled = currentIndex === 0; prevBtn.classList.toggle('opacity-50', prevBtn.disabled); nextBtn.disabled = currentIndex === tabOrder.length - 1; nextBtn.classList.toggle('opacity-50', nextBtn.disabled); } /** * Navigates to the next or previous tab. * @param {string} direction - 'next' or 'prev'. */ window.navigateTabs = (direction) => { const tabOrder = ['dashboard', 'task-input', 'team-config']; const currentIndex = tabOrder.indexOf(state.currentTab); let newIndex = currentIndex; if (direction === 'next' && currentIndex < tabOrder.length - 1) { newIndex++; } else if (direction === 'prev' && currentIndex > 0) { newIndex--; } showTab(tabOrder[newIndex]); }; // --- PDF DOWNLOAD FUNCTIONALITY --- // /** * Generates and triggers the download of a PDF report of the current workload. */ window.downloadPDF = () => { const { jsPDF } = window.jspdf; const doc = new jsPDF(); // Add a title to the PDF doc.setFontSize(18); doc.text("Department Workload Report", 14, 22); doc.setFontSize(11); doc.setTextColor(100); doc.text(`Generated on: ${new Date().toLocaleDateString('en-US')}`, 14, 30); // Prepare data for the table const head = [['Team Member', 'Task', 'Estimated Hours']]; const body = []; state.team.forEach(member => { const memberTasks = state.tasks.filter(task => task.assigneeId === member.id); if (memberTasks.length > 0) { memberTasks.forEach((task, index) => { // Show member name only for the first task to group them const memberName = index === 0 ? member.name : ''; body.push([memberName, task.name, task.hours]); }); } else { body.push([member.name, 'No tasks assigned', '0']); } }); // Add unassigned tasks const unassignedTasks = state.tasks.filter(task => task.assigneeId === null); if (unassignedTasks.length > 0) { unassignedTasks.forEach((task, index) => { const memberName = index === 0 ? 'Unassigned' : ''; body.push([memberName, task.name, task.hours]); }); } // Create the main table doc.autoTable({ head: head, body: body, startY: 40, didDrawCell: (data) => { // This helps group rows by drawing a line above each new member if (data.section === 'body' && data.column.index === 0 && data.cell.text[0]) { if (data.row.index > 0) { doc.setDrawColor(200); // gray doc.line(data.cell.x, data.cell.y, data.cell.x + data.table.columns[0].width + data.table.columns[1].width + data.table.columns[2].width, data.cell.y); } } }, headStyles: { fillColor: [59, 130, 246] }, // blue-500 styles: { cellPadding: 2.5, fontSize: 10 }, }); // Add a summary table for total hours per member const summaryHead = [['Team Member', 'Total Hours']]; const summaryBody = state.team.map(member => { const totalHours = state.tasks .filter(task => task.assigneeId === member.id) .reduce((sum, task) => sum + parseFloat(task.hours), 0); return [member.name, totalHours.toFixed(1)]; }); doc.addPage(); doc.setFontSize(16); doc.text("Workload Summary", 14, 22); doc.autoTable({ head: summaryHead, body: summaryBody, startY: 30, headStyles: { fillColor: [22, 163, 74] }, // green-600 }); // Save the PDF doc.save('workload-balancer-report.pdf'); }; // --- ATTACH EVENT LISTENERS --- // // Null checks are important to prevent errors if an element is not found. if (teamForm) { teamForm.addEventListener('submit', handleAddMember); } if (teamList) { teamList.addEventListener('click', handleTeamListClick); } if (taskForm) { taskForm.addEventListener('submit', handleAddTask); } if (dashboardContainer) { dashboardContainer.addEventListener('change', handleReassignTask); } // --- INITIAL APP START --- // initializeWithSampleData(); showTab('dashboard'); // Ensure dashboard is the first tab shown });