Task Status Reporter

Task Status Reporter

Add New Task

Current Task List

    Report Configuration & Summary

    Assignee: ${task.assignee || 'N/A'} Due: ${formatDate(task.dueDate)} Prio: ${task.priority.charAt(0).toUpperCase() + task.priority.slice(1)} Status: ${STATUS_OPTIONS[task.status].label}

    ${notesPreview}

    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))); populateFilterDropdowns(); // Update filter dropdowns based on current tasks } // --- Filter Population --- function populateFilterDropdowns() { const uniqueAssignees = new Set(tasks.map(t => t.assignee || '').filter(a => a)); const uniqueStatuses = new Set(tasks.map(t => t.status)); // Status Filter (for task list) const currentStatusValList = filterTasksByStatusListSelect.value; filterTasksByStatusListSelect.innerHTML = ''; Object.keys(STATUS_OPTIONS).forEach(key => { if (uniqueStatuses.has(key) || Object.keys(STATUS_OPTIONS).length < 8) { // Show if tasks have this status or always show all filterTasksByStatusListSelect.innerHTML += ``; } }); filterTasksByStatusListSelect.value = currentStatusValList; // Assignee Filter (for task list) const currentAssigneeValList = filterTasksByAssigneeListSelect.value; filterTasksByAssigneeListSelect.innerHTML = ''; if (tasks.some(t => !t.assignee)) { // Add unassigned if any task is unassigned filterTasksByAssigneeListSelect.innerHTML += ``; } uniqueAssignees.forEach(assignee => { filterTasksByAssigneeListSelect.innerHTML += ``; }); filterTasksByAssigneeListSelect.value = currentAssigneeValList; } filterTasksByStatusListSelect.addEventListener('change', renderTasksList); filterTasksByAssigneeListSelect.addEventListener('change', renderTasksList); function populateReportFilters() { const uniqueAssignees = new Set(tasks.map(t => t.assignee || '').filter(a => a)); // Status Filter (for report tab) reportFilterStatusSelect.innerHTML = ''; Object.keys(STATUS_OPTIONS).forEach(key => { reportFilterStatusSelect.innerHTML += ``; }); // Assignee Filter (for report tab) reportFilterAssigneeSelect.innerHTML = ''; if (tasks.some(t => !t.assignee)) { reportFilterAssigneeSelect.innerHTML += ``; } uniqueAssignees.forEach(assignee => { reportFilterAssigneeSelect.innerHTML += ``; }); } // --- Report Generation --- generateReportBtn.addEventListener('click', () => { const statusFilter = reportFilterStatusSelect.value; const assigneeFilter = reportFilterAssigneeSelect.value; const filteredReportTasks = tasks.filter(task => (statusFilter === 'all' || task.status === statusFilter) && (assigneeFilter === 'all' || (task.assignee || '').toLowerCase() === assigneeFilter.toLowerCase() || (assigneeFilter === '_unassigned_' && !task.assignee)) ); reportTotalTasksSpan.textContent = filteredReportTasks.length; // Status Counts & Chart const statusCounts = {}; Object.keys(STATUS_OPTIONS).forEach(key => statusCounts[key] = 0); // Initialize all to 0 filteredReportTasks.forEach(task => statusCounts[task.status]++); reportStatusCountsDiv.innerHTML = ''; statusChartDiv.innerHTML = ''; let maxCount = 0; Object.keys(statusCounts).forEach(key => { if (statusCounts[key] > maxCount) maxCount = statusCounts[key]; }); if (maxCount === 0) maxCount = 1; // Avoid division by zero for chart for (const statusKey in statusCounts) { if (!STATUS_OPTIONS[statusKey]) continue; // Should not happen const count = statusCounts[statusKey]; const card = document.createElement('div'); card.className = 'summary-card'; card.innerHTML = `
    ${count}
    ${STATUS_OPTIONS[statusKey].label}
    `; reportStatusCountsDiv.appendChild(card); // Chart Bar const barGroup = document.createElement('div'); barGroup.className = 'chart-bar-group'; const bar = document.createElement('div'); bar.className = 'chart-bar'; bar.style.height = `${(count / maxCount) * 100}%`; bar.style.backgroundColor = STATUS_OPTIONS[statusKey].color; bar.textContent = count > 0 ? count : ''; bar.title = `${STATUS_OPTIONS[statusKey].label}: ${count}`; const label = document.createElement('div'); label.className = 'chart-label'; label.textContent = STATUS_OPTIONS[statusKey].label; barGroup.appendChild(bar); barGroup.appendChild(label); statusChartDiv.appendChild(barGroup); } // Overdue tasks const today = getTodayDateString(); const overdue = filteredReportTasks.filter(t => t.dueDate && t.dueDate < today && t.status !== 'done' && t.status !== 'cancelled'); overdueTasksUL.innerHTML = ''; if (overdue.length > 0) { overdueTasksListDiv.style.display = 'block'; overdue.forEach(task => { const li = document.createElement('li'); li.className = `task-list-item status-${task.status}`; li.innerHTML = `

    ${task.description}

    Assignee: ${task.assignee||'N/A'}, Due: ${formatDate(task.dueDate)}

    `; overdueTasksUL.appendChild(li); }); } else { overdueTasksListDiv.style.display = 'none'; } // Detailed Task List for Report View reportDetailedTasksUL.innerHTML = ''; const priorityOrder = { high: 1, medium: 2, low: 3 }; // For sorting report list filteredReportTasks.sort((a,b) => { // Group by status in report, then by due date if (a.status !== b.status) return STATUS_OPTIONS[a.status].label.localeCompare(STATUS_OPTIONS[b.status].label); const dateA = a.dueDate ? new Date(a.dueDate) : null; const dateB = b.dueDate ? new Date(b.dueDate) : null; if (dateA && dateB) { if (dateA - dateB !== 0) return dateA - dateB; } else if (dateA) return -1; else if (dateB) return 1; return priorityOrder[a.priority] - priorityOrder[b.priority]; }); if (filteredReportTasks.length > 0) { filteredReportTasks.forEach(task => { const li = document.createElement('li'); li.className = `task-list-item status-${task.status}`; const notesPreview = task.notes ? `

    Notes: ${task.notes.substring(0,100)}${task.notes.length > 100 ? '...' : ''}

    ` : ''; li.innerHTML = `

    ${task.description}

    Assignee: ${task.assignee || 'N/A'} Due: ${formatDate(task.dueDate)} Prio: ${task.priority.charAt(0).toUpperCase() + task.priority.slice(1)} Status: ${STATUS_OPTIONS[task.status].label}

    ${notesPreview}

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

    `; reportDetailedTasksUL.appendChild(li); }); } else { reportDetailedTasksUL.innerHTML = '
  • No tasks match the selected filters for the detailed report.
  • '; } reportSummaryOutputDiv.style.display = 'block'; reportDetailedListSectionDiv.style.display = 'block'; }); // --- PDF Download --- downloadReportPdfBtn.addEventListener('click', async () => { const { jsPDF } = window.jspdf; const doc = new jsPDF('p', 'pt', 'a4'); const statusFilter = reportFilterStatusSelect.value; const assigneeFilter = reportFilterAssigneeSelect.value; let filterText = "Filters: "; if(statusFilter !== 'all') filterText += `Status: ${STATUS_OPTIONS[statusFilter].label}; `; if(assigneeFilter !== 'all') filterText += `Assignee: ${assigneeFilter === '_unassigned_' ? '(Unassigned)' : assigneeFilter}; `; if(filterText === "Filters: ") filterText = "Filters: None applied"; doc.setFontSize(18); doc.setTextColor(varToRGB('--primary-color').r, varToRGB('--primary-color').g, varToRGB('--primary-color').b); doc.text('Task Status Report', 40, 60); doc.setFontSize(10); doc.setTextColor(100); doc.text(`Report Date: ${new Date().toLocaleString()}`, 40, 75); doc.text(filterText, 40, 90); let yPos = 110; // Summary Table const summaryData = [ ['Total Tasks (filtered)', reportTotalTasksSpan.textContent] ]; Object.keys(STATUS_OPTIONS).forEach(key => { const countCard = Array.from(reportStatusCountsDiv.children).find(card => card.querySelector('.label').textContent === STATUS_OPTIONS[key].label); if (countCard) summaryData.push([STATUS_OPTIONS[key].label, countCard.querySelector('.count').textContent]); }); doc.autoTable({ startY: yPos, head: [['Metric', 'Count']], body: summaryData, theme: 'striped', headStyles: { fillColor: varToRGB('--secondary-color', true)}, columnStyles: { 0: { fontStyle: 'bold' } } }); yPos = doc.lastAutoTable.finalY + 15; // Chart Image if (html2canvas && statusChartContainer.offsetHeight > 0 && reportSummaryOutputDiv.style.display !== 'none') { try { const canvas = await html2canvas(statusChartContainer, { scale: 1.5, 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; if (yPos + imgHeight > doc.internal.pageSize.getHeight() - 40) { doc.addPage(); yPos = 40; } doc.addImage(imgData, 'PNG', 40, yPos, pdfWidth, imgHeight); yPos += imgHeight + 15; } catch (error) { console.error("Error generating chart image for PDF:", error); if (yPos > doc.internal.pageSize.getHeight() - 40) { doc.addPage(); yPos = 40; } doc.setFontSize(10); doc.setTextColor(255,0,0); doc.text("Status chart image could not be generated.", 40, yPos); yPos += 15; } } // Detailed Task List const taskRows = []; reportDetailedTasksUL.querySelectorAll('.task-list-item').forEach(li => { if (li.textContent.startsWith("No tasks match")) return; // Skip the placeholder const desc = li.querySelector('.task-description')?.textContent || 'N/A'; const metaSpans = li.querySelectorAll('.task-meta span'); const assignee = metaSpans[0]?.textContent.replace('Assignee: ','') || 'N/A'; const due = metaSpans[1]?.textContent.replace('Due: ','') || 'N/A'; const prio = metaSpans[2]?.textContent.replace('Prio: ','') || 'N/A'; const status = metaSpans[3]?.textContent.replace('Status: ','') || 'N/A'; const notes = li.querySelector('.task-notes-preview')?.textContent.replace('Notes: ','') || ''; const updated = li.querySelector('.task-meta small')?.textContent.replace('Last Updated: ','') || ''; taskRows.push([desc, assignee, due, prio, status, updated, notes]); }); if (taskRows.length > 0) { if (yPos > doc.internal.pageSize.getHeight() - 60) { doc.addPage(); yPos = 40; } // Check for page break before table doc.setFontSize(12); doc.setTextColor(varToRGB('--primary-color').r, varToRGB('--primary-color').g, varToRGB('--primary-color').b); doc.text('Detailed Task List', 40, yPos); yPos += 15; doc.autoTable({ startY: yPos, head: [['Description', 'Assignee', 'Due', 'Prio', 'Status', 'Last Updated', 'Notes']], body: taskRows, theme: 'grid', headStyles: { fillColor: varToRGB('--primary-color', true), textColor: varToRGB('--light-text', true) }, columnStyles: { 0: {cellWidth: 100}, 6: {cellWidth: 120} } // Wider for desc and notes }); } else if (reportSummaryOutputDiv.style.display !== 'none') { // Only show if report was generated but no tasks if (yPos > doc.internal.pageSize.getHeight() - 40) { doc.addPage(); yPos = 40; } doc.text('No tasks to display in the detailed list for the current filters.', 40, yPos); } doc.save('Task_Status_Report.pdf'); }); function varToRGB(varName, asArray = false) { const colorHex = getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); if (!colorHex.startsWith('#')) { // It's a variable name like 'var(--some-color)' or a word like 'purple' console.warn(`Color for ${varName} is not a direct hex: ${colorHex}. PDF might use default.`); return asArray ? [0,0,0] : {r:0,g:0,b:0}; // Default to black } 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 --- // Populate status dropdowns in forms Object.keys(STATUS_OPTIONS).forEach(key => { taskStatusInput.innerHTML += ``; }); renderTasksList(); // This also populates filter dropdowns initially populateReportFilters(); // Populate report tab filters initially // Default form status taskStatusInput.value = 'todo'; taskPriorityInput.value = 'medium'; })();
    Scroll to Top