feat(ui): klickbarer Prototyp mit Beispieldaten (#002)

Layout im AdminLTE-Stil (Sidebar, Kopfleiste, Inhaltskopf, Footer), Logo als
SVG-Komponente, Farbschema mit Dark-Mode, Dashboard, Kunden- und Host-Ansichten,
Rechnungsuebersicht, Jahresbahn und globale Suche (Strg+K).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 20:03:15 +02:00
co-authored by Claude Opus 5
parent d98bde9922
commit 8596a18a27
47 changed files with 4128 additions and 40 deletions
+77
View File
@@ -0,0 +1,77 @@
/** Deutsche Formatierung für Datum, Währung und Zahlen. Anzeige-Zeitzone: Europe/Berlin. */
const ZEITZONE = "Europe/Berlin";
const datumFormat = new Intl.DateTimeFormat("de-DE", {
timeZone: ZEITZONE,
day: "2-digit",
month: "2-digit",
year: "numeric",
});
const datumZeitFormat = new Intl.DateTimeFormat("de-DE", {
timeZone: ZEITZONE,
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
});
const waehrungFormat = new Intl.NumberFormat("de-DE", { style: "currency", currency: "EUR" });
export function formatDatum(wert: string | Date | null | undefined): string {
if (!wert) return "–";
return datumFormat.format(new Date(wert));
}
export function formatDatumZeit(wert: string | Date | null | undefined): string {
if (!wert) return "–";
return `${datumZeitFormat.format(new Date(wert))} Uhr`;
}
export function formatEuro(betrag: number | null | undefined): string {
if (betrag === null || betrag === undefined) return "–";
return waehrungFormat.format(betrag);
}
export function formatZahl(wert: number | null | undefined): string {
if (wert === null || wert === undefined) return "–";
return new Intl.NumberFormat("de-DE").format(wert);
}
/** Ganze Tage zwischen heute und einem Datum; negativ, wenn das Datum in der Vergangenheit liegt. */
export function tageBis(datum: string | Date | null | undefined, heute: Date): number | null {
if (!datum) return null;
const tagMs = 24 * 60 * 60 * 1000;
const ziel = Date.UTC(
new Date(datum).getUTCFullYear(),
new Date(datum).getUTCMonth(),
new Date(datum).getUTCDate(),
);
const start = Date.UTC(heute.getUTCFullYear(), heute.getUTCMonth(), heute.getUTCDate());
return Math.round((ziel - start) / tagMs);
}
export function formatTageBis(tage: number | null): string {
if (tage === null) return "–";
if (tage === 0) return "heute";
if (tage === 1) return "in 1 Tag";
if (tage > 1) return `in ${tage} Tagen`;
if (tage === -1) return "seit 1 Tag";
return `seit ${Math.abs(tage)} Tagen`;
}
export function formatUptime(sekunden: number | null | undefined): string {
if (sekunden === null || sekunden === undefined) return "–";
const tage = Math.floor(sekunden / 86400);
const stunden = Math.floor((sekunden % 86400) / 3600);
if (tage > 0) return `${formatZahl(tage)} Tage, ${stunden} h`;
return `${stunden} h`;
}
/** Stunden seit einem Zeitpunkt, für die Regel „keine Meldung seit 48 h“. */
export function stundenSeit(zeitpunkt: string | Date | null | undefined, jetzt: Date): number | null {
if (!zeitpunkt) return null;
return (jetzt.getTime() - new Date(zeitpunkt).getTime()) / 3_600_000;
}