import { LEGACY_DEFAULT_CLOCK_DESCRIPTION } from "@beenvoice/domain/time-clock"; import type { ClockOutOutcome } from "@beenvoice/domain/time-clock"; export { formatElapsedSeconds, LEGACY_DEFAULT_CLOCK_DESCRIPTION, type ClockOutOutcome, } from "@beenvoice/domain/time-clock"; export function normalizeOptionalId(value?: string | null): string | null { const trimmed = value?.trim(); return trimmed == null || trimmed === "" ? null : trimmed; } export function resolveEffectiveHourlyRate( enteredRate: number, client?: { defaultHourlyRate?: number | null } | null, ): number { if (Number.isFinite(enteredRate) && enteredRate > 0) return enteredRate; const clientRate = client?.defaultHourlyRate ?? 0; if (Number.isFinite(clientRate) && clientRate > 0) return clientRate; return 0; } export function startedAtFromMinutesAgo(minutes: number): Date { return new Date(Date.now() - minutes * 60 * 1000); } export function resolveClockDescription( title: string, existingDescription?: string | null, ): string { const trimmed = title.trim(); if (trimmed) return trimmed; if (existingDescription?.trim()) return existingDescription.trim(); return ""; } export function formatRunningTimerLabel(description?: string | null): string { const trimmed = description?.trim() ?? ""; if (!trimmed || trimmed === LEGACY_DEFAULT_CLOCK_DESCRIPTION) return "Clocked in"; return trimmed; } export function resolveBillingDescription(description?: string | null): string { const trimmed = description?.trim() ?? ""; if (!trimmed || trimmed === LEGACY_DEFAULT_CLOCK_DESCRIPTION) { return LEGACY_DEFAULT_CLOCK_DESCRIPTION; } return trimmed; } export function computeTrackedHours(startedAt: Date, endedAt: Date): number { const seconds = Math.floor((endedAt.getTime() - startedAt.getTime()) / 1000); return Math.max(0.25, Math.ceil(seconds / 900) * 0.25); } 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 invoice ${label}`; } return `Added ${input.hours}h to invoice`; case "saved_no_invoice": return `Saved ${input.hours}h — could not create or find a draft invoice for this client.`; case "saved_no_client": return `Saved ${input.hours}h — assign a client and invoice to bill this time.`; case "zero_hours": return "Timer stopped (less than minimum billable increment)."; } }