Time Budgeting Tool

Time Budgeting Tool

Budget Period & Settings

Manage Categories

My Categories:

    Allocate Budgeted Time for

    Total Budgeted: 0h 0m / Available: 8h 0m

    Track Actual Time Spent for

    Select a date and allocate budget on the first tab, then track actuals here to generate a report.

    '; return; } currentBudget.categories.sort((a,b) => a.name.localeCompare(b.name)); currentBudget.categories.forEach(catAllocation => { if (catAllocation.budgetedHours <= 0 && catAllocation.actualHours <=0) return; // Only show if budgeted or actuals exist const itemDiv = document.createElement('div'); itemDiv.className = 'actuals-tracking-item'; const { hours: budgetedH, minutes: budgetedM } = hoursToParts(catAllocation.budgetedHours); const { hours: actualH, minutes: actualM } = hoursToParts(catAllocation.actualHours); itemDiv.innerHTML = ` ${catAllocation.name} (Budgeted: ${budgetedH}h ${budgetedM}m)
    h m
    `; actualsTrackingListDiv.appendChild(itemDiv); }); actualsTrackingListDiv.querySelectorAll('.actual-hours-input, .actual-minutes-input').forEach(input => { input.addEventListener('change', (e) => { const catId = e.target.dataset.catId; const catBudget = timeBudgets[currentBudgetDateStr].categories.find(c => c.id === catId); if (catBudget) { const container = e.target.closest('.time-input'); const hoursInputEl = container.querySelector('.actual-hours-input'); const minutesInputEl = container.querySelector('.actual-minutes-input'); catBudget.actualHours = timeToHours(hoursInputEl.value, minutesInputEl.value); // Forcing save via button for actuals: saveTimeBudgets(); // generateAndDisplayReportSummary(); // Live update report as actuals change } }); }); // If any category has actuals or budget, try to display report if (currentBudget.categories.some(c => c.budgetedHours > 0 || c.actualHours > 0)) { generateAndDisplayReportSummary(); } } saveActualsBtn.addEventListener('click', () => { // Trigger save for any pending changes from input fields actualsTrackingListDiv.querySelectorAll('.actual-hours-input').forEach(input => { input.dispatchEvent(new Event('change', {bubbles:true})); // Ensure last changes are captured }); saveTimeBudgets(); generateAndDisplayReportSummary(); alert('Actual times saved and report updated!'); }); function generateAndDisplayReportSummary() { const currentBudget = timeBudgets[currentBudgetDateStr]; if (!currentBudget) { reportOutputSection.style.display = 'none'; noReportDataMsg.style.display = 'block'; return; } let totalBudgetedH = 0; let totalActualH = 0; const chartData = []; const tableRowsData = []; currentBudget.categories.forEach(cat => { if (cat.budgetedHours > 0 || cat.actualHours > 0) { totalBudgetedH += (cat.budgetedHours || 0); totalActualH += (cat.actualHours || 0); const variance = (cat.actualHours || 0) - (cat.budgetedHours || 0); chartData.push({ name: cat.name, budgeted: (cat.budgetedHours || 0), actual: (cat.actualHours || 0) }); tableRowsData.push({ name: cat.name, budgeted: (cat.budgetedHours || 0), actual: (cat.actualHours || 0), variance: variance }); } }); if (chartData.length === 0) { reportOutputSection.style.display = 'none'; noReportDataMsg.style.display = 'block'; return; } reportTotalAvailableSpan.textContent = formatHoursToHM(currentBudget.totalAvailableHours); reportTotalBudgetedSpan.textContent = formatHoursToHM(totalBudgetedH); reportTotalActualSpan.textContent = formatHoursToHM(totalActualH); const overallVariance = totalActualH - totalBudgetedH; reportOverallVarianceSpan.textContent = formatHoursToHM(overallVariance); reportOverallVarianceSpan.style.color = overallVariance === 0 ? 'var(--dark-text)' : (overallVariance > 0 ? 'var(--danger-color)' : 'var(--accent-color)'); // Positive variance (more actual) is red, negative (less actual) is green renderComparisonChart(chartData); renderDetailedBudgetTable(tableRowsData); reportOutputSection.style.display = 'block'; noReportDataMsg.style.display = 'none'; } function renderComparisonChart(data) { comparisonChartDiv.innerHTML = ''; if (data.length === 0) { comparisonChartDiv.innerHTML = '

    No data to display in chart.

    '; return; } const maxTime = Math.max(...data.map(d => Math.max(d.budgeted, d.actual)), 0.1); data.sort((a,b) => a.name.localeCompare(b.name)).forEach(item => { const rowDiv = document.createElement('div'); rowDiv.className = 'chart-row'; const labelDiv = document.createElement('div'); labelDiv.className = 'chart-row-label'; labelDiv.textContent = item.name; labelDiv.title = item.name; rowDiv.appendChild(labelDiv); const barsContainer = document.createElement('div'); barsContainer.className = 'chart-bars-container'; const budgetedBar = document.createElement('div'); budgetedBar.className = 'chart-bar-budgeted'; budgetedBar.style.width = `${(item.budgeted / maxTime) * 100}%`; budgetedBar.textContent = item.budgeted > 0 ? formatHoursToHM(item.budgeted) : ''; budgetedBar.title = `Budgeted: ${formatHoursToHM(item.budgeted)}`; barsContainer.appendChild(budgetedBar); const actualBar = document.createElement('div'); actualBar.className = 'chart-bar-actual'; actualBar.style.width = `${(item.actual / maxTime) * 100}%`; actualBar.textContent = item.actual > 0 ? formatHoursToHM(item.actual) : ''; actualBar.title = `Actual: ${formatHoursToHM(item.actual)}`; barsContainer.appendChild(actualBar); rowDiv.appendChild(barsContainer); comparisonChartDiv.appendChild(rowDiv); }); } function renderDetailedBudgetTable(data) { detailedBudgetTbody.innerHTML = ''; if (data.length === 0) return; data.sort((a,b) => a.name.localeCompare(b.name)).forEach(item => { const row = detailedBudgetTbody.insertRow(); row.insertCell().textContent = item.name; row.insertCell().textContent = formatHoursToHM(item.budgeted); row.insertCell().textContent = formatHoursToHM(item.actual); const varianceCell = row.insertCell(); varianceCell.textContent = formatHoursToHM(item.variance); varianceCell.className = item.variance === 0 ? '' : (item.variance > 0 ? 'variance-negative' : 'variance-positive'); // Negative variance means actual < budget (good), Positive means actual > budget (bad for 'time spent') }); } // --- PDF Download --- downloadBudgetReportPdfBtn.addEventListener('click', async () => { const currentBudget = timeBudgets[currentBudgetDateStr]; if (!currentBudget || reportOutputSection.style.display === 'none') { alert('Please generate a report by tracking actuals 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('Time Budget Report', pageWidth / 2, yPos, { align: 'center' }); yPos += 20; doc.setFontSize(12); doc.setTextColor(100); doc.text(`For Date: ${formatDateForDisplay(currentBudget.date)}`, pageWidth / 2, yPos, { align: 'center' }); yPos += 15; doc.setFontSize(10); doc.text(`Generated: ${new Date().toLocaleString()}`, pageWidth / 2, yPos, { align: 'center' }); yPos += 25; const summaryPdfData = [ ['Total Available for Budgeting:', reportTotalAvailableSpan.textContent], ['Total Budgeted Time:', reportTotalBudgetedSpan.textContent], ['Total Actual Time Spent:', reportTotalActualSpan.textContent], ['Overall Variance (Actual - Budgeted):', reportOverallVarianceSpan.textContent] ]; doc.autoTable({ startY: yPos, body: summaryPdfData, theme: 'plain', styles: {fontSize:10}, columnStyles: {0:{fontStyle:'bold'}}}); yPos = doc.lastAutoTable.finalY + 15; if (html2canvas && comparisonChartDiv && comparisonChartDiv.offsetHeight > 0) { if (yPos > doc.internal.pageSize.getHeight() - 150) { doc.addPage(); yPos = margin; } doc.setFontSize(12); doc.setTextColor(varToRGB('--primary-color').r, varToRGB('--primary-color').g, varToRGB('--primary-color').b); doc.text("Budgeted vs. Actual Time per Category", margin, yPos); yPos += 15; try { const canvas = await html2canvas(comparisonChartDiv, { scale: 1.5, backgroundColor: getComputedStyle(document.documentElement).getPropertyValue('--white-bg').trim() }); const imgData = canvas.toDataURL('image/png'); const imgProps = doc.getImageProperties(imgData); const pdfImgWidth = pageWidth - 2 * margin; const pdfImgHeight = (imgProps.height * pdfImgWidth) / imgProps.width; doc.addImage(imgData, 'PNG', margin, yPos, pdfImgWidth, pdfImgHeight); yPos += pdfImgHeight + 10; } catch (e) { console.error("Chart to PDF error:", e); doc.setTextColor(255,0,0).text("Chart could not be rendered.", margin, yPos); yPos+=15; } } 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 Breakdown:', margin, yPos); yPos += 15; const detailedPdfBody = []; detailedBudgetTbody.querySelectorAll('tr').forEach(tr => { const cells = Array.from(tr.querySelectorAll('td')).map(td => td.textContent); detailedPdfBody.push(cells); }); if (detailedPdfBody.length > 0) { doc.autoTable({ startY: yPos, head: [['Category', 'Budgeted', 'Actual', 'Variance']], body: detailedPdfBody, theme: 'grid', headStyles: { fillColor: varToRGB('--primary-color', true), textColor: varToRGB('--light-text', true) }, styles:{fontSize:9} }); } else { doc.text("No detailed data to show.", margin, yPos); } doc.save(`Time_Budget_Report_${currentBudget.date}.pdf`); }); function varToRGB(varName, asArray = false) { const colorHex = getComputedStyle(document.documentElement).getPropertyValue(varName).trim(); if (!colorHex.startsWith('#')) { 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 --- renderDefinedCategoriesList(); handleDateOrAvailableHoursChange(); // Load or create budget for the default date & available hours })();
    Scroll to Top