Real-Time Work Hour Logging Tool

Real-Time Work Hour Logger

Live Timer

Quick Start Recent Tasks:

Log time for projects to see them here for quick starting.


Daily Log for Date

Manage Projects/Common Tasks

Generate Time Report

Real-Time Work Hour Logging Guide

1. Define Your Projects (Projects Tab)
Set up a list of common projects, clients, or general task types you work on. Assigning a color can help in visual identification.
2. Logging Time (Timer & Daily Log Tab)
Using the Live Timer:
  • Select a project from the dropdown or choose "Log Ad-hoc Task" and enter a specific task name.
  • Click "Start New Timer". The timer will run, showing elapsed time for the active task. The browser tab title will also show the timer.
  • Use "Pause" and "Resume" as needed. The timer accounts for paused duration.
  • When finished, click "Stop & Log". A modal will appear allowing you to confirm/edit the auto-filled start/end times, task description (if ad-hoc or needs refinement), select a project if not already, and add notes.
  • "Discard Timer" will stop the timer without creating a log entry.
Manual Entry:
  • Select the date for which you want to log time using the "View/Manage Log for Date" picker.
  • Click "Add Manual Entry".
  • Fill in the task description, select a project (or leave for ad-hoc), set Start Date/Time, and End Date/Time. The duration will be auto-calculated. Add optional notes.
3. Recovering Interrupted Timers
If you accidentally close your browser or refresh the page while a timer is running, the tool will try to detect this when you next open it. You'll be prompted to log the potentially elapsed time, adjust it, or discard it.
4. Viewing Reports (Reports Tab)
Select a period (Today, This Week, This Month, or a Custom Date Range) and click "Generate Report". You'll see:
  • Total time logged for the period.
  • A summary of time spent per project/task.
  • A detailed chronological list of all individual time entries within that period.
5. Exporting Your Data
Use the "Download Report as PDF" button to save your time analysis for records, billing, or sharing.
Tips for Effective Time Logging:
  • Log in Real-Time: Start the timer when you begin a task for maximum accuracy.
  • Be Consistent: The more consistently you log, the more valuable your data.
  • Use Projects/Categories: This helps in analyzing where your time is spent.
  • Add Notes: Brief notes can provide context to your time entries later.
  • Review Reports: Periodically check your reports to understand your work patterns and identify areas for improvement or better estimation.
×

Log Time Entry

No projects defined. Add one above.

'; return; } rtwlProjects.forEach(proj => { const itemEl = document.createElement('div'); itemEl.className = 'rtwl-entry-item'; itemEl.innerHTML = `
${proj.name}
`; rtwlProjectsListEl.appendChild(itemEl); }); } window.rtwlDeleteProject = function(projId) { if (confirm("Are you sure you want to delete this project? Existing time entries will retain its name but it won't be available for new selections.")) { rtwlProjects = rtwlProjects.filter(p => p.id !== projId); rtwlSaveProjects(); rtwlRenderProjectsList(); rtwlPopulateProjectSelects(); } } function rtwlPopulateProjectSelects() { const selects = [rtwlTaskSelectTimerEl, rtwlEntryProjectIdSelectEl]; selects.forEach(selectEl => { if(!selectEl) return; const currentVal = selectEl.value; let baseOptions = ''; if (selectEl === rtwlTaskSelectTimerEl) { baseOptions = ''; } else if (selectEl === rtwlEntryProjectIdSelectEl) { baseOptions = ''; } selectEl.innerHTML = baseOptions; rtwlProjects.forEach(proj => { const option = document.createElement('option'); option.value = proj.id; option.textContent = proj.name; selectEl.appendChild(option); }); if(selectEl === rtwlTaskSelectTimerEl) { selectEl.innerHTML += ''; } if (Array.from(selectEl.options).some(opt => opt.value === currentVal)) { selectEl.value = currentVal; } }); } // --- Live Timer Functions --- function rtwlStartNewTimer() { if (rtwlActiveTimer) { alert("A timer is already running. Stop or discard it first."); return; } const projectId = rtwlTaskSelectTimerEl.value; let taskDescription; let currentProjectNameForDisplay = ""; if (projectId === 'adhoc') { taskDescription = rtwlAdhocTaskNameTimerEl.value.trim(); if (!taskDescription) { alert("Please enter a name for the ad-hoc task."); return; } currentProjectNameForDisplay = taskDescription; } else if (projectId) { const project = rtwlProjects.find(p => p.id === projectId); if (!project) { alert("Selected project not found."); return; } taskDescription = project.name; currentProjectNameForDisplay = project.name; } else { alert("Please select a project or choose 'Log Ad-hoc Task'."); return; } rtwlActiveTimer = { projectId: projectId === 'adhoc' ? null : projectId, taskDescription: taskDescription, currentProjectNameForDisplay: currentProjectNameForDisplay, startTimeEpoch: Date.now(), isPaused: false, pausedDuration: 0, lastPauseTime: 0, intervalId: null }; rtwlActiveTimerAreaEl.style.display = 'block'; rtwlActiveTaskNameDisplayEl.textContent = `Working on: ${rtwlActiveTimer.currentProjectNameForDisplay}`; rtwlPauseResumeTimerButtonEl.textContent = "Pause"; rtwlUpdateRunningTimerDisplay(); rtwlActiveTimer.intervalId = setInterval(rtwlUpdateRunningTimerDisplay, 1000); rtwlSaveActiveTimer(); } function rtwlUpdateRunningTimerDisplay() { if (!rtwlActiveTimer || rtwlActiveTimer.isPaused || !rtwlActiveTimerDisplayEl) return; const now = Date.now(); const elapsedMs = now - rtwlActiveTimer.startTimeEpoch - rtwlActiveTimer.pausedDuration; const totalSeconds = Math.floor(elapsedMs / 1000); const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); const seconds = totalSeconds % 60; rtwlActiveTimerDisplayEl.textContent = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; document.title = `${rtwlActiveTimerDisplayEl.textContent} - ${rtwlActiveTimer.currentProjectNameForDisplay}`; } function rtwlTogglePauseResumeTimer() { if (!rtwlActiveTimer) return; rtwlActiveTimer.isPaused = !rtwlActiveTimer.isPaused; if (rtwlActiveTimer.isPaused) { if(rtwlActiveTimer.intervalId) clearInterval(rtwlActiveTimer.intervalId); rtwlActiveTimer.lastPauseTime = Date.now(); rtwlPauseResumeTimerButtonEl.textContent = "Resume"; document.title = `Paused - ${rtwlActiveTimer.currentProjectNameForDisplay}`; } else { rtwlActiveTimer.pausedDuration += (Date.now() - rtwlActiveTimer.lastPauseTime); rtwlActiveTimer.lastPauseTime = 0; rtwlUpdateRunningTimerDisplay(); rtwlActiveTimer.intervalId = setInterval(rtwlUpdateRunningTimerDisplay, 1000); rtwlPauseResumeTimerButtonEl.textContent = "Pause"; } rtwlSaveActiveTimer(); } function rtwlStopAndLogTimer() { if (!rtwlActiveTimer) return; if(rtwlActiveTimer.intervalId) clearInterval(rtwlActiveTimer.intervalId); const endTimeEpoch = Date.now(); let finalPausedDuration = rtwlActiveTimer.pausedDuration; if(rtwlActiveTimer.isPaused && rtwlActiveTimer.lastPauseTime > 0) { // If stopped while paused finalPausedDuration += (endTimeEpoch - rtwlActiveTimer.lastPauseTime); } const elapsedMs = endTimeEpoch - rtwlActiveTimer.startTimeEpoch - finalPausedDuration; const durationMinutes = Math.max(1, Math.round(elapsedMs / (1000 * 60))); rtwlTimeEntryModalTitleEl.textContent = "Confirm & Log Timed Entry"; rtwlEntryIdInput.value = ''; const startDateObj = new Date(rtwlActiveTimer.startTimeEpoch); const {date: startDate, time: startTime} = rtwlDateTimeToInputFields(startDateObj.toISOString()); const endDateObj = new Date(endTimeEpoch); const {date: endDate, time: endTime} = rtwlDateTimeToInputFields(endDateObj.toISOString()); rtwlEntryTaskDescriptionInput.value = rtwlActiveTimer.taskDescription; rtwlEntryProjectIdSelectEl.value = rtwlActiveTimer.projectId || ""; rtwlEntryStartDateInput.value = startDate; rtwlEntryStartTimeInput.value = startTime; rtwlEntryEndDateInput.value = endDate; rtwlEntryEndTimeInput.value = endTime; rtwlEntryDurationMinutesInput.value = durationMinutes; rtwlEntryNotesInput.value = ""; rtwlEntryStartDateInput.readOnly = true; rtwlEntryStartTimeInput.readOnly = true; rtwlEntryEndDateInput.readOnly = true; rtwlEntryEndTimeInput.readOnly = true; rtwlEntryDurationMinutesInput.readOnly = true; rtwlTimeEntryModalEl.style.display = 'block'; // Active timer state is fully cleared when modal's Save is successful } function rtwlDiscardActiveTimer() { if (rtwlActiveTimer && confirm("Are you sure you want to discard this running timer?")) { if(rtwlActiveTimer.intervalId) clearInterval(rtwlActiveTimer.intervalId); rtwlActiveTimer = null; rtwlActiveTimerAreaEl.style.display = 'none'; rtwlSaveActiveTimer(); document.title = "Real-Time Work Hour Logger"; } } function rtwlResumeInterruptedTimer() { if (!rtwlActiveTimer || !rtwlActiveTimer.startTimeEpoch) { rtwlActiveTimer = null; rtwlSaveActiveTimer(); return; } const now = Date.now(); let effectiveStartTime = rtwlActiveTimer.startTimeEpoch; let totalPausedTime = rtwlActiveTimer.pausedDuration || 0; if (rtwlActiveTimer.isPaused && rtwlActiveTimer.lastPauseTime > 0) { totalPausedTime += (now - rtwlActiveTimer.lastPauseTime); } const actualRunningTimeMs = (now - effectiveStartTime) - totalPausedTime; const actualRunningTimeMinutes = Math.round(actualRunningTimeMs / (1000 * 60)); let userConfirmation = false; if (actualRunningTimeMinutes < 1) { userConfirmation = confirm(`An active timer for "${rtwlActiveTimer.currentProjectNameForDisplay || rtwlActiveTimer.taskDescription}" was found but ran for less than a minute. Still log or adjust it?`); } else { userConfirmation = confirm(`An active timer for "${rtwlActiveTimer.currentProjectNameForDisplay || rtwlActiveTimer.taskDescription}" was found. It appears to have been active for about ${rtwlFormatDuration(actualRunningTimeMinutes)}. Do you want to log or adjust this?`); } if (!userConfirmation) { rtwlActiveTimer = null; rtwlSaveActiveTimer(); if(rtwlActiveTimerAreaEl) rtwlActiveTimerAreaEl.style.display = 'none'; document.title = "Real-Time Work Hour Logger"; return; } const endTimeEpoch = now; const durationMinutes = Math.max(1, actualRunningTimeMinutes); rtwlTimeEntryModalTitleEl.textContent = "Log Recovered Timer Session"; rtwlEntryIdInput.value = ''; const startDateObj = new Date(rtwlActiveTimer.startTimeEpoch); const {date: startDate, time: startTime} = rtwlDateTimeToInputFields(startDateObj.toISOString()); const endDateObj = new Date(endTimeEpoch); const {date: endDate, time: endTime} = rtwlDateTimeToInputFields(endDateObj.toISOString()); rtwlEntryTaskDescriptionInput.value = rtwlActiveTimer.taskDescription; rtwlEntryProjectIdSelectEl.value = rtwlActiveTimer.projectId || ""; rtwlEntryStartDateInput.value = startDate; rtwlEntryStartTimeInput.value = startTime; rtwlEntryEndDateInput.value = endDate; rtwlEntryEndTimeInput.value = endTime; rtwlEntryDurationMinutesInput.value = durationMinutes; rtwlEntryNotesInput.value = "Recovered session (browser closed/refreshed). Please verify times."; rtwlEntryStartDateInput.readOnly = false; rtwlEntryStartTimeInput.readOnly = false; rtwlEntryEndDateInput.readOnly = false; rtwlEntryEndTimeInput.readOnly = false; rtwlEntryDurationMinutesInput.readOnly = true; // Calculated from start/end rtwlTimeEntryModalEl.style.display = 'block'; // Active timer will be fully cleared when modal is saved (handleTimeEntryFormSubmit) if(rtwlActiveTimer.intervalId) clearInterval(rtwlActiveTimer.intervalId); if(rtwlActiveTimerAreaEl) rtwlActiveTimerAreaEl.style.display = 'none'; document.title = "Real-Time Work Hour Logger"; } // --- Manual Entry & Daily Log --- window.rtwlOpenManualEntryModal = function(entryId = null) { if (!rtwlTimeEntryModalEl) return; rtwlTimeEntryForm.reset(); rtwlPopulateProjectSelects(); rtwlEntryIdInput.value = ''; rtwlEntryStartDateInput.readOnly = false; rtwlEntryStartTimeInput.readOnly = false; rtwlEntryEndDateInput.readOnly = false; rtwlEntryEndTimeInput.readOnly = false; rtwlEntryDurationMinutesInput.readOnly = true; rtwlEntryDurationMinutesInput.style.backgroundColor = "#f0f0f0"; if (entryId) { rtwlTimeEntryModalTitleEl.textContent = "Edit Time Entry"; const entry = rtwlTimeEntries.find(e => e.id === entryId); if (entry) { rtwlEntryIdInput.value = entry.id; rtwlEntryTaskDescriptionInput.value = entry.taskDescription; rtwlEntryProjectIdSelectEl.value = entry.projectId || ""; const startDT = rtwlDateTimeToInputFields(entry.startTimeISO); rtwlEntryStartDateInput.value = startDT.date; rtwlEntryStartTimeInput.value = startDT.time; const endDT = rtwlDateTimeToInputFields(entry.endTimeISO); rtwlEntryEndDateInput.value = endDT.date; rtwlEntryEndTimeInput.value = endDT.time; rtwlEntryDurationMinutesInput.value = entry.durationMinutes; rtwlEntryNotesInput.value = entry.notes || ""; } } else { rtwlTimeEntryModalTitleEl.textContent = "Add Manual Time Entry"; const defaultStartDate = rtwlLogViewDateEl.value || rtwlFormatDateKey(new Date(2025, 4, 14)); rtwlEntryStartDateInput.value = defaultStartDate; rtwlEntryStartTimeInput.value = "09:00"; rtwlEntryEndDateInput.value = defaultStartDate; rtwlEntryEndTimeInput.value = "10:00"; rtwlCalculateDurationManual(); } rtwlTimeEntryModalEl.style.display = 'block'; }; function rtwlCalculateDurationManual() { const startISO = rtwlInputFieldsToISO(rtwlEntryStartDateInput.value, rtwlEntryStartTimeInput.value); const endISO = rtwlInputFieldsToISO(rtwlEntryEndDateInput.value, rtwlEntryEndTimeInput.value); if (startISO && endISO) { const diffMs = new Date(endISO).getTime() - new Date(startISO).getTime(); if (diffMs > 0) { rtwlEntryDurationMinutesInput.value = Math.round(diffMs / (1000 * 60)); } else { rtwlEntryDurationMinutesInput.value = 0; } } else { rtwlEntryDurationMinutesInput.value = 0; } } function handleTimeEntryFormSubmit(e) { e.preventDefault(); // If this save is for a recovered timer, clear the active timer state if (rtwlActiveTimer && rtwlTimeEntryModalTitleEl.textContent.includes("Recovered")) { rtwlActiveTimer = null; rtwlSaveActiveTimer(); } // If this save is for a timer just stopped (not recovered), clear active timer state if (rtwlActiveTimer && rtwlTimeEntryModalTitleEl.textContent.includes("Confirm & Log Timed Entry")) { rtwlActiveTimer = null; rtwlSaveActiveTimer(); } const entryId = rtwlEntryIdInput.value; const taskDescription = rtwlEntryTaskDescriptionInput.value.trim(); const projectId = rtwlEntryProjectIdSelectEl.value; const startDate = rtwlEntryStartDateInput.value; const startTime = rtwlEntryStartTimeInput.value; const endDate = rtwlEntryEndDateInput.value; const endTime = rtwlEntryEndTimeInput.value; let durationMinutes; // Recalculate based on final times const notes = rtwlEntryNotesInput.value.trim(); if (!taskDescription || !startDate || !startTime || !endDate || !endTime) { alert("Task description, and all start/end date & time fields are required for manual entry."); return; } const startTimeISO = rtwlInputFieldsToISO(startDate, startTime); const endTimeISO = rtwlInputFieldsToISO(endDate, endTime); if (!startTimeISO || !endTimeISO) { alert("Invalid date/time format."); return; } const diffMs = new Date(endTimeISO).getTime() - new Date(startTimeISO).getTime(); if (diffMs <=0) { alert("End time must be after start time."); return; } durationMinutes = Math.max(1, Math.round(diffMs / (1000 * 60))); const newEntryData = { projectId: projectId || null, taskDescription, startTimeISO, endTimeISO, durationMinutes, notes }; if (entryId) { const index = rtwlTimeEntries.findIndex(entry => entry.id === entryId); if (index > -1) { rtwlTimeEntries[index] = { ...rtwlTimeEntries[index], ...newEntryData }; } } else { rtwlTimeEntries.push({ id: 'entry-' + Date.now(), ...newEntryData }); } rtwlSaveTimeEntries(); rtwlRenderDailyLogEntries(); rtwlCloseModal('rtwlTimeEntryModal'); rtwlUpdateQuickStartButtons(); } window.rtwlDeleteLogEntry = function(entryId) { if (confirm("Are you sure you want to delete this time entry?")) { rtwlTimeEntries = rtwlTimeEntries.filter(entry => entry.id !== entryId); rtwlSaveTimeEntries(); rtwlRenderDailyLogEntries(); rtwlUpdateQuickStartButtons(); } }; function rtwlRenderDailyLogEntries() { if (!rtwlDailyLogEntriesListEl || !rtwlLogViewDateEl || !rtwlCurrentDateDisplayEl) return; const selectedDateKey = rtwlLogViewDateEl.value; if (!selectedDateKey) { rtwlDailyLogEntriesListEl.innerHTML = '

Select a date to view log entries.

'; if(rtwlCurrentDateDisplayEl) rtwlCurrentDateDisplayEl.textContent = 'N/A'; return; } if(rtwlCurrentDateDisplayEl) rtwlCurrentDateDisplayEl.textContent = new Date(selectedDateKey + "T00:00:00").toLocaleDateString(); const entriesForDay = rtwlTimeEntries.filter(entry => rtwlFormatDateKey(new Date(entry.startTimeISO)) === selectedDateKey ).sort((a,b) => new Date(a.startTimeISO).getTime() - new Date(b.startTimeISO).getTime()); rtwlDailyLogEntriesListEl.innerHTML = ''; if (entriesForDay.length === 0) { rtwlDailyLogEntriesListEl.innerHTML = '

No time logged for this day.

'; return; } entriesForDay.forEach(entry => { const itemEl = document.createElement('div'); itemEl.className = 'rtwl-entry-item'; const project = entry.projectId ? rtwlProjects.find(p => p.id === entry.projectId) : null; const projectTagHTML = project ? `${project.name}` : ''; itemEl.innerHTML = `

${projectTagHTML}${entry.taskDescription}

${rtwlFormatTimeForDisplay(entry.startTimeISO)} - ${rtwlFormatTimeForDisplay(entry.endTimeISO)} (Duration: ${rtwlFormatDuration(entry.durationMinutes)})

${entry.notes ? `

${entry.notes.replace(/\n/g, '
')}

` : ''}
`; rtwlDailyLogEntriesListEl.appendChild(itemEl); }); } function rtwlUpdateQuickStartButtons() { if(!rtwlQuickStartButtonsEl) return; rtwlQuickStartButtonsEl.innerHTML = ''; const recentTaskMap = new Map(); let count = 0; for (let i = 0; i < rtwlTimeEntries.length && count < 4; i++) { const entry = rtwlTimeEntries[i]; const key = entry.projectId ? entry.projectId : `adhoc-${entry.taskDescription}`; // Use task desc for adhoc key if (!recentTaskMap.has(key)) { let displayName = entry.taskDescription; if(entry.projectId){ const proj = rtwlProjects.find(p=>p.id===entry.projectId); if(proj) displayName = proj.name; // Prefer project name if available } recentTaskMap.set(key, { projectId: entry.projectId, taskDescription: entry.taskDescription, // Store original adhoc desc if needed display: displayName }); count++; } } if (recentTaskMap.size === 0) { rtwlQuickStartButtonsEl.innerHTML = '

Log time for projects to see them here for quick starting.

'; return; } recentTaskMap.forEach((taskInfo) => { const btn = document.createElement('button'); btn.className = 'rtwl-button rtwl-button-secondary'; btn.textContent = `Start: ${taskInfo.display.substring(0,25)}${taskInfo.display.length > 25 ? '...' : '' }`; btn.title = `Start timer for ${taskInfo.display}`; btn.onclick = () => { if (taskInfo.projectId) { rtwlTaskSelectTimerEl.value = taskInfo.projectId; if(rtwlAdhocTaskNameGroupEl) rtwlAdhocTaskNameGroupEl.style.display = 'none'; } else { rtwlTaskSelectTimerEl.value = 'adhoc'; if(rtwlAdhocTaskNameTimerEl) rtwlAdhocTaskNameTimerEl.value = taskInfo.taskDescription; // Use original taskDescription for adhoc if(rtwlAdhocTaskNameGroupEl) rtwlAdhocTaskNameGroupEl.style.display = 'block'; } rtwlStartNewTimer(); }; rtwlQuickStartButtonsEl.appendChild(btn); }); } // --- Reports --- window.rtwlGenerateReport = function() { /* ... same as stl, with rtwl prefixes ... */ if (!rtwlReportOutputAreaEl || !rtwlReportPeriodSelectEl || !rtwlReportPeriodDisplayEl || !rtwlReportSummaryEl || !rtwlReportDetailsTableBodyEl) return; const period = rtwlReportPeriodSelectEl.value; let startDateObj, endDateObj; // Use actual Date objects for filtering const todayForReport = new Date(2025, 4, 14); todayForReport.setHours(0,0,0,0); let periodDisplayString = ""; switch(period) { case 'today': startDateObj = new Date(todayForReport); endDateObj = new Date(todayForReport); endDateObj.setHours(23,59,59,999); periodDisplayString = "Today (" + todayForReport.toLocaleDateString() + ")"; break; case 'this_week': startDateObj = new Date(todayForReport); const dayOfWeek = startDateObj.getDay(); const diffToMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek; startDateObj.setDate(startDateObj.getDate() + diffToMonday); endDateObj = new Date(startDateObj); endDateObj.setDate(startDateObj.getDate() + 6); endDateObj.setHours(23,59,59,999); periodDisplayString = `This Week (${startDateObj.toLocaleDateString()} - ${endDateObj.toLocaleDateString()})`; break; case 'this_month': startDateObj = new Date(todayForReport.getFullYear(), todayForReport.getMonth(), 1); endDateObj = new Date(todayForReport.getFullYear(), todayForReport.getMonth() + 1, 0); endDateObj.setHours(23,59,59,999); periodDisplayString = todayForReport.toLocaleDateString(undefined, {month: 'long', year: 'numeric'}); break; case 'custom_range': if (!rtwlReportStartDateInputEl.value || !rtwlReportEndDateInputEl.value) { alert("Please select a start and end date for custom range."); return; } startDateObj = new Date(rtwlReportStartDateInputEl.value + "T00:00:00"); endDateObj = new Date(rtwlReportEndDateInputEl.value + "T23:59:59"); if (startDateObj > endDateObj) { alert("End date must be after start date."); return; } periodDisplayString = `Custom (${startDateObj.toLocaleDateString()} - ${endDateObj.toLocaleDateString()})`; break; } if(rtwlReportPeriodDisplayEl) rtwlReportPeriodDisplayEl.textContent = periodDisplayString; const entriesInPeriod = rtwlTimeEntries.filter(entry => { const entryStart = new Date(entry.startTimeISO); return entryStart >= startDateObj && entryStart <= endDateObj; }); let totalLoggedMinutes = 0; const projectTime = {}; if(rtwlReportDetailsTableBodyEl) rtwlReportDetailsTableBodyEl.innerHTML = ''; entriesInPeriod.sort((a,b) => new Date(a.startTimeISO) - new Date(b.startTimeISO)).forEach(entry => { totalLoggedMinutes += entry.durationMinutes; const project = entry.projectId ? rtwlProjects.find(p => p.id === entry.projectId) : null; // For summary, group by project name if available, otherwise use taskDescription as key const summaryKey = project ? project.name : (entry.taskDescription || "Ad-hoc"); projectTime[summaryKey] = (projectTime[summaryKey] || 0) + entry.durationMinutes; if(rtwlReportDetailsTableBodyEl){ const row = rtwlReportDetailsTableBodyEl.insertRow(); row.insertCell().textContent = new Date(entry.startTimeISO).toLocaleDateString(); row.insertCell().textContent = project ? project.name : "Ad-hoc"; row.insertCell().textContent = entry.taskDescription; row.insertCell().textContent = rtwlFormatTimeForDisplay(entry.startTimeISO); row.insertCell().textContent = rtwlFormatTimeForDisplay(entry.endTimeISO); row.insertCell().textContent = rtwlFormatDuration(entry.durationMinutes); row.insertCell().textContent = entry.notes || ""; } }); let summaryHTML = `

Total Time Logged: ${rtwlFormatDuration(totalLoggedMinutes)}

Time by Project/Task:

    `; Object.keys(projectTime).sort((a,b) => projectTime[b] - projectTime[a]).forEach(projName => { summaryHTML += `
  • ${projName}: ${rtwlFormatDuration(projectTime[projName])}
  • `; }); summaryHTML += `
`; if(rtwlReportSummaryEl) rtwlReportSummaryEl.innerHTML = summaryHTML; if(rtwlReportOutputAreaEl) rtwlReportOutputAreaEl.style.display = 'block'; if(rtwlDownloadPdfBtn) rtwlDownloadPdfBtn.style.display = entriesInPeriod.length > 0 ? 'block' : 'none'; } function handlePdfDownload() { /* ... same as stlHandlePdfDownload, with rtwl prefix ... */ const periodText = rtwlReportPeriodDisplayEl.textContent; const totalTimeText = rtwlReportSummaryEl.querySelector('p strong')?.parentElement.textContent || "Total Time Logged: N/A"; const projectSummaryData = []; const projectLis = rtwlReportSummaryEl.querySelectorAll('ul li'); projectLis.forEach(li => projectSummaryData.push(li.textContent)); const detailTableRowsData = []; const detailTableHtmlRows = rtwlReportDetailsTableBodyEl.querySelectorAll('tr'); detailTableHtmlRows.forEach(row => { const cells = Array.from(row.cells).map(cell => cell.textContent); detailTableRowsData.push(cells); }); if(detailTableRowsData.length === 0 && totalTimeText.includes("N/A") && projectSummaryData.length === 0) { alert("No data to export for the selected report period."); return; } const { jsPDF } = window.jspdf; const doc = new jsPDF(); doc.setFontSize(18); doc.setTextColor(getComputedStyle(document.documentElement).getPropertyValue('--rtwl-primary-color').trim()); doc.text("Real-Time Work Log Report", 14, 22); doc.setFontSize(12); doc.setTextColor(100); doc.text(`Period: ${periodText}`, 14, 30); doc.text(totalTimeText, 14, 38); let yPos = 46; if (projectSummaryData.length > 0) { doc.setFontSize(11); doc.setTextColor(getComputedStyle(document.documentElement).getPropertyValue('--rtwl-text-color').trim()); doc.text("Summary by Project/Task:", 14, yPos); yPos += 7; doc.setFontSize(9); projectSummaryData.forEach(item => { if (yPos > 270) { doc.addPage(); yPos = 20; } doc.text(`\u2022 ${item}`, 16, yPos); yPos += 5; }); yPos += 5; } if (detailTableRowsData.length > 0) { if (yPos > 250) { doc.addPage(); yPos = 20; } doc.setFontSize(11); doc.setTextColor(getComputedStyle(document.documentElement).getPropertyValue('--rtwl-text-color').trim()); doc.text("Detailed Log:", 14, yPos); yPos += 7; doc.autoTable({ head: [['Date', 'Project', 'Task Description', 'Start', 'End', 'Duration', 'Notes']], body: detailTableRowsData, startY: yPos, theme: 'striped', headStyles: { fillColor: getComputedStyle(document.documentElement).getPropertyValue('--rtwl-primary-color').trim() }, styles: { fontSize: 8, cellPadding: 1.5, overflow: 'linebreak' }, columnStyles: { 1: { cellWidth: 30 }, 2: { cellWidth: 50 }, 6: { cellWidth: 'auto'} } }); } doc.save(`TimeLogReport_${periodText.replace(/[^a-zA-Z0-9]/g, '_')}.pdf`); } window.rtwlCloseModal = (modalId) => { /* ... */ const modal = document.getElementById(modalId); if(modal) modal.style.display = 'none'; // Reset timer-related modal fields if active timer was cleared during modal interaction. if (!rtwlActiveTimer && modalId === 'rtwlTimeEntryModal' && rtwlTimeEntryModalTitleEl.textContent.includes("Confirm & Log Timed Entry")) { if(rtwlTaskSelectTimerEl) rtwlTaskSelectTimerEl.value = ""; if(rtwlAdhocTaskNameTimerEl) rtwlAdhocTaskNameTimerEl.value = ""; if(rtwlAdhocTaskNameGroupEl) rtwlAdhocTaskNameGroupEl.style.display = "none"; } } // --- DOMContentLoaded --- document.addEventListener('DOMContentLoaded', () => { // Assign DOM elements rtwlTaskSelectTimerEl = document.getElementById('rtwlTaskSelectTimer'); rtwlAdhocTaskNameGroupEl = document.getElementById('rtwlAdhocTaskNameGroup'); rtwlAdhocTaskNameTimerEl = document.getElementById('rtwlAdhocTaskNameTimer'); rtwlStartNewTimerButtonEl = document.getElementById('rtwlStartNewTimerButton'); rtwlActiveTimerAreaEl = document.getElementById('rtwlActiveTimerArea'); rtwlActiveTaskNameDisplayEl = document.getElementById('rtwlActiveTaskNameDisplay'); rtwlActiveTimerDisplayEl = document.getElementById('rtwlActiveTimerDisplay'); rtwlPauseResumeTimerButtonEl = document.getElementById('rtwlPauseResumeTimerButton'); rtwlStopTimerButtonEl = document.getElementById('rtwlStopTimerButton'); rtwlDiscardTimerButtonEl = document.getElementById('rtwlDiscardTimerButton'); rtwlCurrentDateDisplayEl = document.getElementById('rtwlCurrentDateDisplay'); rtwlLogViewDateEl = document.getElementById('rtwlLogViewDate'); rtwlDailyLogEntriesListEl = document.getElementById('rtwlDailyLogEntriesList'); rtwlQuickStartButtonsEl = document.getElementById('rtwlQuickStartButtons'); rtwlAddProjectForm = document.getElementById('rtwlAddProjectForm'); rtwlProjectNameInputEl = document.getElementById('rtwlProjectNameInput'); rtwlProjectColorInputEl = document.getElementById('rtwlProjectColorInput'); rtwlProjectsListEl = document.getElementById('rtwlProjectsList'); rtwlReportPeriodSelectEl = document.getElementById('rtwlReportPeriodSelect'); rtwlCustomDateRangePickerEl = document.getElementById('rtwlCustomDateRangePicker'); rtwlReportStartDateInputEl = document.getElementById('rtwlReportStartDate'); rtwlReportEndDateInputEl = document.getElementById('rtwlReportEndDate'); rtwlReportOutputAreaEl = document.getElementById('rtwlReportOutputArea'); rtwlReportPeriodDisplayEl = document.getElementById('rtwlReportPeriodDisplay'); rtwlReportSummaryEl = document.getElementById('rtwlReportSummary'); rtwlReportDetailsTableBodyEl = document.querySelector('#rtwlReportDetailsTable tbody'); // Corrected selector rtwlTimeEntryModalEl = document.getElementById('rtwlTimeEntryModal'); rtwlTimeEntryModalTitleEl = document.getElementById('rtwlTimeEntryModalTitle'); rtwlTimeEntryForm = document.getElementById('rtwlTimeEntryForm'); rtwlEntryIdInput = document.getElementById('rtwlEntryId'); rtwlEntryTaskDescriptionInput = document.getElementById('rtwlEntryTaskDescription'); rtwlEntryProjectIdSelectEl = document.getElementById('rtwlEntryProjectId'); rtwlEntryStartDateInput = document.getElementById('rtwlEntryStartDate'); rtwlEntryStartTimeInput = document.getElementById('rtwlEntryStartTime'); rtwlEntryEndDateInput = document.getElementById('rtwlEntryEndDate'); rtwlEntryEndTimeInput = document.getElementById('rtwlEntryEndTime'); rtwlEntryDurationMinutesInput = document.getElementById('rtwlEntryDurationMinutes'); rtwlEntryNotesInput = document.getElementById('rtwlEntryNotes'); rtwlDownloadPdfBtn = document.getElementById('rtwlDownloadPdfButton'); // Check if all critical elements are found const criticalElements = [rtwlTaskSelectTimerEl, rtwlAddProjectForm, rtwlTimeEntryForm, rtwlLogViewDateEl, rtwlReportPeriodSelectEl]; if (criticalElements.some(el => !el)) { console.error("RealTimeWorkHourLoggingTool: Not all critical DOM elements were found. Tool may not function correctly."); alert("Error initializing the Time Logger: Some UI elements are missing. Please ensure the HTML code is complete."); return; // Stop further initialization if critical parts are missing } // Attach event listeners if(rtwlAddProjectForm) rtwlAddProjectForm.addEventListener('submit', handleAddProjectFormSubmit); if(rtwlTimeEntryForm) rtwlTimeEntryForm.addEventListener('submit', handleTimeEntryFormSubmit); if(rtwlTaskSelectTimerEl) rtwlTaskSelectTimerEl.addEventListener('change', () => { if(rtwlAdhocTaskNameGroupEl) rtwlAdhocTaskNameGroupEl.style.display = rtwlTaskSelectTimerEl.value === 'adhoc' ? 'block' : 'none'; }); if(rtwlStartNewTimerButtonEl) rtwlStartNewTimerButtonEl.addEventListener('click', rtwlStartNewTimer); if(rtwlPauseResumeTimerButtonEl) rtwlPauseResumeTimerButtonEl.addEventListener('click', rtwlTogglePauseResumeTimer); if(rtwlStopTimerButtonEl) rtwlStopTimerButtonEl.addEventListener('click', rtwlStopAndLogTimer); if(rtwlDiscardTimerButtonEl) rtwlDiscardTimerButtonEl.addEventListener('click', rtwlDiscardActiveTimer); if(rtwlLogViewDateEl) rtwlLogViewDateEl.addEventListener('change', rtwlRenderDailyLogEntries); if(rtwlReportPeriodSelectEl) rtwlReportPeriodSelectEl.addEventListener('change', () => { if(rtwlCustomDateRangePickerEl) rtwlCustomDateRangePickerEl.style.display = rtwlReportPeriodSelectEl.value === 'custom_range' ? 'grid' : 'none'; }); if(rtwlDownloadPdfBtn) rtwlDownloadPdfBtn.addEventListener('click', handlePdfDownload); [rtwlEntryStartDateInput, rtwlEntryStartTimeInput, rtwlEntryEndDateInput, rtwlEntryEndTimeInput].forEach(el => { if(el) el.addEventListener('change', rtwlCalculateDurationManual); }); // Removed rtwlAdjustEndTimeManual listener from duration input as it's now read-only in modal const today = new Date(2025, 4, 14); // May 14, 2025 if(rtwlLogViewDateEl) rtwlLogViewDateEl.value = rtwlFormatDateKey(today); if(rtwlCurrentDateDisplayEl) rtwlCurrentDateDisplayEl.textContent = today.toLocaleDateString(); rtwlLoadData(); const firstTabButton = document.querySelector('#realTimeWorkHourLoggingTool .rtwl-tab-button'); if (firstTabButton) firstTabButton.click(); window.addEventListener('click', function(event) { if (event.target.classList.contains('rtwl-modal')) { rtwlCloseModal(event.target.id); } }); });
Scroll to Top