Files
daily_console_web/src/features/store-admin/format.ts
2026-08-25 18:00:16 +05:30

78 lines
3.1 KiB
TypeScript

/**
* 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;
}