`;
tasksListUL.appendChild(li);
});
tasksListUL.querySelectorAll('.edit-task-btn').forEach(btn => btn.addEventListener('click', (e) => populateTaskFormForEdit(e.target.dataset.id)));
tasksListUL.querySelectorAll('.delete-task-btn').forEach(btn => btn.addEventListener('click', (e) => deleteTask(e.target.dataset.id)));
}
filterTaskListStatusSelect.addEventListener('change', renderTaskList);
filterTaskListAssigneeSelect.addEventListener('change', renderTaskList);
// --- Performance Report Logic ---
reportPeriodSelect.addEventListener('change', () => {
customReportRangeDiv.style.display = reportPeriodSelect.value === 'custom' ? 'flex' : 'none';
});
function setDefaultReportDates(){
if(!reportStartDateInput.value) { // Set defaults if empty (e.g. on first tab view)
const weekStart = getWeekStart(today);
const weekEnd = new Date(weekStart);
weekEnd.setDate(weekStart.getDate() + 6);
reportStartDateInput.value = new Date(weekStart.getTime() - weekStart.getTimezoneOffset() * 60000).toISOString().slice(0,10);
reportEndDateInput.value = new Date(weekEnd.getTime() - weekEnd.getTimezoneOffset() * 60000).toISOString().slice(0,10);
}
}
generatePerformanceReportBtn.addEventListener('click', () => {
let startDate, endDate;
const period = reportPeriodSelect.value;
const todayNorm = new Date(todayStr + "T00:00:00");
switch(period) {
case 'today': startDate = new Date(todayNorm); endDate = new Date(todayNorm); break;
case 'thisWeek': startDate = getWeekStart(todayNorm); endDate = new Date(startDate); endDate.setDate(startDate.getDate() + 6); break;
case 'lastWeek':
const lastWeekStartSeed = new Date(todayNorm); lastWeekStartSeed.setDate(todayNorm.getDate() - 7);
startDate = getWeekStart(lastWeekStartSeed); endDate = new Date(startDate); endDate.setDate(startDate.getDate() + 6);
break;
case 'thisMonth': startDate = new Date(todayNorm.getFullYear(), todayNorm.getMonth(), 1); endDate = new Date(todayNorm.getFullYear(), todayNorm.getMonth() + 1, 0); break;
case 'lastMonth': startDate = new Date(todayNorm.getFullYear(), todayNorm.getMonth() - 1, 1); endDate = new Date(todayNorm.getFullYear(), todayNorm.getMonth(), 0); break;
case 'custom':
if (!reportStartDateInput.value || !reportEndDateInput.value) { alert('Select custom date range.'); return; }
startDate = new Date(reportStartDateInput.value + "T00:00:00"); endDate = new Date(reportEndDateInput.value + "T00:00:00");
if (startDate > endDate) { alert('Start date cannot be after end date.'); return; }
break;
default: return;
}
endDate.setHours(23,59,59,999); // Ensure end date is inclusive for the whole day
reportPeriodDisplay.textContent = `${formatDate(startDate.toISOString().slice(0,10))} - ${formatDate(endDate.toISOString().slice(0,10))}`;
let teamTotalTasksCompleted = 0;
let teamTotalValueDelivered = 0;
let teamTotalTasksWithDueDateCompleted = 0;
let teamTotalTasksCompletedOnTime = 0;
const memberStats = {};
teamMembers.forEach(m => memberStats[m.id] = { name:m.name, tasksCompleted:0, valueDelivered:0, onTimeTasks:0, totalDueTasksCompleted:0, inProgress:0, blocked:0 });
if(!teamMembers.find(m => m.id === "")) memberStats[""] = { name:"Unassigned", tasksCompleted:0, valueDelivered:0, onTimeTasks:0, totalDueTasksCompleted:0, inProgress:0, blocked:0 };
teamTasks.forEach(task => {
const assigneeId = task.assigneeId || ""; // Group unassigned
if (!memberStats[assigneeId] && assigneeId === "") { // Handle case where "Unassigned" might not be in teamMembers list
memberStats[""] = { name:"Unassigned", tasksCompleted:0, valueDelivered:0, onTimeTasks:0, totalDueTasksCompleted:0, inProgress:0, blocked:0 };
} else if (!memberStats[assigneeId] && assigneeId !== "") {
return; // Skip task if assignee no longer exists and not unassigned
}
if (task.status === 'completed' && task.completionDate) {
const completionDate = new Date(task.completionDate + "T00:00:00");
if (completionDate >= startDate && completionDate <= endDate) {
teamTotalTasksCompleted++;
teamTotalValueDelivered += task.value;
memberStats[assigneeId].tasksCompleted++;
memberStats[assigneeId].valueDelivered += task.value;
if (task.dueDate) {
teamTotalTasksWithDueDateCompleted++;
memberStats[assigneeId].totalDueTasksCompleted++;
const dueDate = new Date(task.dueDate + "T00:00:00");
if (completionDate <= dueDate) {
teamTotalTasksCompletedOnTime++;
memberStats[assigneeId].onTimeTasks++;
}
}
}
} else if (task.status === 'inprogress') {
memberStats[assigneeId].inProgress++;
} else if (task.status === 'blocked') {
memberStats[assigneeId].blocked++;
}
});
teamTasksCompletedSpan.textContent = teamTotalTasksCompleted;
teamValueDeliveredSpan.textContent = teamTotalValueDelivered;
teamOnTimeRateSpan.textContent = teamTotalTasksWithDueDateCompleted > 0 ? `${((teamTotalTasksCompletedOnTime / teamTotalTasksWithDueDateCompleted) * 100).toFixed(1)}%` : 'N/A';
// Overall Status Chart (All non-cancelled/completed tasks OR tasks completed in period)
const overallStatusCounts = {};
Object.keys(STATUS_OPTIONS_PERFORMANCE).forEach(key => overallStatusCounts[key] = 0);
teamTasks.forEach(task => { // This chart shows current state of all tasks
if (task.status !== 'cancelled') overallStatusCounts[task.status]++;
});
renderBarChart(overallStatusChartDiv, overallStatusCounts, STATUS_OPTIONS_PERFORMANCE);
// Value Per Member Chart
const valuePerMemberData = {};
teamMembers.forEach(m => valuePerMemberData[m.name] = memberStats[m.id].valueDelivered);
if(memberStats[""] && memberStats[""].valueDelivered > 0) valuePerMemberData["Unassigned"] = memberStats[""].valueDelivered;
renderBarChart(valuePerMemberChartDiv, valuePerMemberData, null, varToRGB('--accent-color', false)); // Use accent color for value bars
// Individual Performance Table
individualPerformanceTbody.innerHTML = '';
Object.values(memberStats).forEach(stats => {
if(!stats.name) return; // Skip if somehow a stat entry has no name (shouldn't happen)
const onTimeRate = stats.totalDueTasksCompleted > 0 ? `${((stats.onTimeTasks / stats.totalDueTasksCompleted) * 100).toFixed(1)}%` : 'N/A';
const row = individualPerformanceTbody.insertRow();
row.insertCell().textContent = stats.name;
row.insertCell().textContent = stats.tasksCompleted;
row.insertCell().textContent = stats.valueDelivered;
row.insertCell().textContent = onTimeRate;
row.insertCell().textContent = stats.inProgress;
row.insertCell().textContent = stats.blocked;
});
reportOutputSection.style.display = 'block';
});
function renderBarChart(chartElement, data, colorMapping = null, defaultBarColor = null) {
chartElement.innerHTML = '';
let maxValue = 0;
Object.values(data).forEach(val => { if (val > maxValue) maxValue = val; });
if (maxValue === 0) maxValue = 1; // Avoid division by zero
for (const key in data) {
const value = data[key];
const barGroup = document.createElement('div');
barGroup.className = 'chart-bar-group';
const bar = document.createElement('div');
bar.className = 'chart-bar';
bar.style.height = `${(value / maxValue) * 100}%`;
bar.style.backgroundColor = colorMapping ? colorMapping[key]?.color : (defaultBarColor ? `rgb(${defaultBarColor.r},${defaultBarColor.g},${defaultBarColor.b})` : 'var(--primary-color)');
bar.textContent = value > 0 ? value : '';
bar.title = `${colorMapping ? colorMapping[key]?.label || key : key}: ${value}`;
const label = document.createElement('div');
label.className = 'chart-label';
label.textContent = colorMapping ? colorMapping[key]?.label || key : key;
barGroup.appendChild(bar);
barGroup.appendChild(label);
chartElement.appendChild(barGroup);
}
}
// --- PDF Download ---
downloadPerformancePdfBtn.addEventListener('click', async () => {
if(reportOutputSection.style.display === 'none'){ alert("Generate a report first."); return; }
const { jsPDF } = window.jspdf;
const doc = new jsPDF('p', 'pt', 'a4');
const margin = 40; let yPos = margin;
const pageWidth = doc.internal.pageSize.getWidth();
doc.setFontSize(18); doc.setTextColor(varToRGB('--primary-color').r, varToRGB('--primary-color').g, varToRGB('--primary-color').b);
doc.text('Team Performance Report', pageWidth / 2, yPos, { align: 'center' }); yPos += 20;
doc.setFontSize(11); doc.setTextColor(100);
doc.text(`Period: ${reportPeriodDisplay.textContent}`, pageWidth / 2, yPos, { align: 'center' }); yPos += 15;
doc.text(`Generated: ${new Date().toLocaleString()}`, pageWidth / 2, yPos, { align: 'center' }); yPos += 25;
const summaryItems = [
["Tasks Completed:", teamTasksCompletedSpan.textContent],
["Value Delivered:", teamValueDeliveredSpan.textContent],
["On-Time Rate:", teamOnTimeRateSpan.textContent]
];
doc.autoTable({ startY: yPos, head:[['Team Metric', 'Value']], body: summaryItems, theme: 'plain', styles: {fontSize:10}, headStyles:{fillColor:[220,220,220], textColor:20}, columnStyles:{0:{fontStyle:'bold'}}});
yPos = doc.lastAutoTable.finalY + 15;
async function addChartToPdf(chartContainerId, titleText, currentY) {
const chartContainer = document.getElementById(chartContainerId);
if (html2canvas && chartContainer && chartContainer.offsetHeight > 0) {
if (currentY > doc.internal.pageSize.getHeight() - 150) { doc.addPage(); currentY = margin; } // Check for page break
doc.setFontSize(12); doc.setTextColor(varToRGB('--primary-color').r, varToRGB('--primary-color').g, varToRGB('--primary-color').b);
doc.text(titleText, margin, currentY); currentY += 15;
try {
const canvas = await html2canvas(chartContainer.querySelector('.chart'), { scale: 1.5, backgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--white-bg').trim() });
const imgData = canvas.toDataURL('image/png');
const imgProps = doc.getImageProperties(imgData);
const pdfImgWidth = pageWidth - 2 * margin;
const pdfImgHeight = (imgProps.height * pdfImgWidth) / imgProps.width;
doc.addImage(imgData, 'PNG', margin, currentY, pdfImgWidth, pdfImgHeight);
currentY += pdfImgHeight + 10;
} catch(e) { console.error("Chart to PDF error:", e); doc.setTextColor(255,0,0).text("Chart could not be rendered.", margin, currentY); currentY+=15; }
}
return currentY;
}
yPos = await addChartToPdf('overallStatusChartContainer', 'Overall Task Status Distribution', yPos);
yPos = await addChartToPdf('valuePerMemberChartContainer', 'Value Delivered per Member', yPos);
if (yPos > doc.internal.pageSize.getHeight() - 100) { doc.addPage(); yPos = margin; }
doc.setFontSize(12); doc.setTextColor(varToRGB('--primary-color').r, varToRGB('--primary-color').g, varToRGB('--primary-color').b);
doc.text('Individual Member Performance', margin, yPos); yPos += 15;
const individualData = [];
individualPerformanceTbody.querySelectorAll('tr').forEach(row => {
const cells = Array.from(row.querySelectorAll('td')).map(td => td.textContent);
individualData.push(cells);
});
if(individualData.length > 0){
doc.autoTable({
startY: yPos,
head: [['Member', 'Tasks Done', 'Value Delivered', 'On-Time %', 'In Progress', 'Blocked']],
body: individualData, theme: 'grid', headStyles: { fillColor: varToRGB('--primary-color', true), textColor: varToRGB('--light-text', true) },
styles:{fontSize:9}
});
} else { doc.text("No individual data to display for this period.", margin, yPos); }
doc.save(`Team_Performance_Report_${reportPeriodDisplay.textContent.replace(/\s-\s/g,'_to_')}.pdf`);
});
function varToRGB(varName, asArray = false) {
const colorHex = getComputedStyle(document.documentElement).getPropertyValue(varName).trim();
if (!colorHex.startsWith('#')) { return asArray ? [0,0,0] : {r:0,g:0,b:0}; }
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 ---
renderTeamMemberList();
updateAssigneeDropdowns(); // Populate for task form and filters
renderTaskList(); // Render tasks and also set up task list filters
setDefaultReportDates(); // Set default dates for report period selector
})();