Wire DocumentNumberSequence allocator, ledger-based subsidiary/detail reports, and posted-voucher reopen flow without mock ops forms. Co-authored-by: Cursor <cursoragent@cursor.com>
68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
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));
|
||
}
|