Files
app/src/admin/utils/formatters.ts
BOHA 7e4469b29a fix(richtext): blank lines survive into PDFs and read-only previews
Quill 2's semantic HTML saves a blank line as a truly empty <p></p> (no
<br> placeholder) — verified in the DB (invoice_sections hold <p></p>,
zero <p><br>) — which collapses to nothing outside the editor.
cleanQuillHtml (PDF path) and the new restoreQuillBlankLines (order/
invoice read-only previews) restore the placeholder at render time, so
existing stored content is covered retroactively.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 08:35:46 +02:00

91 lines
3.1 KiB
TypeScript

export function formatCurrency(
amount: number | string,
currency: string,
): string {
const num = Number(amount) || 0;
switch (currency) {
case "EUR":
return `${num.toLocaleString("cs-CZ", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} €`;
case "USD":
return `$${num.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
case "CZK":
return `${num.toLocaleString("cs-CZ", { minimumFractionDigits: 2, maximumFractionDigits: 2 })} Kč`;
case "GBP":
return `£${num.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
default:
return `${num.toFixed(2)} ${currency}`;
}
}
/**
* Joins per-currency amounts into a single display string
* (e.g. `"1 234,00 Kč · 50,00 €"`). Typed structurally so it stays decoupled
* from the React Query layer's `CurrencyAmount`. Empty/invalid input →
* `formatCurrency(0, "CZK")`.
*/
export function formatMultiCurrency(
amounts: Array<{ amount: number; currency: string }>,
): string {
if (!Array.isArray(amounts) || amounts.length === 0)
return formatCurrency(0, "CZK");
return amounts.map((a) => formatCurrency(a.amount, a.currency)).join(" · ");
}
export function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return "—";
const d = new Date(dateStr);
return d.toLocaleDateString("cs-CZ");
}
/**
* Today's date as `YYYY-MM-DD` in LOCAL (Prague) time — use this for
* date-input defaults. `new Date().toISOString().slice(0, 10)` gives the UTC
* date, which is a day behind the Czech date during the post-midnight window
* (Prague is UTC+1/+2), so a "today" default could land on yesterday.
*/
export function todayLocalStr(): string {
const d = new Date();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${d.getFullYear()}-${m}-${day}`;
}
/**
* Coerce a server-returned numeric (number | decimal-string | null/undefined)
* to a number, falling back ONLY when the value is missing or not numeric —
* NEVER on a legitimate 0 (e.g. a 0% VAT rate / reverse charge). The
* `Number(x) || fallback` idiom silently turns stored zeros into the fallback.
*/
export function numberOr(raw: unknown, fallback: number): number {
if (raw == null || raw === "") return fallback;
const n = Number(raw);
return Number.isFinite(n) ? n : fallback;
}
export function formatKm(km: number | string): string {
return new Intl.NumberFormat("cs-CZ").format(Number(km) || 0);
}
export function czechPlural(
n: number,
one: string,
few: string,
many: string,
): string {
if (n === 1) return one;
if (n >= 2 && n <= 4) return few;
return many;
}
/**
* Quill 2's semantic HTML saves blank lines as truly empty <p></p>, which
* collapse to zero height outside the editor — restore the <br> placeholder
* before rendering stored rich text read-only. (The PDF path does the same
* in cleanQuillHtml.)
*/
export function restoreQuillBlankLines(
html: string | null | undefined,
): string {
return (html || "").replace(/<p([^>]*)>\s*<\/p>/gi, "<p$1><br></p>");
}