import type { ReactNode } from 'react'; /** Einheitliches Statussystem: Text + Symbol, nie nur Farbe. */ const MAP: Record = { active: { cls: 'ok', icon: '●', label: 'Aktiv' }, invited: { cls: 'info', icon: '◐', label: 'Eingeladen' }, disabled: { cls: 'lock', icon: '⊘', label: 'Gesperrt' }, suspended: { cls: 'lock', icon: '⊘', label: 'Gesperrt' }, closed: { cls: 'lock', icon: '■', label: 'Beendet' }, warning: { cls: 'warn', icon: '▲', label: 'Warnung' }, error: { cls: 'err', icon: '✕', label: 'Gestört' }, }; export function Status({ value }: { value: string }) { const m = MAP[value] ?? { cls: 'info', icon: '○', label: value }; return {m.label}; } export const Alert = ({ kind, children }: { kind: 'err' | 'ok' | 'warn' | 'info'; children: ReactNode }) =>
{children}
; export const Field = ({ id, label, children, hint }: { id: string; label: string; children: ReactNode; hint?: string }) => (
{children}{hint && {hint}}
); export const Empty = ({ title, children }: { title: string; children?: ReactNode }) =>

{title}

{children}
; export const fmt = (d: string | Date) => new Date(d).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' }); export const ROLE_LABEL: Record = { support: 'Support', accounting: 'Buchhaltung', admin: 'Administrator', superadmin: 'Superadministrator', owner: 'Inhaber', member: 'Mitglied' }; export const STATE: Record = { active: { cls: 'ok', icon: '●', label: 'Aktiv' }, suspended: { cls: 'lock', icon: '⊘', label: 'Gesperrt' }, expired: { cls: 'warn', icon: '▲', label: 'Abgelaufen' }, error: { cls: 'err', icon: '✕', label: 'Gestört' }, unknown: { cls: 'info', icon: '○', label: 'Unbekannt' }, }; export function ResState({ value }: { value: string }) { const m = STATE[value] ?? STATE.unknown!; return {m.label}; } export const JOB_LABEL: Record = { scheduled: 'Geplant', running: 'Wird ausgeführt', waiting_external: 'Wartet auf externes System', succeeded: 'Erfolgreich', partial: 'Teilweise erfolgreich', failed: 'Fehlgeschlagen', retrying: 'Wird wiederholt', needs_review: 'Manuelle Prüfung erforderlich' }; export function JobStatus({ value }: { value: string }) { const cls = value === 'succeeded' ? 'ok' : value === 'failed' || value === 'needs_review' ? 'err' : value === 'retrying' || value === 'partial' ? 'warn' : 'info'; const icon = cls === 'ok' ? '✓' : cls === 'err' ? '✕' : cls === 'warn' ? '▲' : '◐'; return {JOB_LABEL[value] ?? value}; } export const ACTION_LABEL: Record = { suspend: 'Sperren', unsuspend: 'Entsperren', extend: 'Verlängern', 'panel.login': 'Panel-Login', 'domain.manage': 'Domains verwalten', 'email.manage': 'Postfächer verwalten', 'database.manage': 'Datenbanken verwalten', 'ftp.manage': 'FTP-Zugänge verwalten' }; /** Aktionen, die einem Kunden je Produkt/Ressource freigegeben werden können. */ export const CUSTOMER_ACTIONS = ['suspend', 'unsuspend', 'extend', 'panel.login', 'domain.manage', 'email.manage', 'database.manage', 'ftp.manage'] as const; export const eur = (cents: number): string => (cents / 100).toLocaleString('de-DE', { style: 'currency', currency: 'EUR' }); export const INTERVAL: Record = { once: 'einmalig', monthly: 'monatlich', yearly: 'jährlich' }; const ORDER: Record = { pending_approval: ['warn', '◐', 'Wartet auf Freigabe'], approved: ['info', '◐', 'Freigegeben'], provisioning: ['info', '◐', 'Wird bereitgestellt'], completed: ['ok', '✓', 'Abgeschlossen'], failed: ['err', '✕', 'Fehlgeschlagen'], rejected: ['lock', '⊘', 'Abgelehnt'], cancelled: ['lock', '⊘', 'Storniert'], }; const CONTRACT: Record = { pending: ['info', '◐', 'In Vorbereitung'], active: ['ok', '●', 'Aktiv'], suspended: ['lock', '⊘', 'Gesperrt'], cancelled: ['lock', '■', 'Beendet'], expired: ['warn', '▲', 'Abgelaufen'], failed: ['err', '✕', 'Fehlgeschlagen'], }; const badge = (m: Record, v: string) => { const [c, i, l] = m[v] ?? ['info', '○', v]; return {l}; }; export const OrderStatusBadge = ({ value }: { value: string }) => badge(ORDER, value); export const ContractStatusBadge = ({ value }: { value: string }) => badge(CONTRACT, value); export interface Price { interval: string; quantity: number; taxBp: number; discountBp: number; setup: { net: number; tax: number; gross: number }; recurring: { net: number; tax: number; gross: number } } /** Preisdarstellung: Privatkunden brutto, Geschäftskunden netto mit Brutto in Klammern. */ export function PriceText({ p, priv }: { p: Price; priv: boolean }) { const f = (x: { net: number; gross: number }) => (priv ? eur(x.gross) + ' inkl. USt.' : `${eur(x.net)} netto (${eur(x.gross)} brutto)`); return <>{p.setup.net > 0 &&
Einrichtung: {f(p.setup)}
}{(p.recurring.net > 0 || p.interval !== 'once') &&
{INTERVAL[p.interval]}: {f(p.recurring)}
}{p.discountBp > 0 &&
inkl. {p.discountBp / 100} % Rabatt
}; }