Initial commit
This commit is contained in:
77
src/features/store-admin/format.ts
Normal file
77
src/features/store-admin/format.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 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 * 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;
|
||||
}
|
||||
Reference in New Issue
Block a user