Work Hour Tracking for Freelancers

Work Hour Tracking for Freelancers

Settings

Add New Project

Log Work Hours

Time Log

Generate Report

No time logged yet.

'; return; } // Sort entries by date, most recent first const sortedEntries = [...state.timeEntries].sort((a, b) => new Date(b.date) - new Date(a.date)); timeLogContainer.innerHTML = ` ${sortedEntries.map(entry => { const project = state.projects.find(p => p.id === entry.projectId); return ` `; }).join('')}
Date Project Duration Earnings
${new Date(entry.date).toLocaleDateString()} ${project ? project.name : 'N/A'} ${entry.duration.toFixed(2)} hrs $${entry.earnings.toFixed(2)}
`; }; const addProject = () => { const name = projectNameInput.value.trim(); if (!name) { alert('Please enter a project name.'); return; } const newProject = { id: Date.now(), name, rate: projectRateInput.value ? parseFloat(projectRateInput.value) : null }; state.projects.push(newProject); projectNameInput.value = ''; projectRateInput.value = ''; saveState(); renderProjects(); }; const addTimeEntry = () => { const projectId = parseInt(entryProjectSelect.value); const date = entryDateInput.value; const start = entryStartInput.value; const end = entryEndInput.value; if (!projectId || !date || !start || !end) { alert('Please fill all time entry fields.'); return; } const startTime = new Date(`${date}T${start}`); const endTime = new Date(`${date}T${end}`); if (endTime <= startTime) { alert('End time must be after start time.'); return; } const durationHours = (endTime - startTime) / 1000 / 60 / 60; const project = state.projects.find(p => p.id === projectId); const rate = project?.rate ?? state.defaultRate; const earnings = durationHours * rate; const newEntry = { id: Date.now(), projectId, date, start, end, description: entryDescriptionInput.value.trim(), duration: durationHours, earnings }; state.timeEntries.push(newEntry); saveState(); renderTimeLog(); // Clear inputs entryDescriptionInput.value = ''; entryStartInput.value = ''; entryEndInput.value = ''; }; const removeTimeEntry = (id) => { state.timeEntries = state.timeEntries.filter(entry => entry.id !== id); saveState(); renderTimeLog(); }; const generateReport = () => { const startDate = reportStartInput.value; const endDate = reportEndInput.value; if (!startDate || !endDate) { alert('Please select a start and end date for the report.'); return; } const start = new Date(startDate); const end = new Date(endDate); const filteredEntries = state.timeEntries.filter(entry => { const entryDate = new Date(entry.date); return entryDate >= start && entryDate <= end; }); if (filteredEntries.length === 0) { reportPreviewOutput.innerHTML = '

No time entries found for the selected date range.

'; } else { const reportByProject = {}; filteredEntries.forEach(entry => { const project = state.projects.find(p => p.id === entry.projectId); const projectName = project ? project.name : 'Uncategorized'; if (!reportByProject[projectName]) { reportByProject[projectName] = { hours: 0, earnings: 0, entries: [] }; } reportByProject[projectName].hours += entry.duration; reportByProject[projectName].earnings += entry.earnings; reportByProject[projectName].entries.push(entry); }); let totalHours = 0; let totalEarnings = 0; let reportHTML = `

Report Period: ${start.toLocaleDateString()} to ${end.toLocaleDateString()}

`; for (const projectName in reportByProject) { const data = reportByProject[projectName]; totalHours += data.hours; totalEarnings += data.earnings; reportHTML += `

${projectName}

${data.entries.map(e => ` `).join('')}
${new Date(e.date).toLocaleDateString()} ${e.description} ${e.duration.toFixed(2)} hrs
Project Total ${data.hours.toFixed(2)} hrs ($${data.earnings.toFixed(2)})
`; } reportHTML += `

Grand Total Hours: ${totalHours.toFixed(2)}

Grand Total Earnings: $${totalEarnings.toFixed(2)}

`; reportPreviewOutput.innerHTML = reportHTML; } reportPreviewContainer.style.display = 'block'; downloadPdfBtn.disabled = false; downloadPdfBtn.classList.remove('bg-gray-600', 'hover:bg-gray-700'); downloadPdfBtn.classList.add('bg-green-600', 'hover:bg-green-700'); reportPreviewContainer.scrollIntoView({ behavior: 'smooth' }); }; const generatePDF = () => { if (downloadPdfBtn.disabled) return; const { jsPDF } = jspdf; const doc = new jsPDF(); const startDate = new Date(reportStartInput.value); const endDate = new Date(reportEndInput.value); const filteredEntries = state.timeEntries.filter(entry => { const entryDate = new Date(entry.date); return entryDate >= startDate && entryDate <= endDate; }); const reportByProject = {}; filteredEntries.forEach(entry => { const project = state.projects.find(p => p.id === entry.projectId); const projectName = project ? project.name : 'Uncategorized'; if (!reportByProject[projectName]) { reportByProject[projectName] = { hours: 0, earnings: 0, entries: [] }; } reportByProject[projectName].hours += entry.duration; reportByProject[projectName].earnings += entry.earnings; reportByProject[projectName].entries.push(entry); }); let totalHours = 0; let totalEarnings = 0; doc.setFontSize(18); doc.text('Work Hours Report', 14, 22); doc.setFontSize(11); doc.setTextColor(100); doc.text(`Period: ${startDate.toLocaleDateString()} to ${endDate.toLocaleDateString()}`, 14, 30); let lastY = 35; for (const projectName in reportByProject) { const data = reportByProject[projectName]; totalHours += data.hours; totalEarnings += data.earnings; doc.setFontSize(14); doc.setTextColor(40); doc.text(projectName, 14, lastY + 10); lastY += 10; const tableBody = data.entries.map(e => [ new Date(e.date).toLocaleDateString(), e.description, `${e.duration.toFixed(2)} hrs` ]); tableBody.push([{ content: `Project Total: ${data.hours.toFixed(2)} hrs ($${data.earnings.toFixed(2)})`, colSpan: 3, styles: { halign: 'right', fontStyle: 'bold' } }]); doc.autoTable({ startY: lastY + 2, head: [['Date', 'Description', 'Duration']], body: tableBody, theme: 'striped' }); lastY = doc.autoTable.previous.finalY; } doc.setFontSize(12); doc.setFont(undefined, 'bold'); doc.text(`Grand Total Hours: ${totalHours.toFixed(2)}`, 14, lastY + 15); doc.text(`Grand Total Earnings: $${totalEarnings.toFixed(2)}`, 14, lastY + 22); doc.save(`Work-Report-${reportStartInput.value}-to-${reportEndInput.value}.pdf`); }; // --- Event Listeners --- defaultRateInput.addEventListener('change', () => { state.defaultRate = parseFloat(defaultRateInput.value) || 50; }); addProjectBtn.addEventListener('click', addProject); addEntryBtn.addEventListener('click', addTimeEntry); timeLogContainer.addEventListener('click', (e) => { if (e.target.classList.contains('remove-entry-btn')) { const id = parseInt(e.target.dataset.id); removeTimeEntry(id); } }); generateReportBtn.addEventListener('click', generateReport); downloadPdfBtn.addEventListener('click', generatePDF); // --- Initial Setup --- const setupInitialData = () => { const today = new Date(); const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); entryDateInput.value = today.toISOString().split('T')[0]; reportEndInput.value = today.toISOString().split('T')[0]; const lastWeek = new Date(today); lastWeek.setDate(today.getDate() - 7); reportStartInput.value = lastWeek.toISOString().split('T')[0]; state.projects = [ { id: 1, name: 'Client A - Marketing Site', rate: 65 }, { id: 2, name: 'Client B - App Backend', rate: null } ]; state.timeEntries = [ { id: 1, projectId: 1, date: yesterday.toISOString().split('T')[0], start: '09:00', end: '11:30', description: 'Homepage design mockups', duration: 2.5, earnings: 2.5 * 65 }, { id: 2, projectId: 2, date: yesterday.toISOString().split('T')[0], start: '13:00', end: '16:00', description: 'API endpoint development', duration: 3, earnings: 3 * 50 }, { id: 3, projectId: 1, date: today.toISOString().split('T')[0], start: '10:00', end: '12:00', description: 'Contact form implementation', duration: 2, earnings: 2 * 65 } ]; renderProjects(); renderTimeLog(); }; setupInitialData(); });
Scroll to Top