export type ClockOutOutcome = | "linked_to_invoice" | "saved_no_invoice" | "saved_no_client" | "zero_hours"; export const DEFAULT_CLOCK_DESCRIPTION = "Clock In"; /** Stored on entries clocked in before empty descriptions were allowed. */ export const LEGACY_DEFAULT_CLOCK_DESCRIPTION = "Professional services"; export function resolveClockDescription(description: string | null | undefined): string { const trimmed = description?.trim(); return trimmed || DEFAULT_CLOCK_DESCRIPTION; } export function formatRunningTimerLabel(description?: string | null): string { const trimmed = description?.trim() ?? ""; if ( !trimmed || trimmed === DEFAULT_CLOCK_DESCRIPTION || trimmed === LEGACY_DEFAULT_CLOCK_DESCRIPTION ) { return "Clocked in"; } return trimmed; } export function formatElapsedSeconds(seconds: number): string { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; return [h, m, s].map((v) => String(v).padStart(2, "0")).join(":"); } /** Hours and minutes only — for Live Activity / compact displays. */ export function formatElapsedHoursMinutes(seconds: number): string { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); return `${h}:${String(m).padStart(2, "0")}`; } export function resolveEffectiveHourlyRate( rateText: string, clientDefaultRate?: number | null, ): number | null { const parsed = rateText.trim() ? Number(rateText) : null; if (parsed != null && !Number.isNaN(parsed) && parsed >= 0) return parsed; if (clientDefaultRate != null && clientDefaultRate >= 0) return clientDefaultRate; return null; } export function startedAtFromMinutesAgo(minutes: number): Date { return new Date(Date.now() - minutes * 60_000); } export function describeClockOutOutcome(input: { outcome: ClockOutOutcome; hours: number; rate: number; invoice?: { invoicePrefix: string; invoiceNumber: string } | null; }): string { const amount = input.hours * input.rate; switch (input.outcome) { case "linked_to_invoice": if (input.invoice) { const label = `${input.invoice.invoicePrefix}${input.invoice.invoiceNumber}`; return `Added ${input.hours}h @ $${input.rate}/hr ($${amount.toFixed(2)}) to ${label}`; } return `Added ${input.hours}h to invoice`; case "saved_no_invoice": return `Saved ${input.hours}h — no open invoice for this client.`; case "saved_no_client": return `Saved ${input.hours}h — pick a client and invoice to bill.`; case "zero_hours": return "Timer stopped."; } }