Files
beenvoice/apps/web/src/lib/time-clock.ts
T

81 lines
2.7 KiB
TypeScript

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).";
}
}