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