/** * Number formatting, in one place. * * Every figure on these pages is money, a count, or a percentage, and each has * exactly one correct rendering. Scattering `toFixed(2)` across four pages is * how ₹1,350 becomes ₹1350.00 on one screen and ₹1.35K on another. */ const INR = new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', maximumFractionDigits: 0, }); const INR_PRECISE = new Intl.NumberFormat('en-IN', { style: 'currency', currency: 'INR', minimumFractionDigits: 2, maximumFractionDigits: 2, }); const COUNT = new Intl.NumberFormat('en-IN'); /** * Money for a KPI or a table cell — whole rupees. * * Paise are dropped on purpose. A day's takings read at a glance do not need * two decimal places, and the digits they add are the two the eye has to skip * past to compare two branches. */ export const money = (value: number | undefined | null): string => value === undefined || value === null || !Number.isFinite(value) ? '—' : INR.format(value); /** Money where the paise matter — a single bill, a unit price, a tax line. */ export const moneyExact = (value: number | undefined | null): string => value === undefined || value === null || !Number.isFinite(value) ? '—' : INR_PRECISE.format(value); export const count = (value: number | undefined | null): string => value === undefined || value === null || !Number.isFinite(value) ? '—' : COUNT.format(value); /** * A percentage of a total, guarded. * * Returns `null` rather than 0 when the denominator is zero: "0% cancelled" * across no orders is a claim the data does not support, and a tile that says * "—" is the honest rendering of a shop that has not traded yet. */ export function share(part: number | undefined, total: number | undefined): number | null { if (!total || !Number.isFinite(total) || total <= 0) return null; if (part === undefined || !Number.isFinite(part)) return null; return part / total; } export const percent = (value: number | null): string => value === null ? '—' : value === 0 ? '0%' : `${(value * 100).toFixed(value < 0.1 ? 1 : 0)}%`; /** A short local time — "14:32". Used on bills and last-sync stamps. */ export const clock = (date: Date | null): string => date ? date.toLocaleTimeString('en-IN', { hour: '2-digit', minute: '2-digit', hour12: false }) : '—'; /** * A branch name with the tenant prefix stripped: "R mart — Peelamedu" → "Peelamedu". * * Every row of every table in this workspace belongs to the same merchant, so * repeating their name down a column is pure noise — and it is expensive noise: * the prefix cost about 55px per row, which was enough to push the Amount * column off the edge of the deliveries table on a 1440px screen. * * Splits on an em dash or a hyphen surrounded by spaces, which is how the * onboarding form composes these names. A name without a separator is returned * untouched rather than guessed at. */ export function branchLabel(name: string | undefined): string { if (!name) return '—'; const match = name.split(/\s+[—–-]\s+/); return match.length > 1 ? (match[match.length - 1] as string) : name; }