const fmtPct1 = new Intl.NumberFormat("fr-CA", { minimumFractionDigits: 1, maximumFractionDigits: 1 }); const fmtPct0 = new Intl.NumberFormat("fr-CA", { maximumFractionDigits: 0 }); const fmtInt = new Intl.NumberFormat("fr-CA"); export function pct(v: number | null | undefined, digits: 0 | 1 = 1, suffix = " %"): string { if (v === null || v === undefined || Number.isNaN(v)) return "—"; return (digits === 1 ? fmtPct1 : fmtPct0).format(v) + suffix; } export function prob(p: number | null | undefined): string { if (p === null || p === undefined) return "—"; if (p > 0.995) return "> 99 %"; if (p < 0.005) return "< 1 %"; return fmtPct0.format(p * 100) + " %"; } export function delta(v: number | null | undefined, digits: 0 | 1 = 1, suffix = ""): string { if (v === null || v === undefined || Number.isNaN(v)) return "—"; const f = (digits === 1 ? fmtPct1 : fmtPct0).format(Math.abs(v)); if (Math.abs(v) < (digits === 1 ? 0.05 : 0.5)) return "0" + suffix; return (v > 0 ? "+" : "−") + f + suffix; } export function int(v: number | null | undefined): string { if (v === null || v === undefined) return "—"; return fmtInt.format(Math.round(v)); } export function dateFr(iso: string | null | undefined, opts: Intl.DateTimeFormatOptions = { day: "numeric", month: "long", year: "numeric" }): string { if (!iso) return "—"; const d = iso.length === 10 ? new Date(iso + "T12:00:00") : new Date(iso); return new Intl.DateTimeFormat("fr-CA", { timeZone: "America/Toronto", ...opts }).format(d); } export function dateShort(iso: string | null | undefined): string { return dateFr(iso, { day: "numeric", month: "short" }); } export function dateTimeFr(iso: string | null | undefined): string { if (!iso) return "—"; return new Intl.DateTimeFormat("fr-CA", { timeZone: "America/Toronto", day: "numeric", month: "long", hour: "2-digit", minute: "2-digit" }).format(new Date(iso)); } export function timeFr(iso: string | null | undefined): string { if (!iso) return "—"; return new Intl.DateTimeFormat("fr-CA", { timeZone: "America/Toronto", hour: "2-digit", minute: "2-digit", second: "2-digit" }).format(new Date(iso)); } export function relTime(iso: string | null | undefined): string { if (!iso) return "—"; const s = Math.round((Date.now() - new Date(iso).getTime()) / 1000); if (s < 60) return `il y a ${s} s`; if (s < 3600) return `il y a ${Math.round(s / 60)} min`; if (s < 86400) return `il y a ${Math.round(s / 3600)} h`; return `il y a ${Math.round(s / 86400)} j`; } export function daysLabel(d: number): string { if (d > 0) return `J-${d}`; if (d === 0) return "JOUR J"; return `J+${-d}`; } export function ratingLabel(r: string | null | undefined): string { return ({ safe: "Acquis", likely: "Probable", lean: "Favori fragile", tossup: "Serré" } as Record)[r ?? ""] ?? "—"; } export function statusLabel(s: string | null | undefined): string { return ({ NOT_STARTED: "En attente", REPORTING: "Dépouillement", LEADING: "En avance", PROJECTED: "Projeté QC26", ELECTED: "Élu·e", FINAL: "Final" } as Record)[s ?? ""] ?? "—"; } export function modeLabel(m: string | null | undefined): string { return ({ web: "Web", phone: "Téléphone", ivr: "IVR (automatisé)", mixed: "Mixte", sms: "SMS", unknown: "Non précisé" } as Record)[m ?? ""] ?? (m ?? "—"); } export function cls(...xs: (string | false | null | undefined)[]): string { return xs.filter(Boolean).join(" "); }