format.ts2.1 KBView on GitHub /**
* Formatters shared by the metric tiles. Rail-sized on purpose: the numbers here sit in a
* 21rem column at a glance, so "2h 14m" beats "2 hours 14 minutes" and "$740k" beats
* "$740,000" — /statistics is where exact figures live.
*/
/** Seconds → the coarsest unit that still says something useful. `null` stays `null`. */
export function formatDuration(seconds: number | null | undefined): string | null {
if (seconds == null || !Number.isFinite(seconds)) return null;
if (seconds < 60) return `${Math.round(seconds)}s`;
const minutes = Math.round(seconds / 60);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
const remainder = minutes % 60;
if (hours < 24) return remainder > 0 ? `${hours}h ${remainder}m` : `${hours}h`;
const days = Math.floor(hours / 24);
const leftoverHours = hours % 24;
return leftoverHours > 0 ? `${days}d ${leftoverHours}h` : `${days}d`;
}
/** Compact currency: $740k, $1.2M. Whole dollars below 1k. */
export function formatCurrency(amount: number | null | undefined): string | null {
if (amount == null || !Number.isFinite(amount)) return null;
const sign = amount < 0 ? '-' : '';
const value = Math.abs(amount);
if (value >= 1_000_000) return `${sign}$${(value / 1_000_000).toFixed(value >= 10_000_000 ? 0 : 1)}M`;
if (value >= 1_000) return `${sign}$${Math.round(value / 1_000)}k`;
return `${sign}$${Math.round(value)}`;
}
/** Thousands separators, no decimals. */
export function formatCount(count: number | null | undefined): string | null {
if (count == null || !Number.isFinite(count)) return null;
return count.toLocaleString('en-US');
}
/**
* A CRM status slug as a human label: `closed_won` → `Closed won`.
* Statuses arrive as free text from the user's own pipeline, so this only tidies casing and
* separators rather than mapping against a fixed vocabulary it cannot know.
*/
export function formatStageLabel(status: string): string {
const cleaned = status.replace(/[_-]+/g, ' ').trim();
if (!cleaned) return 'Unknown';
return cleaned.charAt(0).toUpperCase() + cleaned.slice(1).toLowerCase();
}