2026-09-27 00:51:32 +02:00
'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 ) ;
Lizenzverwaltung: Übersicht, Vergabe, Aktivierungen, Lebenszyklus und Kundensicht
Neuer Menüpunkt "Lizenzen" (Personal) bzw. "Meine Lizenzen" (Kunden):
- Übersicht mit Kennzahlen, Filtern (aktiv, läuft in 30 Tagen ab, gesperrt,
abgelaufen, ohne Kunde) und Suche; Dashboard-Kachel
- Vergabe mit Vertrag (normaler Bestellweg) oder ohne Berechnung direkt im
Lizenzsystem: Lizenz, Test (Trial) oder Add-on zu einer Basislizenz
- Detailseite: Schlüssel, Geräte (Aktivierungen) freigeben, Sperren/Entsperren,
Verlängern, Widerrufen, Limits, Funktionsumfang (Entitlement), Produktwechsel
bzw. Testumwandlung, Add-ons, Verlauf
- Kunden-Selbstbedienung: Inhaber/Admin geben eigene Geräte frei
Verwaltungs-Client für das Lizenzsystem in @kc/connector-licensing (createLicensingAdmin),
Fehlermeldungen des Lizenzsystems werden verständlich weitergereicht. Alle Änderungen
im Audit-Protokoll; der lokale Stand wird danach sofort aktualisiert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:53:20 +02:00
const [ licenses , setLicenses ] = useState < { active : number ; expiring : number ; unassigned : number } | null > ( null ) ;
2026-09-27 00:51:32 +02:00
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 ) ) ) ;
Lizenzverwaltung: Übersicht, Vergabe, Aktivierungen, Lebenszyklus und Kundensicht
Neuer Menüpunkt "Lizenzen" (Personal) bzw. "Meine Lizenzen" (Kunden):
- Übersicht mit Kennzahlen, Filtern (aktiv, läuft in 30 Tagen ab, gesperrt,
abgelaufen, ohne Kunde) und Suche; Dashboard-Kachel
- Vergabe mit Vertrag (normaler Bestellweg) oder ohne Berechnung direkt im
Lizenzsystem: Lizenz, Test (Trial) oder Add-on zu einer Basislizenz
- Detailseite: Schlüssel, Geräte (Aktivierungen) freigeben, Sperren/Entsperren,
Verlängern, Widerrufen, Limits, Funktionsumfang (Entitlement), Produktwechsel
bzw. Testumwandlung, Add-ons, Verlauf
- Kunden-Selbstbedienung: Inhaber/Admin geben eigene Geräte frei
Verwaltungs-Client für das Lizenzsystem in @kc/connector-licensing (createLicensingAdmin),
Fehlermeldungen des Lizenzsystems werden verständlich weitergereicht. Alle Änderungen
im Audit-Protokoll; der lokale Stand wird danach sofort aktualisiert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:53:20 +02:00
if ( can ( 'licenses.read' ) ) api < { active : number ; expiring : number ; unassigned : number } > ( 'GET' , '/admin/licenses/summary' ) . then ( setLicenses ) . catch ( ( ) = > undefined ) ;
2026-09-27 00:51:32 +02:00
} , [ 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 > }
Lizenzverwaltung: Übersicht, Vergabe, Aktivierungen, Lebenszyklus und Kundensicht
Neuer Menüpunkt "Lizenzen" (Personal) bzw. "Meine Lizenzen" (Kunden):
- Übersicht mit Kennzahlen, Filtern (aktiv, läuft in 30 Tagen ab, gesperrt,
abgelaufen, ohne Kunde) und Suche; Dashboard-Kachel
- Vergabe mit Vertrag (normaler Bestellweg) oder ohne Berechnung direkt im
Lizenzsystem: Lizenz, Test (Trial) oder Add-on zu einer Basislizenz
- Detailseite: Schlüssel, Geräte (Aktivierungen) freigeben, Sperren/Entsperren,
Verlängern, Widerrufen, Limits, Funktionsumfang (Entitlement), Produktwechsel
bzw. Testumwandlung, Add-ons, Verlauf
- Kunden-Selbstbedienung: Inhaber/Admin geben eigene Geräte frei
Verwaltungs-Client für das Lizenzsystem in @kc/connector-licensing (createLicensingAdmin),
Fehlermeldungen des Lizenzsystems werden verständlich weitergereicht. Alle Änderungen
im Audit-Protokoll; der lokale Stand wird danach sofort aktualisiert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 12:53:20 +02:00
{ licenses && < div className = "card" > < div className = "stat" > { licenses . active } < / div > < p className = "muted" > Aktive Lizenzen < / p > { licenses . expiring > 0 && < p className = "small" > < span className = "badge warn" > < span aria-hidden = "true" > ▲ < / span > { licenses . expiring } laufen in 30 Tagen ab < / span > < / p > } { licenses . unassigned > 0 && < p className = "small" > < span className = "badge warn" > < span aria-hidden = "true" > ▲ < / span > { licenses . unassigned } ohne Kunde < / span > < / p > } < Link href = "/lizenzen" > Lizenzen verwalten < / Link > < / div > }
2026-09-27 00:51:32 +02:00
{ 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 >
< / > ) }
< / > ) ;
}