Grundgeruest der Anwendung: Next.js 15 (App Router), TypeScript im strict-Modus, Tailwind mit shadcn/ui-Farbtokens. Bewusste Festlegungen: - Nur Systemschriften, keine externen Fonts, kein CDN. - Grundschrift eine Stufe groesser, Fokusring immer sichtbar, Touchziele mindestens 44px. - Ampelfarben fuer Fristen als eigene Design-Tokens. - Betraege werden als Cent-Ganzzahl gefuehrt, damit die Rechnungslegung keine Rundungsfehler erbt. - .env.example dokumentiert alle Geheimnisse; npm run schluessel erzeugt sie. Etappe: 1 Phase: Fundament
82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
import { type ClassValue, clsx } from 'clsx'
|
||
import { twMerge } from 'tailwind-merge'
|
||
|
||
/** Tailwind-Klassen zusammenfuehren, spaetere gewinnen. */
|
||
export function cn(...inputs: ClassValue[]) {
|
||
return twMerge(clsx(inputs))
|
||
}
|
||
|
||
const DATUM_FORMAT = new Intl.DateTimeFormat('de-DE', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
})
|
||
|
||
const DATUM_ZEIT_FORMAT = new Intl.DateTimeFormat('de-DE', {
|
||
day: '2-digit',
|
||
month: '2-digit',
|
||
year: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
})
|
||
|
||
const EURO_FORMAT = new Intl.NumberFormat('de-DE', {
|
||
style: 'currency',
|
||
currency: 'EUR',
|
||
})
|
||
|
||
/** 22.08.2026 - oder ein Gedankenstrich, wenn nichts da ist. */
|
||
export function formatDatum(datum: Date | string | null | undefined): string {
|
||
if (!datum) return '–'
|
||
const d = typeof datum === 'string' ? new Date(datum) : datum
|
||
if (Number.isNaN(d.getTime())) return '–'
|
||
return DATUM_FORMAT.format(d)
|
||
}
|
||
|
||
/** 22.08.2026, 14:30 */
|
||
export function formatDatumZeit(datum: Date | string | null | undefined): string {
|
||
if (!datum) return '–'
|
||
const d = typeof datum === 'string' ? new Date(datum) : datum
|
||
if (Number.isNaN(d.getTime())) return '–'
|
||
return DATUM_ZEIT_FORMAT.format(d)
|
||
}
|
||
|
||
/**
|
||
* Betraege liegen in der Datenbank als Cent-Ganzzahl (BigInt), damit
|
||
* keine Rundungsfehler in die Rechnungslegung wandern.
|
||
*/
|
||
export function formatCent(cent: bigint | number | null | undefined): string {
|
||
if (cent === null || cent === undefined) return '–'
|
||
const zahl = typeof cent === 'bigint' ? Number(cent) : cent
|
||
return EURO_FORMAT.format(zahl / 100)
|
||
}
|
||
|
||
/** Tage zwischen heute (Mitternacht) und einem Datum. Negativ = ueberfaellig. */
|
||
export function tageBis(datum: Date | string): number {
|
||
const ziel = typeof datum === 'string' ? new Date(datum) : datum
|
||
const heute = new Date()
|
||
heute.setHours(0, 0, 0, 0)
|
||
const zielTag = new Date(ziel)
|
||
zielTag.setHours(0, 0, 0, 0)
|
||
return Math.round((zielTag.getTime() - heute.getTime()) / 86_400_000)
|
||
}
|
||
|
||
/** "vor 3 Tagen", "in 2 Wochen" - fuer die Dashboard-Kacheln. */
|
||
export function relativeTage(tage: number): string {
|
||
if (tage === 0) return 'heute'
|
||
if (tage === 1) return 'morgen'
|
||
if (tage === -1) return 'gestern'
|
||
if (tage < 0) return `vor ${Math.abs(tage)} Tagen`
|
||
return `in ${tage} Tagen`
|
||
}
|
||
|
||
/** Aus "Erika Mustermann" wird "EM" - fuer Avatare in der Navigation. */
|
||
export function initialen(name: string): string {
|
||
return name
|
||
.split(/\s+/)
|
||
.filter(Boolean)
|
||
.slice(0, 2)
|
||
.map((teil) => teil[0]?.toUpperCase() ?? '')
|
||
.join('')
|
||
}
|