Work Session Length Analyzer
Log Work Sessions
Analyze Session Data
Log New Work Session
☆
☆
☆
☆
☆
Recently Logged Sessions (Last 10)
Analysis Configuration
Analysis for Period:
Total Sessions Logged: 0
Total Productive Time: 0h 0m
Average Session Length: 0m
Average Focus Level: N/A
Overall Time Variance (Actual - Planned): 0h 0m
Distribution of Session Lengths
Avg. Focus by Session Length
Sessions by Hour of Day (Start Time)
Avg. Session Length by Day of Week
No data for this period.
No data for this period.
`; return; } let maxValue = YAxisMaxValue !== null ? YAxisMaxValue : 0; if(YAxisMaxValue === null) { Object.values(data).forEach(val => { if (val > maxValue) maxValue = val; }); if (maxValue === 0) maxValue = 1; } keys.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'), varToRGB('--primary-color'), varToRGB('--warning-color', true), varToRGB('--danger-color', true), varToRGB('--secondary-color', true)]; 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.toFixed(valueSuffix === "Avg Focus" || valueSuffix === "Avg Mins" ? 1:0); bar.title = `${key}: ${value.toFixed(1)} ${valueSuffix}`; const label = document.createElement('div'); label.className = 'chart-label'; label.textContent = key; barGroup.appendChild(bar); barGroup.appendChild(label); chartElement.appendChild(barGroup); }); } // --- PDF Download --- downloadAnalysisReportPdfBtn.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('Work Session Analysis 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; const summaryPdfData = [ ['Total Sessions Logged:', totalSessionsSpan.textContent], ['Total Productive Time:', totalProductiveTimeSpan.textContent], ['Average Session Length:', avgSessionLengthSpan.textContent], ['Average Focus Level:', avgFocusLevelSpan.textContent], ['Overall Time Variance (Actual - Planned):', overallTimeVarianceSpan.textContent] ]; doc.autoTable({ startY: yPos, body: summaryPdfData, theme: 'plain', styles: {fontSize:10}, columnStyles: {0:{fontStyle:'bold', cellWidth:250}}}); yPos = doc.lastAutoTable.finalY + 15; async function addChartToPdf(chartContainerId, titleText, currentY) { /* ... (same as previous tool) ... */ const chartContainer = document.getElementById(chartContainerId); if (html2canvas && chartContainer && chartContainer.querySelector('.chart')?.children.length > 0 && !chartContainer.querySelector('.chart p')) { // check if not "no data" 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('sessionLengthDistChartContainer', 'Distribution of Session Lengths', yPos); yPos = await addChartToPdf('focusByLengthChartContainer', 'Average Focus by Session Length', yPos); yPos = await addChartToPdf('sessionsByHourChartContainer', 'Sessions by Hour of Day (Start Time)', yPos); yPos = await addChartToPdf('avgLengthByDayOfWeekChartContainer', 'Average Session Length by Day of Week', yPos); // Detailed Log for PDF let startDatePdf, endDatePdf; /* ... (get dates like in analyzeSessionsBtn) ... */ const periodPdf = analysisDateRangeSelect.value; const todayObjPdf = new Date(todayStr + "T00:00:00"); switch(periodPdf) { 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 'thisMonth': startDatePdf = new Date(todayObjPdf.getFullYear(), todayObjPdf.getMonth(), 1); endDatePdf = new Date(todayObjPdf.getFullYear(), todayObjPdf.getMonth() + 1, 0); break; case 'lastMonth': startDatePdf = new Date(todayObjPdf.getFullYear(), todayObjForAnalysis.getMonth() - 1, 1); endDatePdf = new Date(todayObjPdf.getFullYear(), todayObjPdf.getMonth(), 0); break; case 'custom': startDatePdf = new Date(analysisStartDateInput.value + "T00:00:00"); endDatePdf = new Date(analysisEndDateInput.value + "T00:00:00"); break; default: return; } if(endDatePdf) endDatePdf.setHours(23,59,59,999); const logsForPdf = workSessionLogs.filter(log => { const logDate = new Date(log.date + "T00:00:00"); return logDate >= startDatePdf && logDate <= endDatePdf; }) .sort((a,b) => new Date(a.date + "T" + a.startTime) - new Date(b.date + "T" + b.startTime)); if(logsForPdf.length > 0){ 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 Session Log:', margin, yPos); yPos += 18; const detailedBody = logsForPdf.map(log => [ formatDateForDisplay(log.date), `${formatTime(log.startTime)}-${formatTime(log.endTime)}`, log.description, formatDurationFromMinutes(log.durationMinutes), log.plannedDurationMinutes ? formatDurationFromMinutes(log.plannedDurationMinutes) : 'N/A', log.focusLevel > 0 ? `${log.focusLevel}/5` : 'N/A' ]); doc.autoTable({ startY: yPos, head: [['Date', 'Time', 'Description', 'Actual Dur.', 'Planned Dur.', 'Focus']], body: detailedBody, theme: 'grid', headStyles: { fillColor: varToRGB('--primary-color', true), textColor: varToRGB('--light-text', true) }, styles:{fontSize:8, cellPadding:3}, columnStyles: {2:{cellWidth:'auto'}} }); } doc.save(`Work_Session_Analysis_${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"}; const hexFromName = namedColors[colorHex.toLowerCase()]; 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 --- renderRecentSessionsList(); setDefaultAnalysisDates(); if (workSessionLogs.length > 0) analyzeSessionsBtn.click(); else analysisOutputDiv.style.display = 'none'; })();