Task Interruption Monitor

Task Interruption Monitor

Today's Interruption Log (0)

    Add New Interruption Type

    My Interruption Types

      Filter Report Data

      ` : ''} ${log.notes ? `

      Notes: ${log.notes}

      ` : ''}
      `; todaysInterruptionsListUL.appendChild(li); }); todaysInterruptionsListUL.querySelectorAll('.edit-log-btn').forEach(btn => btn.addEventListener('click', (e) => openLogModal(e.target.dataset.id))); todaysInterruptionsListUL.querySelectorAll('.delete-log-btn').forEach(btn => { btn.addEventListener('click', (e) => { if (confirm('Delete this log entry?')) { interruptionLog = interruptionLog.filter(log => log.id !== e.target.dataset.id); saveLog(); renderTodaysInterruptions(); } }); }); } // --- Analysis & Report Logic --- analysisDateRangeSelect.addEventListener('change', function() { customReportRangeGroup.style.display = this.value === 'custom' ? 'flex' : 'none'; if (this.value !== 'custom') generateAnalysisBtn.click(); }); function setDefaultAnalysisDates() { const today = new Date(); const todayISO = new Date(today.getTime() - today.getTimezoneOffset() * 60000).toISOString().slice(0,10); if(!reportEndDateInput.value) reportEndDateInput.value = todayISO; const sevenDaysAgo = new Date(today); sevenDaysAgo.setDate(today.getDate() - 6); if(!reportStartDateInput.value) reportStartDateInput.value = new Date(sevenDaysAgo.getTime() - sevenDaysAgo.getTimezoneOffset() * 60000).toISOString().slice(0,10); } generateAnalysisBtn.addEventListener('click', () => { let startDate, endDate; const period = analysisDateRangeSelect.value; const todayObjForAnalysis = new Date(todayStr + "T00:00:00"); switch(period) { case 'today': startDate = new Date(todayObjForAnalysis); endDate = new Date(todayObjForAnalysis); break; case 'yesterday': startDate = new Date(todayObjForAnalysis); startDate.setDate(todayObjForAnalysis.getDate() - 1); endDate = new Date(startDate); break; case 'last7days': endDate = new Date(todayObjForAnalysis); startDate = new Date(todayObjForAnalysis); startDate.setDate(todayObjForAnalysis.getDate() - 6); break; case 'last30days': endDate = new Date(todayObjForAnalysis); startDate = new Date(todayObjForAnalysis); startDate.setDate(todayObjForAnalysis.getDate() - 29); 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); analysisPeriodDisplaySpan.textContent = `${formatDate(startDate.toISOString().slice(0,10))} - ${formatDate(endDate.toISOString().slice(0,10))}`; const filteredLogs = interruptionLog.filter(log => { const logDate = new Date(log.timestamp); // Full timestamp for accurate filtering return logDate >= startDate && logDate <= endDate; }); reportTotalInterruptionsSpan.textContent = filteredLogs.length; let totalTimeLostMins = 0; filteredLogs.forEach(log => { if (log.durationMinutes) totalTimeLostMins += log.durationMinutes; }); reportTotalTimeLostSpan.textContent = formatDuration(totalTimeLostMins); const typeCounts = {}; filteredLogs.forEach(log => typeCounts[log.type] = (typeCounts[log.type] || 0) + 1); renderGenericBarChartIM(typeDistractionChartDiv, typeCounts, "Type"); const hourCounts = Array(24).fill(0); filteredLogs.forEach(log => hourCounts[new Date(log.timestamp).getHours()]++); const hourDataForChart = {}; hourCounts.forEach((count, hr) => { if(count > 0) hourDataForChart[`${String(hr).padStart(2,'0')}:00`] = count; }); renderGenericBarChartIM(timeOfDayDistractionChartDiv, hourDataForChart, "Hour"); reportDetailedLogListUL.innerHTML = ''; const recentLogsForReport = filteredLogs.sort((a,b) => new Date(b.timestamp) - new Date(a.timestamp)).slice(0,100); if(recentLogsForReport.length === 0){ reportDetailedLogListUL.innerHTML = '
    • No interruptions logged in this period.
    • ';} recentLogsForReport.forEach(log => { const li = document.createElement('li'); li.className = 'list-item'; li.innerHTML = `

      ${formatTime(log.timestamp)} (${formatDate(log.timestamp.slice(0,10))}) - ${log.type} ${log.durationMinutes ? ` (~${formatDuration(log.durationMinutes)})` : ''}

      ${log.taskBeingWorkedOn ? `

      During: ${log.taskBeingWorkedOn}

      ` : ''} ${log.notes ? `

      Notes: ${log.notes}

      ` : ''}
      `; reportDetailedLogListUL.appendChild(li); }); analysisOutputDiv.style.display = 'block'; }); function renderGenericBarChartIM(chartElement, data, labelPrefix = "") { chartElement.innerHTML = ''; const keys = Object.keys(data); if (keys.length === 0) { chartElement.innerHTML = `

      No data for this period.

      `; return; } let maxValue = 0; Object.values(data).forEach(val => { if (val > maxValue) maxValue = val; }); if (maxValue === 0) maxValue = 1; const sortedKeys = Object.keys(data).sort((a,b) => { if (labelPrefix === "Hour") return parseInt(a.split(':')[0]) - parseInt(b.split(':')[0]); return data[b] - data[a]; // Sort categories by count desc }); sortedKeys.forEach((key, index) => { 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}%`; const colors = [varToRGB('--info-color'), varToRGB('--accent-color',true), varToRGB('--primary-color'), varToRGB('--warning-color', true), varToRGB('--danger-color', true), varToRGB('--secondary-color', true), {r:150,g:100,b:200}]; // Added more for variety const barColor = colors[index % colors.length]; if(typeof barColor === 'string') bar.style.backgroundColor = barColor; else bar.style.backgroundColor = `rgb(${barColor.r},${barColor.g},${barColor.b})`; bar.textContent = value; bar.title = `${key}: ${value}`; const label = document.createElement('div'); label.className = 'chart-label'; label.textContent = key; barGroup.appendChild(bar); barGroup.appendChild(label); chartElement.appendChild(barGroup); }); } // --- PDF Download --- downloadInterruptionReportPdfBtn.addEventListener('click', async () => { if (analysisOutputDiv.style.display === 'none') { alert('Generate an analysis 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('Task Interruption Report', pageWidth / 2, yPos, { align: 'center' }); yPos += 20; doc.setFontSize(11); doc.setTextColor(100); doc.text(`Period: ${analysisPeriodDisplaySpan.textContent}`, pageWidth / 2, yPos, { align: 'center' }); yPos += 15; doc.text(`Generated: ${new Date().toLocaleString()}`, pageWidth / 2, yPos, { align: 'center' }); yPos += 25; doc.setFontSize(10); doc.text(`Total Interruptions: ${reportTotalInterruptionsSpan.textContent}`, margin, yPos); yPos += 15; doc.text(`Total Time Lost: ${reportTotalTimeLostSpan.textContent}`, margin, yPos); yPos += 20; async function addChartToPdf(chartContainerId, titleText, currentY) { /* Same as other tools */ const chartContainer = document.getElementById(chartContainerId); if (html2canvas && chartContainer && chartContainer.querySelector('.chart')?.children.length > 0 && !chartContainer.querySelector('.chart p')) { if (currentY > doc.internal.pageSize.getHeight() - 200) { doc.addPage(); currentY = margin; } 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('--light-bg').trim() }); const imgData = canvas.toDataURL('image/png'); const imgProps = doc.getImageProperties(imgData); const pdfImgWidth = pageWidth - 2 * margin; const pdfImgHeight = Math.min(180, (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('typeChartContainer', 'Interruptions by Type', yPos); yPos = await addChartToPdf('timeOfDayChartContainerIM', 'Interruptions by Hour of Day', yPos); if (yPos > doc.internal.pageSize.getHeight() - 80) { doc.addPage(); yPos = margin; } doc.setFontSize(12); doc.setTextColor(varToRGB('--primary-color').r, varToRGB('--primary-color').g, varToRGB('--primary-color').b); doc.text('Detailed Interruption Log (Filtered Period)', margin, yPos); yPos += 18; let startDatePdf, endDatePdf; /* ... (get dates like in generateAnalysisBtn) ... */ const periodPdf = analysisDateRangeSelect.value; const todayObjPdf = new Date(todayStr + "T00:00:00"); switch(periodPdf) { case 'today': startDatePdf = new Date(todayObjPdf); endDatePdf = new Date(todayObjPdf); break; case 'yesterday': startDatePdf = new Date(todayObjPdf); startDatePdf.setDate(todayObjPdf.getDate() - 1); endDatePdf = new Date(startDatePdf); break; case 'last7days': endDatePdf = new Date(todayObjPdf); startDatePdf = new Date(todayObjPdf); startDatePdf.setDate(todayObjPdf.getDate() - 6); break; case 'last30days': endDatePdf = new Date(todayObjPdf); startDatePdf = new Date(todayObjPdf); startDatePdf.setDate(todayObjPdf.getDate() - 29); break; case 'custom': startDatePdf = new Date(reportStartDateInput.value + "T00:00:00"); endDatePdf = new Date(reportEndDateInput.value + "T00:00:00"); break; } if(endDatePdf) endDatePdf.setHours(23,59,59,999); const logsForPdf = interruptionLog.filter(log => { const logDate = new Date(log.timestamp); return logDate >= startDatePdf && logDate <= endDatePdf; }) .sort((a,b) => new Date(a.timestamp) - new Date(b.timestamp)); const detailedBody = logsForPdf.map(log => [ formatDate(log.timestamp.slice(0,10)) + " " + formatTime(log.timestamp), log.type, log.taskBeingWorkedOn || '', formatDuration(log.durationMinutes), log.notes || '' ]); if (detailedBody.length > 0) { doc.autoTable({ startY: yPos, head: [['Timestamp', 'Type', 'Task Worked On', 'Duration', 'Notes']], body: detailedBody, theme: 'grid', headStyles: { fillColor: varToRGB('--primary-color', true), textColor: varToRGB('--light-text', true) }, styles:{fontSize:8, cellPadding:3}, columnStyles: {0:{cellWidth:80},1:{cellWidth:80}, 2:{cellWidth:100}, 3:{cellWidth:50}, 4:{cellWidth:'auto'}} }); } else { doc.text("No detailed logs for this period.", margin, yPos); } doc.save(`Task_Interruption_Report_${analysisPeriodDisplaySpan.textContent.replace(/\s-\s/g,'_to_')}.pdf`); }); function varToRGB(varName, asArray = false) { let colorHex = getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); if (!colorHex.startsWith('#')) { const namedColors = {"purple":"#800080", "lightpurple": "#AF7AC5"}; const hexFromName = namedColors[colorHex.toLowerCase().replace(/\s+/g, '')]; // remove spaces for matching if(hexFromName) colorHex = hexFromName; else 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 --- if (interruptionTypes.length === 0) { interruptionTypes = [...DEFAULT_INTERRUPTION_TYPES]; saveTypes(); } renderInterruptionTypesList(); renderTodaysInterruptions(); setDefaultAnalysisDates(); showTab(0); // Ensure correct initial tab and button states for nav if (interruptionLog.length > 0) generateAnalysisBtn.click(); else analysisOutputDiv.style.display = 'none'; })();
      Scroll to Top