Stand vor Einführung des Nacht-Agenten
This commit is contained in:
commit
4763548bfb
168 changed files with 12726 additions and 0 deletions
53
apps/web/src/components/ui.tsx
Normal file
53
apps/web/src/components/ui.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import type { ReactNode } from 'react';
|
||||
|
||||
/** Einheitliches Statussystem: Text + Symbol, nie nur Farbe. */
|
||||
const MAP: Record<string, { cls: string; icon: string; label: string }> = {
|
||||
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 <span className={`badge ${m.cls}`}><span aria-hidden="true">{m.icon}</span>{m.label}</span>;
|
||||
}
|
||||
export const Alert = ({ kind, children }: { kind: 'err' | 'ok' | 'warn' | 'info'; children: ReactNode }) => <div className={`alert ${kind}`} role={kind === 'err' ? 'alert' : 'status'}>{children}</div>;
|
||||
export const Field = ({ id, label, children, hint }: { id: string; label: string; children: ReactNode; hint?: string }) => (
|
||||
<div className="field"><label htmlFor={id}>{label}</label>{children}{hint && <span className="muted small">{hint}</span>}</div>
|
||||
);
|
||||
export const Empty = ({ title, children }: { title: string; children?: ReactNode }) => <div className="empty"><h3>{title}</h3>{children}</div>;
|
||||
export const fmt = (d: string | Date) => new Date(d).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' });
|
||||
export const ROLE_LABEL: Record<string, string> = { support: 'Support', accounting: 'Buchhaltung', admin: 'Administrator', superadmin: 'Superadministrator', owner: 'Inhaber', member: 'Mitglied' };
|
||||
|
||||
export const STATE: Record<string, { cls: string; icon: string; label: string }> = {
|
||||
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 <span className={`badge ${m.cls}`}><span aria-hidden="true">{m.icon}</span>{m.label}</span>; }
|
||||
export const JOB_LABEL: Record<string, string> = { 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 <span className={`badge ${cls}`}><span aria-hidden="true">{icon}</span>{JOB_LABEL[value] ?? value}</span>;
|
||||
}
|
||||
export const ACTION_LABEL: Record<string, string> = { 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<string, string> = { once: 'einmalig', monthly: 'monatlich', yearly: 'jährlich' };
|
||||
const ORDER: Record<string, [string, string, string]> = {
|
||||
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<string, [string, string, string]> = {
|
||||
pending: ['info', '◐', 'In Vorbereitung'], active: ['ok', '●', 'Aktiv'], suspended: ['lock', '⊘', 'Gesperrt'], cancelled: ['lock', '■', 'Beendet'], expired: ['warn', '▲', 'Abgelaufen'], failed: ['err', '✕', 'Fehlgeschlagen'],
|
||||
};
|
||||
const badge = (m: Record<string, [string, string, string]>, v: string) => { const [c, i, l] = m[v] ?? ['info', '○', v]; return <span className={`badge ${c}`}><span aria-hidden="true">{i}</span>{l}</span>; };
|
||||
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 && <div>Einrichtung: {f(p.setup)}</div>}{(p.recurring.net > 0 || p.interval !== 'once') && <div>{INTERVAL[p.interval]}: {f(p.recurring)}</div>}{p.discountBp > 0 && <div className="small muted">inkl. {p.discountBp / 100} % Rabatt</div>}</>;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue