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();
})();