Workload Distribution Optimizer

Workload Distribution Optimizer

Manage Team Members & Capacity

Current Team

    Add New Task

    Task List (To Be Optimized)

      Total Tasks: ${member.taskCount} Total Raw Effort: ${member.totalEffort.toFixed(1)} units Capacity-Adjusted Load: ${member.capacityAdjustedLoad.toFixed(2)}

      `; optimizedMemberAssignmentsDiv.appendChild(memberDiv); // Chart Bar const barGroup = document.createElement('div'); barGroup.className = 'chart-bar-group'; const bar = document.createElement('div'); bar.className = 'chart-bar'; const chartValue = strategy === 'byCapacityAdjustedEffort' ? member.capacityAdjustedLoad : member.taskCount; const barHeight = (isFinite(chartValue) ? chartValue : 0 / maxLoadMetric) * 100; bar.style.height = `${Math.max(5, barHeight)}%`; bar.textContent = chartValue.toFixed(strategy === 'byCapacityAdjustedEffort' ? 2:0); bar.title = `Member: ${member.name}\nRaw Effort: ${member.totalEffort.toFixed(1)}\nTasks: ${member.taskCount}\nCap-Adj Load: ${member.capacityAdjustedLoad.toFixed(2)}`; const label = document.createElement('div'); label.className = 'chart-label'; label.textContent = member.name; barGroup.appendChild(bar); barGroup.appendChild(label); loadChartDiv.appendChild(barGroup); }); optimizedWorkloadResultsDiv.style.display = 'block'; } function handleTaskReassignment(event) { const taskId = event.target.dataset.taskId; const fromMemberId = event.target.dataset.currentMemberId; const toMemberId = event.target.value; if (!toMemberId || !currentOptimizedDistribution) return; // No selection or no distribution const fromMember = currentOptimizedDistribution.memberWorkloads.find(m => m.id === fromMemberId); const toMember = currentOptimizedDistribution.memberWorkloads.find(m => m.id === toMemberId); const taskIndex = fromMember.assignedTasks.findIndex(t => t.id === taskId); const task = fromMember.assignedTasks[taskIndex]; // Move task fromMember.assignedTasks.splice(taskIndex, 1); toMember.assignedTasks.push(task); // Update metrics for both members fromMember.totalEffort -= task.effort; fromMember.taskCount -= 1; toMember.totalEffort += task.effort; toMember.taskCount += 1; // Re-display with updated data (true indicates manual update, so header doesn't change) displayOptimizedWorkload(currentOptimizedDistribution, true); } clearOptimizedWorkloadBtn.addEventListener('click', () => { currentOptimizedDistribution = null; displayOptimizedWorkload(null); }); // --- PDF Export --- downloadOptimizedPdfBtn.addEventListener('click', async () => { if (!currentOptimizedDistribution) { alert('No optimized workload to download.'); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF('p', 'pt', 'a4'); const { strategy, memberWorkloads } = currentOptimizedDistribution; const strategyText = optimizationStrategySelect.options[optimizationStrategySelect.selectedIndex].text; doc.setFontSize(18); doc.setTextColor(varToRGB('--primary-color').r, varToRGB('--primary-color').g, varToRGB('--primary-color').b); doc.text('Optimized Workload Distribution Report', 40, 60); doc.setFontSize(11); doc.setTextColor(100); doc.text(`Strategy Used: ${strategyText}`, 40, 80); doc.text(`Generated on: ${new Date().toLocaleString()}`, 40, 95); let yPos = 120; if (html2canvas && loadChartContainer.offsetHeight > 0) { try { const canvas = await html2canvas(loadChartContainer, { scale: 2, backgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--white-bg').trim() }); const imgData = canvas.toDataURL('image/png'); const imgProps = doc.getImageProperties(imgData); const pdfWidth = doc.internal.pageSize.getWidth() - 80; const imgHeight = (imgProps.height * pdfWidth) / imgProps.width; doc.setFontSize(12); doc.setTextColor(varToRGB('--primary-color').r, varToRGB('--primary-color').g, varToRGB('--primary-color').b); doc.text(chartTitle.textContent, 40, yPos); yPos += 15; doc.addImage(imgData, 'PNG', 40, yPos, pdfWidth, imgHeight); yPos += imgHeight + 20; } catch (error) { console.error("Error generating chart image for PDF:", error); doc.setFontSize(10); doc.setTextColor(255,0,0); doc.text("Chart image could not be generated.", 40, yPos); yPos += 15; } } memberWorkloads.forEach(member => { if (yPos > doc.internal.pageSize.getHeight() - 120) { doc.addPage(); yPos = 40; } doc.setFontSize(14); doc.setTextColor(varToRGB('--accent-color').r, varToRGB('--accent-color').g, varToRGB('--accent-color').b); doc.text(`${member.name} (Capacity: ${member.capacity.toFixed(1)})`, 40, yPos); yPos += 10; const summaryLines = [ `Total Tasks: ${member.taskCount}`, `Total Raw Effort: ${member.totalEffort.toFixed(1)} units`, `Capacity-Adjusted Load: ${member.capacityAdjustedLoad.toFixed(2)}` ]; doc.setFontSize(10); doc.setTextColor(50); summaryLines.forEach(line => { doc.text(line, 40, yPos); yPos += 12; }); yPos += 3; // Extra space before table if (member.assignedTasks.length > 0) { const taskTableBody = member.assignedTasks.map(task => [ task.description, task.effort.toFixed(1), task.priority, task.dueDate ? new Date(task.dueDate+"T00:00:00").toLocaleDateString() : 'N/A' ]); doc.autoTable({ startY: yPos, head: [['Task', 'Effort', 'Priority', 'Due Date']], body: taskTableBody, theme: 'striped', headStyles: { fillColor: varToRGB('--secondary-color', true) }, margin: { left: 40, right: 40 }, didParseCell: function (data) { const priorityColorsPDF = { high: getComputedStyle(document.documentElement).getPropertyValue('--danger-color').trim(), medium: getComputedStyle(document.documentElement).getPropertyValue('--warning-color').trim(), low: getComputedStyle(document.documentElement).getPropertyValue('--info-color').trim(), }; if (data.column.index === 2 && data.cell.section === 'body') { // Priority column const priorityValue = data.cell.raw.toString().toLowerCase(); if (priorityColorsPDF[priorityValue]) { data.cell.styles.textColor = priorityColorsPDF[priorityValue]; // This sets text color as hex string data.cell.styles.fontStyle = 'bold'; } } } }); yPos = doc.lastAutoTable.finalY + 15; } else { doc.text('No tasks assigned.', 45, yPos); yPos += 15; } }); doc.save('Optimized_Workload_Report.pdf'); }); function varToRGB(varName, asArray = false) { const colorHex = getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); const r = parseInt(colorHex.slice(1, 3), 16); const g = parseInt(colorHex.slice(3, 5), 16); const b = parseInt(colorHex.slice(5, 7), 16); return asArray ? [r,g,b] : {r,g,b}; } // --- Initializations --- renderOptTeamList(); renderOptTaskList(); resetOptTaskForm(); })();
      Scroll to Top