Work Session Length Analyzer

Work Session Length Analyzer

Log New Work Session

Recently Logged Sessions (Last 10)

    Analysis Configuration

    '); return; } let totalDurationSum = 0, totalFocusSum = 0, focusRatedSessions = 0, totalPlannedSum = 0, plannedRatedSessions = 0; const lengthBuckets = { "<30m": 0, "30-60m": 0, "61-90m": 0, "91-120m": 0, ">120m": 0 }; const focusByLength = { "<30m": {sum:0, count:0}, "30-60m": {sum:0, count:0}, "61-90m": {sum:0, count:0}, "91-120m": {sum:0, count:0}, ">120m": {sum:0, count:0} }; const sessionsByHour = Array(24).fill(0); const lengthByDayOfWeek = Array(7).fill(null).map(()=>({sum:0,count:0})); filteredLogs.forEach(log => { totalDurationSum += log.durationMinutes; if (log.focusLevel && log.focusLevel > 0) { totalFocusSum += log.focusLevel; focusRatedSessions++; } if (log.plannedDurationMinutes !== null) { totalPlannedSum += log.plannedDurationMinutes; plannedRatedSessions++; } let bucketKey; if (log.durationMinutes < 30) bucketKey = "<30m"; else if (log.durationMinutes <= 60) bucketKey = "30-60m"; else if (log.durationMinutes <= 90) bucketKey = "61-90m"; else if (log.durationMinutes <= 120) bucketKey = "91-120m"; else bucketKey = ">120m"; lengthBuckets[bucketKey]++; if (log.focusLevel && log.focusLevel > 0) { focusByLength[bucketKey].sum += log.focusLevel; focusByLength[bucketKey].count++; } sessionsByHour[new Date(log.date + "T" + log.startTime).getHours()]++; lengthByDayOfWeek[new Date(log.date + "T00:00:00").getDay()].sum += log.durationMinutes; lengthByDayOfWeek[new Date(log.date + "T00:00:00").getDay()].count++; }); totalSessionsSpan.textContent = filteredLogs.length; totalProductiveTimeSpan.textContent = formatDurationFromMinutes(totalDurationSum); avgSessionLengthSpan.textContent = formatDurationFromMinutes(filteredLogs.length > 0 ? totalDurationSum / filteredLogs.length : 0); avgFocusLevelSpan.textContent = focusRatedSessions > 0 ? `${(totalFocusSum / focusRatedSessions).toFixed(1)}/5 ★` : "N/A"; const timeVariance = plannedRatedSessions > 0 ? totalDurationSum - totalPlannedSum : 0; overallTimeVarianceSpan.textContent = formatDurationFromMinutes(timeVariance); overallTimeVarianceSpan.style.color = timeVariance === 0 ? 'var(--dark-text)' : (timeVariance > 0 ? 'var(--danger-color)' : 'var(--accent-color)'); renderGenericBarChart(sessionLengthDistChartDiv, lengthBuckets, "Sessions"); const avgFocusData = {}; Object.keys(focusByLength).forEach(k => avgFocusData[k] = focusByLength[k].count > 0 ? (focusByLength[k].sum / focusByLength[k].count) : 0); renderGenericBarChart(focusByLengthChartDiv, avgFocusData, "Avg Focus", 5); const sessionsByHourData = {}; sessionsByHour.forEach((c, hr) => { if(c>0) sessionsByHourData[`${String(hr).padStart(2,'0')}:00`] = c; }); renderGenericBarChart(sessionsByHourChartDiv, sessionsByHourData, "Sessions"); const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; const avgLengthByDayData = {}; dayNames.forEach((n, i) => avgLengthByDayData[n] = lengthByDayOfWeek[i].count > 0 ? (lengthByDayOfWeek[i].sum / lengthByDayOfWeek[i].count) : 0); renderGenericBarChart(avgLengthByDayOfWeekChartDiv, avgLengthByDayData, "Avg Mins"); analysisOutputDiv.style.display = 'block'; }); function renderGenericBarChart(chartElement, data, valueSuffix = "", YAxisMaxValue = null) { chartElement.innerHTML = ''; const keys = Object.keys(data); if (keys.length === 0 || keys.every(k => data[k] === 0 && valueSuffix !== "Avg Focus")) { chartElement.innerHTML = `

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