TorbatYar/frontend/lib/utils.ts
Mortezakoohjani 7953e47f8b Ship accounting UX: Rial integers, auto doc numbers, reverse-and-edit, real reports, customers nav.
Wire DocumentNumberSequence allocator, ledger-based subsidiary/detail reports, and posted-voucher reopen flow without mock ops forms.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-26 15:52:06 +03:30

68 lines
2.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
const PERSIAN_DIGITS = "۰۱۲۳۴۵۶۷۸۹";
const ARABIC_DIGITS = "٠١٢٣٤٥٦٧٨٩";
/** Normalize user/API money text to a plain Latin numeric string (no thousand separators). */
export function parseMoneyInput(value: string | number | null | undefined): string {
if (value === null || value === undefined) return "";
let s = String(value).trim();
if (!s) return "";
s = s
.split("")
.map((ch) => {
const pi = PERSIAN_DIGITS.indexOf(ch);
if (pi >= 0) return String(pi);
const ai = ARABIC_DIGITS.indexOf(ch);
if (ai >= 0) return String(ai);
return ch;
})
.join("");
s = s.replace(/[,\s٬]/g, "").replace("٫", ".");
const negative = s.startsWith("-");
s = s.replace(/-/g, "");
const parts = s.split(".");
const intPart = (parts[0] || "").replace(/\D/g, "") || "0";
const decPart = parts.length > 1 ? parts.slice(1).join("").replace(/\D/g, "") : "";
// Preserve empty while user cleared the field (avoid forcing "0" mid-edit).
if (!intPart && !decPart && !/[0-9۰-۹٠-٩]/.test(String(value))) return "";
const normalized = decPart ? `${intPart || "0"}.${decPart}` : intPart || "0";
return negative ? `-${normalized}` : normalized;
}
/** Round/normalize money to integer Rial string (no fractional part). */
export function toRialInteger(value: string | number | null | undefined): string {
const raw = parseMoneyInput(value);
if (!raw) return "";
const n = Number(raw);
if (Number.isNaN(n)) return raw;
return String(Math.round(n));
}
/** Group every 3 digits for MoneyInput display (LTR Latin digits + comma). Rial: no decimals. */
export function formatMoneyGrouped(value: string | number | null | undefined): string {
const raw = toRialInteger(value);
if (!raw) return "";
const negative = raw.startsWith("-");
const body = negative ? raw.slice(1) : raw;
const grouped = (body || "0").replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return negative ? `-${grouped}` : grouped;
}
/** Display money in fa-IR with thousand separators — Rial has no decimal places. */
export function formatMoney(value: string | number | null | undefined, locale = "fa-IR") {
if (value === null || value === undefined || value === "") return "—";
const raw = typeof value === "string" ? parseMoneyInput(value) : String(value);
const n = Number(raw);
if (Number.isNaN(n)) return String(value);
return new Intl.NumberFormat(locale, {
maximumFractionDigits: 0,
minimumFractionDigits: 0,
}).format(Math.round(n));
}