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 = `
Date
Project
Duration
Earnings
${sortedEntries.map(entry => {
const project = state.projects.find(p => p.id === entry.projectId);
return `
${new Date(entry.date).toLocaleDateString()}
${project ? project.name : 'N/A'}
${entry.duration.toFixed(2)} hrs
$${entry.earnings.toFixed(2)}
×
`;
}).join('')}
`;
};
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 => `
${new Date(e.date).toLocaleDateString()}
${e.description}
${e.duration.toFixed(2)} hrs
`).join('')}
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();
});