Stand vor Einführung des Nacht-Agenten
This commit is contained in:
commit
4763548bfb
168 changed files with 12726 additions and 0 deletions
50
apps/web/src/app/(app)/dashboard/page.tsx
Normal file
50
apps/web/src/app/(app)/dashboard/page.tsx
Normal 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>
|
||||
</>)}
|
||||
</>);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue