- Frontend React (SKEEN Brand) con Vite, TypeScript, Tailwind - Frontend Homenest (versión alternativa) - Módulos Odoo 17 custom (citas, pacientes, monedero, pagos, ventas, inventario, whatsapp) - WACRM fork (Next.js 16 + Supabase) - Hermes + Bridge + Skills (Qwen3.6 via Nan Builders) - Scripts de migración y operación - Documentación extensiva en docs/
98 lines
3.7 KiB
TypeScript
98 lines
3.7 KiB
TypeScript
/**
|
|
* Currency — single source of truth for deal-value formatting and
|
|
* the currency picker options.
|
|
*
|
|
* Before this module, ~6 components each defined their own
|
|
* `Intl.NumberFormat(..., { currency: "USD" })` helper with USD
|
|
* baked in. The default currency is now configurable per account
|
|
* (accounts.default_currency, migration 021), so every formatter
|
|
* takes a currency and falls back to DEFAULT_CURRENCY only when
|
|
* nothing is known.
|
|
*/
|
|
|
|
/** App-wide fallback when no account/deal currency is available. */
|
|
export const DEFAULT_CURRENCY = "USD";
|
|
|
|
export interface CurrencyOption {
|
|
/** ISO-4217 code, e.g. "USD". Stored verbatim in the DB. */
|
|
code: string;
|
|
/** Human label for the dropdown, e.g. "US Dollar". */
|
|
label: string;
|
|
/** Symbol for compact display, e.g. "$". */
|
|
symbol: string;
|
|
}
|
|
|
|
/**
|
|
* The currencies offered in pickers. Codes must be valid ISO-4217 so
|
|
* `Intl.NumberFormat` renders the right symbol/grouping. Extend this
|
|
* list to offer more — nothing else needs to change.
|
|
*/
|
|
export const CURRENCIES: CurrencyOption[] = [
|
|
{ code: "USD", label: "US Dollar", symbol: "$" },
|
|
{ code: "EUR", label: "Euro", symbol: "€" },
|
|
{ code: "GBP", label: "British Pound", symbol: "£" },
|
|
{ code: "INR", label: "Indian Rupee", symbol: "₹" },
|
|
{ code: "AUD", label: "Australian Dollar", symbol: "A$" },
|
|
{ code: "CAD", label: "Canadian Dollar", symbol: "C$" },
|
|
{ code: "BRL", label: "Brazilian Real", symbol: "R$" },
|
|
{ code: "JPY", label: "Japanese Yen", symbol: "¥" },
|
|
{ code: "CNY", label: "Chinese Yuan", symbol: "¥" },
|
|
{ code: "AED", label: "UAE Dirham", symbol: "د.إ" },
|
|
{ code: "ZAR", label: "South African Rand", symbol: "R" },
|
|
{ code: "NGN", label: "Nigerian Naira", symbol: "₦" },
|
|
{ code: "SGD", label: "Singapore Dollar", symbol: "S$" },
|
|
{ code: "MXN", label: "Mexican Peso", symbol: "$" },
|
|
];
|
|
|
|
/**
|
|
* Format a deal value as a currency string. Whole-number output
|
|
* (no minor units) — deal values are tracked to the dollar across
|
|
* the app. `currency` defaults to USD so callers with nothing better
|
|
* stay safe, but pass the account/deal currency wherever known.
|
|
*
|
|
* Total by design: `Intl.NumberFormat` throws a RangeError on a
|
|
* structurally invalid currency code, and `deals.currency` carries
|
|
* NO DB CHECK (only `accounts.default_currency` does), so legacy
|
|
* rows, imports, or hand-edited data can hold malformed values like
|
|
* "United States". We never let that crash a render — on a bad code
|
|
* we fall back to "CODE 1,234".
|
|
*/
|
|
export function formatCurrency(
|
|
value: number,
|
|
currency: string = DEFAULT_CURRENCY,
|
|
): string {
|
|
const code = (currency || DEFAULT_CURRENCY).trim();
|
|
const amount = Number(value) || 0;
|
|
try {
|
|
return new Intl.NumberFormat(undefined, {
|
|
style: "currency",
|
|
currency: code,
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0,
|
|
}).format(amount);
|
|
} catch {
|
|
// Invalid ISO code — show the raw code + grouped number so the
|
|
// value is still legible instead of throwing.
|
|
return `${code} ${new Intl.NumberFormat(undefined, {
|
|
maximumFractionDigits: 0,
|
|
}).format(amount)}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compact currency for tight spaces (donut center, legend rows):
|
|
* "$1.2M" / "€34.5k" / "₹900". Uses the currency's symbol from
|
|
* CURRENCIES, falling back to the code when we don't carry a symbol.
|
|
*/
|
|
export function formatCurrencyShort(
|
|
value: number,
|
|
currency: string = DEFAULT_CURRENCY,
|
|
): string {
|
|
const code = currency || DEFAULT_CURRENCY;
|
|
const symbol = CURRENCIES.find((c) => c.code === code)?.symbol ?? `${code} `;
|
|
const v = Number(value || 0);
|
|
if (v >= 1_000_000) return `${symbol}${(v / 1_000_000).toFixed(1)}M`;
|
|
if (v >= 1_000) return `${symbol}${(v / 1_000).toFixed(1)}k`;
|
|
return `${symbol}${v.toFixed(0)}`;
|
|
}
|