Legal Case Time Tracking Software
Live Time Tracker
00:00:00
No active timer
Today's Time Log
| Case | Description | Duration | Billable |
|---|
Add New Case
All Cases & Billing Summary
No cases added yet.
`; return; } state.cases.forEach(c => { const caseEntries = state.timeEntries.filter(e => e.caseId === c.id); const totalSeconds = caseEntries.reduce((acc, entry) => acc + entry.duration, 0); const totalBillable = (totalSeconds / 3600) * c.rate; const item = document.createElement('div'); item.className = 'p-4 border border-gray-200 rounded-lg'; item.innerHTML = `${c.name}
${c.client} - ${formatCurrency(c.rate)}/hr
Total Hours Logged: ${formatHours(totalSeconds)}
Total Billable: ${formatCurrency(totalBillable)}
`;
caseListContainer.appendChild(item);
});
}
// --- TIMER LOGIC ---
function startTimer() {
const caseId = parseInt(timerCaseSelect.value);
if (!caseId) {
alert('Please select a case before starting the timer.');
return;
}
if (state.activeTimer.intervalId) {
alert('A timer is already running.');
return;
}
state.activeTimer.caseId = caseId;
state.activeTimer.startTime = Date.now();
state.activeTimer.intervalId = setInterval(() => {
const elapsed = Math.floor((Date.now() - state.activeTimer.startTime) / 1000);
timerDisplay.textContent = formatDuration(elapsed);
}, 1000);
const caseInfo = state.cases.find(c => c.id === caseId);
activeTimerCaseDisplay.textContent = `Tracking: ${caseInfo.name}`;
startTimerBtn.disabled = true;
stopTimerBtn.disabled = false;
timerCaseSelect.disabled = true;
}
function stopTimer() {
if (!state.activeTimer.intervalId) return;
clearInterval(state.activeTimer.intervalId);
const endTime = Date.now();
const duration = Math.floor((endTime - state.activeTimer.startTime) / 1000);
const description = prompt(`Timer stopped at ${formatDuration(duration)}. Please enter a description for this time entry:`, "");
if (description !== null && description.trim() !== '') {
const newEntry = {
id: Date.now(),
caseId: state.activeTimer.caseId,
description: description.trim(),
duration: duration,
date: new Date().toISOString().split('T')[0]
};
state.timeEntries.push(newEntry);
renderAll();
}
// Reset timer state
state.activeTimer.intervalId = null;
state.activeTimer.startTime = null;
state.activeTimer.caseId = null;
timerDisplay.textContent = '00:00:00';
activeTimerCaseDisplay.textContent = 'No active timer';
startTimerBtn.disabled = false;
stopTimerBtn.disabled = true;
timerCaseSelect.disabled = false;
timerCaseSelect.value = '';
}
// --- EVENT HANDLERS ---
function switchTab(tabName) {
state.currentTab = tabName;
Object.values(tabs).forEach(tab => tab.classList.remove('active'));
Object.values(contents).forEach(content => content.classList.add('hidden'));
tabs[tabName].classList.add('active');
contents[tabName].classList.remove('hidden');
updateNavButtons();
}
function updateNavButtons() {
navButtons.prev.disabled = state.currentTab === 'dashboard';
navButtons.next.disabled = state.currentTab === 'management';
}
caseForm.addEventListener('submit', (e) => {
e.preventDefault();
const caseData = {
id: state.editingCaseId ? state.editingCaseId : Date.now(),
name: caseNameInput.value.trim(),
client: clientNameInput.value.trim(),
rate: parseFloat(hourlyRateInput.value)
};
if (state.editingCaseId) {
const index = state.cases.findIndex(c => c.id === state.editingCaseId);
if (index !== -1) state.cases[index] = caseData;
} else {
state.cases.push(caseData);
}
resetCaseForm();
renderAll();
});
caseListContainer.addEventListener('click', (e) => {
const target = e.target;
const id = parseInt(target.dataset.id);
if (target.classList.contains('edit-case-btn')) {
const caseToEdit = state.cases.find(c => c.id === id);
if (caseToEdit) {
caseFormTitle.textContent = 'Edit Case';
state.editingCaseId = caseToEdit.id;
caseNameInput.value = caseToEdit.name;
clientNameInput.value = caseToEdit.client;
hourlyRateInput.value = caseToEdit.rate;
cancelCaseEditBtn.classList.remove('hidden');
caseForm.querySelector('button[type="submit"]').textContent = 'Update Case';
}
} else if (target.classList.contains('delete-case-btn')) {
if (confirm('Are you sure you want to delete this case? This will also remove all its time entries.')) {
state.cases = state.cases.filter(c => c.id !== id);
state.timeEntries = state.timeEntries.filter(e => e.caseId !== id);
renderAll();
}
}
});
downloadPdfBtn.addEventListener('click', () => {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
doc.setFontSize(20);
doc.text("Timesheet Report", 14, 22);
doc.setFontSize(11);
doc.setTextColor(100);
doc.text(`Report Generated: ${new Date().toLocaleDateString()}`, 14, 30);
const head = [['Date', 'Case Name', 'Description', 'Duration (Hrs)', 'Billable Amount']];
const body = state.timeEntries.map(entry => {
const caseInfo = state.cases.find(c => c.id === entry.caseId);
if (!caseInfo) return [];
const billableAmount = (entry.duration / 3600) * caseInfo.rate;
return [
entry.date,
caseInfo.name,
entry.description,
formatHours(entry.duration),
formatCurrency(billableAmount)
];
}).filter(row => row.length > 0);
doc.autoTable({
startY: 35,
head: head,
body: body,
theme: 'striped',
headStyles: { fillColor: [5, 150, 105] }, // emerald-600
});
doc.save('timesheet_report.pdf');
});
function resetCaseForm() {
caseForm.reset();
state.editingCaseId = null;
caseFormTitle.textContent = 'Add New Case';
cancelCaseEditBtn.classList.add('hidden');
caseForm.querySelector('button[type="submit"]').textContent = 'Save Case';
}
// --- ATTACH EVENT LISTENERS ---
tabs.dashboard.addEventListener('click', () => switchTab('dashboard'));
tabs.management.addEventListener('click', () => switchTab('management'));
navButtons.next.addEventListener('click', () => switchTab('management'));
navButtons.prev.addEventListener('click', () => switchTab('dashboard'));
startTimerBtn.addEventListener('click', startTimer);
stopTimerBtn.addEventListener('click', stopTimer);
cancelCaseEditBtn.addEventListener('click', resetCaseForm);
// --- INITIALIZATION ---
function init() {
loadInitialData();
renderAll();
updateNavButtons();
}
init();
});
