Team Performance Tracker

Team Performance Tracker

Manage Team Members

Current Team:

    Add New Task

    Task List

      Report Configuration

      Last Updated: ${formatDateTime(task.lastUpdatedAt)}

      `; 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 })();
      Scroll to Top