/** * Deutsche Zahlen-, Waehrungs- und Datumsformatierung. * Regeln aus UI_UX_GUIDELINES.md (Abschnitt 3, Zahlenformatierung): * Punkt als Tausender-Trenner, Komma als Dezimaltrenner, grosse Zahlen abkuerzen. */ const NF = new Intl.NumberFormat("de-DE"); /** 12450 -> "12.450" · 1250000 -> "1,3 Mio." */ export function formatZahl(value: number, dezimalstellen = 0): string { if (Math.abs(value) >= 1_000_000) { return `${(value / 1_000_000).toFixed(1).replace(".", ",")} Mio.`; } return new Intl.NumberFormat("de-DE", { minimumFractionDigits: dezimalstellen, maximumFractionDigits: dezimalstellen, }).format(value); } /** 8420 -> "8.420 €" · 245000 -> "245.000 €" (immer voll, fuer KPI-Werte) */ export function formatEuro(value: number): string { return `${NF.format(Math.round(value))} €`; } /** * Kompakte Waehrung fuer Achsen und Balken-Labels. * 8420 -> "8,4 T€" · 245000 -> "245 T€" · 1250000 -> "1,25 Mio. €" */ export function formatEuroKompakt(value: number): string { const abs = Math.abs(value); if (abs >= 1_000_000) { return `${(value / 1_000_000).toFixed(2).replace(".", ",")} Mio. €`; } if (abs >= 1_000) { return `${(value / 1_000).toFixed(1).replace(".", ",")} T€`; } return `${NF.format(Math.round(value))} €`; } /** * Noch kompakter für Achsenbeschriftungen: dort zählt jede Stelle, * und Nachkommastellen sind bei einer Skala ohnehin überflüssig. * 34000 -> "34 T€" · 1250000 -> "1,3 Mio. €" */ export function formatEuroAchse(value: number): string { // Die Nulllinie traegt keine Einheit: "0 €" neben "10 T€" liest sich wie ein // Einheitenwechsel mitten in der Skala. if (value === 0) return "0"; const abs = Math.abs(value); if (abs >= 1_000_000) return `${(value / 1_000_000).toFixed(1).replace(".", ",")} Mio. €`; if (abs >= 1_000) return `${Math.round(value / 1_000)} T€`; return `${NF.format(Math.round(value))} €`; } /** * Achsenformat, einmal aus dem Maximum gewaehlt. * * formatEuroAchse entscheidet die Einheit pro Wert — dadurch steht "0 €" * neben "10 T€". Eine Achse traegt aber genau eine Einheit. */ export function achsenFormat(max: number): (wert: number) => string { if (max >= 1_000_000) { return (wert) => wert === 0 ? "0" : `${(wert / 1_000_000).toFixed(1).replace(".", ",")} Mio. €`; } if (max >= 1_000) { return (wert) => (wert === 0 ? "0" : `${NF.format(Math.round(wert / 1_000))} T€`); } return (wert) => (wert === 0 ? "0" : formatEuro(wert)); } /** 45.32 -> "45,3 %" */ export function formatProzent(value: number, dezimalstellen = 1): string { return `${value.toFixed(dezimalstellen).replace(".", ",")} %`; } const NF1 = new Intl.NumberFormat("de-DE", { minimumFractionDigits: 1, maximumFractionDigits: 1, }); /** 2.4 -> "2,4 Tage" · 1234.5 -> "1.234,5 Tage" */ export function formatTage(value: number): string { return `${NF1.format(value)} Tage`; } /** 2812.5 -> "2.812,5 h" */ export function formatStunden(value: number): string { return `${NF1.format(value)} h`; } /** * Ganze Tage mit korrektem Numerus — "1 Tag", "3 Tage". * Ohne das steht in der Terminspalte "1 Tage ueber". */ export function tageLabel(anzahl: number, suffix = ""): string { const wort = Math.abs(anzahl) === 1 ? "Tag" : "Tage"; return `${NF.format(anzahl)} ${wort}${suffix ? ` ${suffix}` : ""}`; } /** Dativform fuer "seit …": "seit 1 Tag" / "seit 99 Tagen". */ export function tageLabelDativ(anzahl: number): string { const wort = Math.abs(anzahl) === 1 ? "Tag" : "Tagen"; return `${NF.format(anzahl)} ${wort}`; } /** "2026-07-30" -> "30.07.2026" */ export function formatDatum(iso: string): string { const d = new Date(iso); return d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit", year: "numeric", }); } /** "2026-07-30" -> "30.07." */ export function formatDatumKurz(iso: string): string { const d = new Date(iso); return d.toLocaleDateString("de-DE", { day: "2-digit", month: "2-digit" }); } /** "2026-07" -> "Jul 26" */ export function formatMonatKurz(iso: string): string { const [jahr, monat] = iso.split("-"); const namen = ["Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez"]; return `${namen[Number(monat) - 1]} ${jahr.slice(2)}`; } /** Tage zwischen heute und einem ISO-Datum (positiv = in der Vergangenheit). */ export function tageSeit(iso: string, referenz: Date): number { const diff = referenz.getTime() - new Date(iso).getTime(); return Math.floor(diff / 86_400_000); } /** * Trend zwischen aktueller und vorheriger Periode. * Gibt Prozentpunkte und Richtung zurueck; < 0,5 % gilt als unveraendert. */ export function berechneTrend( aktuell: number, vorher: number ): { wert: number; richtung: "up" | "down" | "flat" } { if (vorher === 0) { return aktuell > 0 ? { wert: 100, richtung: "up" } : { wert: 0, richtung: "flat" }; } const veraenderung = ((aktuell - vorher) / vorher) * 100; if (Math.abs(veraenderung) < 0.5) return { wert: 0, richtung: "flat" }; return { wert: Math.abs(veraenderung), richtung: veraenderung > 0 ? "up" : "down", }; } /** Text auf maxLaenge kuerzen, Ellipse anhaengen. */ export function kuerze(text: string, maxLaenge: number): string { return text.length <= maxLaenge ? text : `${text.slice(0, maxLaenge - 1)}…`; }