2026-07-19 18:38:16 +00:00
|
|
|
import { clsx, type ClassValue } from "clsx";
|
|
|
|
|
import { twMerge } from "tailwind-merge";
|
|
|
|
|
|
|
|
|
|
export function cn(...inputs: ClassValue[]) {
|
|
|
|
|
return twMerge(clsx(inputs));
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-21 19:38:47 +00:00
|
|
|
/**
|
|
|
|
|
* Deterministic number formatters.
|
|
|
|
|
*
|
|
|
|
|
* We deliberately avoid `Intl.NumberFormat("en-ZA", …)` because Node and the
|
|
|
|
|
* browser ship different ICU builds — on the server, `en-ZA` falls back to a
|
|
|
|
|
* generic locale and renders "342,707.17", while the browser renders the
|
|
|
|
|
* expected South African "342 707,17". That mismatch blows up SSR hydration
|
|
|
|
|
* (https://react.dev/link/hydration-mismatch). Hard-coding the format keeps
|
|
|
|
|
* server and client byte-identical.
|
|
|
|
|
*
|
|
|
|
|
* Style: space as thousand separator, comma as decimal point, exactly the
|
|
|
|
|
* shape `Intl` produces in a real en-ZA browser. Always positive.
|
|
|
|
|
*/
|
|
|
|
|
function groupThousands(intPart: string): string {
|
|
|
|
|
// Add a space every 3 digits from the right.
|
|
|
|
|
return intPart.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
|
2026-07-19 18:38:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatNumber(value: number | null | undefined, decimals = 2): string {
|
2026-07-21 19:38:47 +00:00
|
|
|
if (value == null || isNaN(value)) return "0";
|
|
|
|
|
// toFixed gives a stable, locale-independent string. Split on the decimal
|
|
|
|
|
// point so we can rejoin with a comma.
|
|
|
|
|
const [intPart, fracPart] = value.toFixed(decimals).split(".");
|
|
|
|
|
const sign = intPart.startsWith("-") ? "-" : "";
|
|
|
|
|
const digits = sign ? intPart.slice(1) : intPart;
|
|
|
|
|
const grouped = groupThousands(digits);
|
|
|
|
|
return fracPart != null ? `${sign}${grouped},${fracPart}` : `${sign}${grouped}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatCurrency(value: number | null | undefined): string {
|
|
|
|
|
if (value == null || isNaN(value)) return "R0,00";
|
|
|
|
|
return `R ${formatNumber(value, 2)}`;
|
2026-07-19 18:38:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatDate(value: string | Date | null | undefined): string {
|
|
|
|
|
if (!value) return "";
|
|
|
|
|
const d = typeof value === "string" ? new Date(value) : value;
|
|
|
|
|
if (isNaN(d.getTime())) return "";
|
2026-07-21 19:38:47 +00:00
|
|
|
// "21 Jul 2026" — stable across server and client.
|
|
|
|
|
const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
|
|
|
return `${String(d.getDate()).padStart(2, "0")} ${months[d.getMonth()]} ${d.getFullYear()}`;
|
2026-07-19 18:38:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function formatDateInput(value: string | Date | null | undefined): string {
|
|
|
|
|
if (!value) return "";
|
|
|
|
|
const d = typeof value === "string" ? new Date(value) : value;
|
|
|
|
|
if (isNaN(d.getTime())) return "";
|
2026-07-21 19:38:47 +00:00
|
|
|
// YYYY-MM-DD for <input type="date"> — already locale-neutral.
|
2026-07-19 18:38:16 +00:00
|
|
|
const yyyy = d.getFullYear();
|
|
|
|
|
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
|
|
|
const dd = String(d.getDate()).padStart(2, "0");
|
|
|
|
|
return `${yyyy}-${mm}-${dd}`;
|
|
|
|
|
}
|