Work-Time Forecasting Tool

Work-Time Forecasting Tool

Add New Task

Task List for Forecasting

    Forecast Settings

    `; forecastTasksListUL.appendChild(li); }); forecastTasksListUL.querySelectorAll('.edit-forecast-task-btn').forEach(btn => btn.addEventListener('click', (e) => populateForecastTaskFormForEdit(e.target.dataset.id))); forecastTasksListUL.querySelectorAll('.delete-forecast-task-btn').forEach(btn => btn.addEventListener('click', (e) => deleteForecastTask(e.target.dataset.id))); } // --- Forecast Calculation & Display --- calculateForecastBtn.addEventListener('click', () => { if (forecastTasks.length === 0) { alert('No tasks to forecast. Please add tasks and their estimates first.'); forecastResultsDiv.style.display = 'none'; return; } const startDateStr = projectStartDateInput.value; if (!startDateStr) { alert('Please select a Project Start Date.'); return; } const hoursPerDay = parseFloat(hoursPerWorkDayInput.value) || 8; if (hoursPerDay <=0) { alert("Working hours per day must be positive."); return; } const considerWeekdays = considerWeekdaysOnlyToggle.checked; let totalAdjustedEffortHours = 0; forecastTasks.forEach(task => { totalAdjustedEffortHours += calculateAdjustedEffort(task); }); const totalWorkDays = totalAdjustedEffortHours / hoursPerDay; let completionDate = new Date(startDateStr + "T00:00:00"); // Ensure it's parsed as local let daysToAdd = totalWorkDays; if (considerWeekdays) { let workDaysAdded = 0; // Adjust daysToAdd to be actual calendar days considering only weekdays let calendarDaysElapsed = 0; let tempDate = new Date(completionDate); // Use a temporary date for iteration while(workDaysAdded < daysToAdd) { tempDate.setDate(tempDate.getDate() + 1); // Move to the next calendar day calendarDaysElapsed++; const dayOfWeek = tempDate.getDay(); // 0 (Sun) to 6 (Sat) if (dayOfWeek !== 0 && dayOfWeek !== 6) { // It's a weekday workDaysAdded++; } if (calendarDaysElapsed > daysToAdd * 3 && daysToAdd > 5) { break;} // Safety break for very long periods } completionDate.setDate(new Date(startDateStr + "T00:00:00").getDate() + calendarDaysElapsed -1); // -1 because loop adds one extra if daysToAdd is integer // If daysToAdd is fractional, the last day will be partial. // The date calculation should give the END of the last full work day, or part way into next if fractional if (daysToAdd > 0) { // Recalculate completionDate based on calendar days to add let currentCalcDate = new Date(startDateStr + "T00:00:00"); let remainingWorkDays = daysToAdd; while (remainingWorkDays > 0) { const dayOfWeek = currentCalcDate.getDay(); if (considerWeekdays && (dayOfWeek === 0 || dayOfWeek === 6)) { // Skip weekend } else { remainingWorkDays -= 1; // Consume one workday } if (remainingWorkDays <= 0) break; // Stop if all workdays are accounted for currentCalcDate.setDate(currentCalcDate.getDate() + 1); // Move to next calendar day } completionDate = currentCalcDate; } } else { // Consider all calendar days completionDate.setDate(completionDate.getDate() + Math.ceil(daysToAdd) -1 ); // If 1 day, it's startDate. If 1.5 days, it's next day. if (daysToAdd > 0) { completionDate = new Date(startDateStr + "T00:00:00"); completionDate.setDate(completionDate.getDate() + Math.ceil(daysToAdd) -1 ); } else { completionDate = new Date(startDateStr + "T00:00:00"); // if 0 days, it's start date } } resultStartDateSpan.textContent = formatDate(startDateStr); resultTotalEffortHoursSpan.textContent = `${totalAdjustedEffortHours.toFixed(2)} hours`; resultTotalWorkDaysSpan.textContent = `${totalWorkDays.toFixed(2)} days`; resultHoursPerDayInfoSpan.textContent = `${hoursPerDay}h`; resultWeekdaysInfoSpan.textContent = considerWeekdays ? 'weekdays only' : 'all days'; resultCompletionDateSpan.textContent = formatDate(new Date(completionDate.getTime() - completionDate.getTimezoneOffset() * 60000).toISOString().slice(0,10)); forecastResultsDiv.style.display = 'block'; }); // --- PDF Download --- downloadForecastPdfBtn.addEventListener('click', () => { if (forecastResultsDiv.style.display === 'none' || forecastTasks.length === 0) { alert('Please calculate a forecast 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-Time Forecast Report', pageWidth / 2, yPos, { align: 'center' }); yPos += 20; doc.setFontSize(10); doc.setTextColor(100); doc.text(`Report Generated: ${new Date().toLocaleString()}`, pageWidth / 2, yPos, { align: 'center' }); yPos += 25; const summaryData = [ ['Project Start Date:', resultStartDateSpan.textContent], ['Total Adjusted Effort (Hours):', resultTotalEffortHoursSpan.textContent], ['Total Work-Days:', `${resultTotalWorkDaysSpan.textContent} (${resultHoursPerDayInfoSpan.textContent}/day, ${resultWeekdaysInfoSpan.textContent})`], ['Projected Completion Date:', resultCompletionDateSpan.textContent], ]; doc.autoTable({ startY: yPos, body: summaryData, theme: 'plain', styles: {fontSize:10}, columnStyles: {0:{fontStyle:'bold', cellWidth:180}}}); yPos = doc.lastAutoTable.finalY + 20; doc.setFontSize(12); doc.setTextColor(varToRGB('--primary-color').r, varToRGB('--primary-color').g, varToRGB('--primary-color').b); doc.text('Detailed Task Estimates:', margin, yPos); yPos += 18; const taskTableBody = forecastTasks.map(task => [ task.description, task.baseEffortHours.toFixed(1), `${task.complexityFactor}x`, `${task.contingencyPercent}%`, calculateAdjustedEffort(task).toFixed(2) ]); if (taskTableBody.length > 0) { doc.autoTable({ startY: yPos, head: [['Task Description', 'Base Effort (H)', 'Complexity', 'Contingency', 'Adjusted Effort (H)']], body: taskTableBody, theme: 'grid', headStyles: { fillColor: varToRGB('--primary-color', true), textColor: varToRGB('--light-text', true) }, styles: { fontSize: 9, cellPadding: 4 }, columnStyles: { 0: {cellWidth: 'auto'} } }); } else { doc.text('No tasks were included in this forecast.', margin, yPos); } doc.save(`Work_Time_Forecast_${forecastSettings.projectStartDate || todayStr}.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 --- initSettingsUI(); renderForecastTasksList(); resetForecastTaskForm(); // Also sets min date for new tasks // Set project start date input default (done by initSettingsUI if settings exist, or set here) if(!projectStartDateInput.value) projectStartDateInput.value = todayStr; })();
    Scroll to Top