Stand vor Einführung des Nacht-Agenten

This commit is contained in:
Kundencenter 2026-09-27 00:51:32 +02:00
commit 4763548bfb
168 changed files with 12726 additions and 0 deletions

View file

@ -0,0 +1,50 @@
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { api, errMsg } from '@/lib/api';
import { useSession } from '@/lib/session';
import { Alert, Empty } from '@/components/ui';
interface Sys { jobs: Record<string, number>; oldestPendingJob: string | null; backup?: { configured: boolean; lastRunAt?: string | null; ok?: boolean; ageHours?: number | null; stale?: boolean; error?: string | null; sizeBytes?: number | null; targets?: { name: string; ok: boolean }[]; lastRestoreTestAt?: string | null; restoreOk?: boolean | null; restoreStale?: boolean } }
export default function Dashboard() {
const { me, can } = useSession();
const [mine, setMine] = useState<{ contracts: { status: string; cancelEffectiveAt: string | null }[]; resources: { state: string; stale: boolean }[]; orders: { status: string }[] } | null>(null);
const [customers, setCustomers] = useState<number | null>(null); const [sys, setSys] = useState<Sys | null>(null); const [err, setErr] = useState('');
useEffect(() => {
if (me?.kind === 'customer') {
Promise.all([api<any[]>('GET', '/contracts'), api<any[]>('GET', '/resources'), api<any[]>('GET', '/orders')]).then(([contracts, resources, orders]) => setMine({ contracts, resources, orders })).catch((e) => setErr(errMsg(e)));
return;
}
if (!me || me.kind !== 'staff') return;
if (can('customers.read')) api<unknown[]>('GET', '/admin/customers').then((l) => setCustomers(l.length)).catch((e) => setErr(errMsg(e)));
if (can('jobs.read')) api<Sys>('GET', '/admin/system').then(setSys).catch((e) => setErr(errMsg(e)));
}, [me, can]);
if (!me) return null;
const failed = sys ? (sys.jobs.failed ?? 0) + (sys.jobs.needs_review ?? 0) : 0;
return (<>
<h1>Hallo {me.user.name.split(' ')[0]}</h1>
{err && <Alert kind="err">{err}</Alert>}
{me.kind === 'customer' ? (<>
{me.organizations.map((o) => (
<div className="card" key={o.id}><h2>{o.name}</h2><p className="muted">Kundennummer {o.customerNumber}</p><Link className="btn" href="/organisation">Organisation ansehen</Link></div>
))}
{mine && mine.contracts.length === 0 && mine.resources.length === 0 && mine.orders.length === 0 && <div className="card"><Empty title="Noch keine Produkte">Sobald Produkte, Verträge, Rechnungen oder Tickets für Ihre Organisation vorhanden sind, erscheinen sie hier.</Empty></div>}
{mine && (mine.contracts.length > 0 || mine.resources.length > 0 || mine.orders.length > 0) && <div className="grid">
<div className="card"><div className="stat">{mine.contracts.filter((c) => c.status === 'active').length}</div><p className="muted">Aktive Verträge</p>{mine.contracts.some((c) => c.cancelEffectiveAt) && <p className="small"><span className="badge warn"><span aria-hidden="true">▲</span>Gekündigter Vertrag vorhanden</span></p>}<Link href="/vertraege">Verträge ansehen</Link></div>
<div className="card"><div className="stat">{mine.resources.length}</div><p className="muted">Ressourcen und Lizenzen</p>{mine.resources.some((r) => r.stale) && <p className="small"><span className="badge warn"><span aria-hidden="true">▲</span>Stand teilweise veraltet</span></p>}<Link href="/ressourcen">Ressourcen ansehen</Link></div>
{mine.orders.filter((o) => ['pending_approval', 'provisioning', 'failed'].includes(o.status)).length > 0 && <div className="card"><div className="stat">{mine.orders.filter((o) => ['pending_approval', 'provisioning', 'failed'].includes(o.status)).length}</div><p className="muted">Bestellungen in Bearbeitung</p><Link href="/bestellungen">Bestellungen ansehen</Link></div>}
</div>}
</>) : (<>
<div className="grid">
{customers !== null && <div className="card"><div className="stat">{customers}</div><p className="muted">Kunden</p><Link href="/admin/kunden">Kunden verwalten</Link></div>}
{sys && <div className="card"><div className="stat">{sys.jobs.scheduled ?? 0}</div><p className="muted">Wartende Aufträge</p></div>}
{sys?.backup && <div className="card"><h3>Backup</h3><p className="small"><Link href="/einstellungen/backup">Details und Einstellungen</Link></p>
{!sys.backup.configured ? <><p className="muted">Nicht eingerichtet.</p><span className="badge warn"><span aria-hidden="true">▲</span>Kein Backup</span></>
: <><p>{sys.backup.stale ? <span className="badge err"><span aria-hidden="true">✕</span>{sys.backup.ok ? 'Veraltet' : 'Fehlgeschlagen'}</span> : <span className="badge ok"><span aria-hidden="true">✓</span>Aktuell</span>}</p>
<p className="small muted">Letzter Lauf: {sys.backup.lastRunAt ? new Date(sys.backup.lastRunAt).toLocaleString('de-DE', { dateStyle: 'medium', timeStyle: 'short' }) : '–'}{sys.backup.sizeBytes ? ` · ${(sys.backup.sizeBytes / 1024).toFixed(0)} KB` : ''}<br />Ziele: {sys.backup.targets?.length ? sys.backup.targets.map((t) => `${t.ok ? '✓' : '✕'} ${t.name}`).join(', ') : '–'}<br />Wiederherstellungstest: {sys.backup.lastRestoreTestAt ? `${sys.backup.restoreOk ? '✓' : '✕'} ${new Date(sys.backup.lastRestoreTestAt).toLocaleDateString('de-DE')}` : 'noch nie'}{sys.backup.restoreStale ? ' (überfällig)' : ''}</p>
{sys.backup.error && <p className="small" style={{ color: 'var(--err)' }}>{sys.backup.error}</p>}</>}</div>}
{sys && <div className="card"><div className="stat">{failed}</div><p className="muted">Aufträge mit Fehlern / Prüfbedarf</p>{failed > 0 && <span className="badge err"><span aria-hidden="true">✕</span>Prüfen</span>}</div>}
</div>
</>)}
</>);
}