78 lines
3.1 KiB
TypeScript
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;
|
|
}
|