// Currency formatting for the client, mirroring the server's utils/money.js. // // The API sends money in two units: bill / summary values are serialized as // DOLLARS (the server calls fromCents before responding), while raw bank // transaction amounts arrive as integer CENTS. So there are two entry points — // formatUSD(dollars) and formatCentsUSD(cents) — matching the server's // formatUSD / formatCentsUSD split. USD / en-US throughout (the app is USD-only, // and this matches the server). Inputs are coerced defensively so null, '', // undefined, or NaN never render as "$NaN". const DASH = '—'; function toNumber(value) { const n = Number(value); if (!Number.isFinite(n)) return 0; return n === 0 ? 0 : n; // normalize -0 → +0 so it never renders as "-$0.00" } function isBlank(value) { return value === null || value === undefined || value === ''; } /** * Format a DOLLAR amount → "$1,234.56". * @param {number|string|null} dollars * @param {{ whole?: boolean, dash?: boolean }} [opts] * whole — drop the cents ("$1,235"); dash — render blank input as "—" not "$0.00". */ export function formatUSD(dollars, { whole = false, dash = false } = {}) { if (dash && isBlank(dollars)) return DASH; return toNumber(dollars).toLocaleString('en-US', { style: 'currency', currency: 'USD', minimumFractionDigits: whole ? 0 : 2, maximumFractionDigits: whole ? 0 : 2, }); } /** Whole-dollar convenience → "$1,235". */ export function formatUSDWhole(dollars, opts = {}) { return formatUSD(dollars, { ...opts, whole: true }); } /** * Format an integer-CENTS amount (e.g. a bank transaction) → "$12.34". * @param {number|string|null} cents * @param {{ signed?: boolean, dash?: boolean, currency?: string }} [opts] * signed — prefix "+"/"-" (income vs expense); dash — blank input → "—"; * currency — ISO code (defaults USD). */ export function formatCentsUSD(cents, { signed = false, dash = false, currency = 'USD' } = {}) { if (dash && isBlank(cents)) return DASH; const c = toNumber(cents); const body = (Math.abs(c) / 100).toLocaleString('en-US', { style: 'currency', currency: currency || 'USD', minimumFractionDigits: 2, maximumFractionDigits: 2, }); if (signed) return (c < 0 ? '-' : '+') + body; return (c < 0 ? '-' : '') + body; } /** * Shared form validator for a non-negative money field (dollars). Blank is * allowed (returns ''); otherwise the value must parse to a number ≥ 0. Returns * '' when valid, or an error string labelled for the field. Zero is allowed — * these are non-negative, not strictly-positive, amounts. * @param {number|string|null} val * @param {string} [label] — field name for the message (e.g. "Amount", "Balance") */ export function validateNonNegativeMoney(val, label = 'Amount') { if (val === '' || val === null || val === undefined) return ''; const num = parseFloat(val); if (isNaN(num) || num < 0) return `${label} must be a non-negative number`; return ''; }