import clsx from "clsx"; import { daysRemaining, formatDate, type LicenceState } from "@/lib/format"; /* * A licence's life as a measured line: issued at the left, expiry at the right, * today as a notch, the part you have not got yet hatched. * * This replaces a 1px progress rule and a "Renews 19 Aug 2026" caption. The * date is still there, but a date alone makes the reader do the arithmetic that * is the only question this product is ever asked — when does this stop * working. The bar answers it before they read a word. * * The fill takes the state's colour, so the same vocabulary the pill uses * carries through. State is never colour alone here either: the remaining span * is hatched rather than tinted, the notch is a hard edge, and the days-left * figure is written out. */ const TONE: Record = { valid: "text-valid", warn: "text-warn", expired: "text-expired", none: "text-accent", }; function span(issuedAt: string, expiresAt: string) { const start = new Date(issuedAt).getTime(); const end = new Date(expiresAt).getTime(); const total = end - start; // A licence issued and expiring at the same instant is not a real record, // but it must not divide by zero on the way to being rendered. if (!Number.isFinite(total) || total <= 0) return 100; const elapsed = Date.now() - start; return Math.max(0, Math.min(100, (elapsed / total) * 100)); } export function TermBar({ issuedAt, expiresAt, state, className, }: { issuedAt: string; expiresAt: string; state: LicenceState; className?: string; }) { const pct = span(issuedAt, expiresAt); const days = daysRemaining(expiresAt); const expired = days <= 0; const remaining = expired ? `Expired ${Math.abs(days)} ${Math.abs(days) === 1 ? "day" : "days"} ago` : `${days} ${days === 1 ? "day" : "days"} left`; return (
{/* The span still to come, drawn as absence rather than as a second colour: it is the thing being bought. */}
{/* * On a phone the three ends stack, and the figure someone actually * came for goes first — wrapping a justify-between row left "9 days * left" marooned between two dates in the middle of the stack. */}
{remaining} Issued {formatDate(issuedAt)} Expires {formatDate(expiresAt)}
); } /* * The same measurement at 56px, for a row in a ledger. Licences, Billing and * the staff expiry queue are all lists of terms, and a list of dates cannot be * scanned for "which of these is nearly out" — a list of bars can. * * It carries a text alternative rather than a title: the row it sits in is * being read, not hovered. */ export function TermSpark({ issuedAt, expiresAt, state }: { issuedAt: string; expiresAt: string; state: LicenceState }) { const pct = span(issuedAt, expiresAt); const days = daysRemaining(expiresAt); return ( {days <= 0 ? `−${Math.abs(days)}d` : `${days}d`} ); }