'use client' import React, { useState, useEffect, useCallback } from 'react' import { Building2, Plus, Users, Shield, AlertTriangle, Activity, GraduationCap, Truck, ChevronDown, ChevronUp, Pencil, Eye, RefreshCw, Loader2, Globe, CheckCircle2, XCircle, BarChart3, Search, X, Settings, } from 'lucide-react' // ============================================================================= // TYPES // ============================================================================= interface TenantOverview { id: string name: string slug: string status: string max_users: number llm_quota_monthly: number compliance_score: number risk_level: string namespace_count: number open_incidents: number open_reports: number pending_dsrs: number training_completion_rate: number vendor_risk_high: number created_at: string updated_at: string } interface TenantNamespace { id: string tenant_id: string name: string slug: string isolation_level: string data_classification: string created_at: string } interface OverviewResponse { tenants: TenantOverview[] total: number average_score: number generated_at: string } interface CreateTenantForm { name: string slug: string max_users: number llm_quota_monthly: number } interface EditTenantForm { name: string max_users: number llm_quota_monthly: number status: string } interface CreateNamespaceForm { name: string slug: string isolation_level: string data_classification: string } type SortField = 'name' | 'score' | 'risk' type StatusFilter = 'all' | 'active' | 'suspended' | 'inactive' // ============================================================================= // CONSTANTS // ============================================================================= const FALLBACK_TENANT_ID = '00000000-0000-0000-0000-000000000001' const FALLBACK_USER_ID = '00000000-0000-0000-0000-000000000001' const API_BASE = '/api/sdk/v1/multi-tenant' const STATUS_OPTIONS = [ { value: 'active', label: 'Aktiv' }, { value: 'suspended', label: 'Suspendiert' }, { value: 'inactive', label: 'Inaktiv' }, ] const FILTER_OPTIONS: { value: StatusFilter; label: string }[] = [ { value: 'all', label: 'Alle' }, { value: 'active', label: 'Aktiv' }, { value: 'suspended', label: 'Suspendiert' }, { value: 'inactive', label: 'Inaktiv' }, ] const SORT_OPTIONS: { value: SortField; label: string }[] = [ { value: 'name', label: 'Name' }, { value: 'score', label: 'Score' }, { value: 'risk', label: 'Risiko' }, ] const ISOLATION_LEVELS = [ { value: 'shared', label: 'Shared' }, { value: 'isolated', label: 'Isoliert' }, { value: 'dedicated', label: 'Dediziert' }, ] const DATA_CLASSIFICATIONS = [ { value: 'public', label: 'Oeffentlich' }, { value: 'internal', label: 'Intern' }, { value: 'confidential', label: 'Vertraulich' }, { value: 'restricted', label: 'Eingeschraenkt' }, ] const RISK_ORDER: Record = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3, } const EMPTY_CREATE_FORM: CreateTenantForm = { name: '', slug: '', max_users: 100, llm_quota_monthly: 10000, } const EMPTY_NAMESPACE_FORM: CreateNamespaceForm = { name: '', slug: '', isolation_level: 'shared', data_classification: 'internal', } // ============================================================================= // HELPERS // ============================================================================= function getTenantId(): string { if (typeof window !== 'undefined') { return localStorage.getItem('sdk-tenant-id') || FALLBACK_TENANT_ID } return FALLBACK_TENANT_ID } function getUserId(): string { if (typeof window !== 'undefined') { return localStorage.getItem('sdk-user-id') || FALLBACK_USER_ID } return FALLBACK_USER_ID } function formatDate(dateStr: string | null): string { if (!dateStr) return '-' return new Date(dateStr).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }) } function formatDateTime(dateStr: string | null): string { if (!dateStr) return '-' return new Date(dateStr).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }) } function slugify(text: string): string { return text .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') } function getScoreColor(score: number): string { if (score >= 80) return '#22c55e' if (score >= 60) return '#eab308' if (score >= 40) return '#f97316' return '#ef4444' } function getScoreBgClasses(score: number): string { if (score >= 80) return 'bg-green-50 text-green-700 border-green-200' if (score >= 60) return 'bg-amber-50 text-amber-700 border-amber-200' if (score >= 40) return 'bg-orange-50 text-orange-700 border-orange-200' return 'bg-red-50 text-red-700 border-red-200' } function getRiskBadgeClasses(level: string): string { switch (level.toUpperCase()) { case 'LOW': return 'bg-green-100 text-green-800' case 'MEDIUM': return 'bg-yellow-100 text-yellow-800' case 'HIGH': return 'bg-orange-100 text-orange-800' case 'CRITICAL': return 'bg-red-100 text-red-800' default: return 'bg-gray-100 text-gray-800' } } function getStatusBadge(status: string): { bg: string; text: string; label: string; icon: React.ReactNode } { switch (status) { case 'active': return { bg: 'bg-green-100', text: 'text-green-700', label: 'Aktiv', icon: , } case 'suspended': return { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Suspendiert', icon: , } case 'inactive': return { bg: 'bg-red-100', text: 'text-red-700', label: 'Inaktiv', icon: , } default: return { bg: 'bg-gray-100', text: 'text-gray-700', label: status, icon: null, } } } async function apiFetch(endpoint: string, options: RequestInit = {}): Promise { const res = await fetch(`${API_BASE}${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': getTenantId(), 'X-User-ID': getUserId(), ...options.headers, }, }) if (!res.ok) { const body = await res.json().catch(() => ({})) throw new Error(body.error || body.message || `HTTP ${res.status}`) } return res.json() } // ============================================================================= // SUB-COMPONENTS // ============================================================================= function LoadingSkeleton() { return (
{/* Header Skeleton */}
{/* Stats Skeleton */}
{Array.from({ length: 4 }).map((_, i) => (
))}
{/* Cards Skeleton */}
{Array.from({ length: 6 }).map((_, i) => (
))}
) } function EmptyState({ icon, title, description, action, }: { icon: React.ReactNode title: string description: string action?: React.ReactNode }) { return (
{icon}

{title}

{description}

{action &&
{action}
}
) } function StatCard({ label, value, icon, color, }: { label: string value: string | number icon: React.ReactNode color: 'indigo' | 'green' | 'blue' | 'red' }) { const colorMap = { indigo: { iconBg: 'bg-indigo-100', iconText: 'text-indigo-600', valueBg: '', }, green: { iconBg: 'bg-green-100', iconText: 'text-green-600', valueBg: '', }, blue: { iconBg: 'bg-blue-100', iconText: 'text-blue-600', valueBg: '', }, red: { iconBg: 'bg-red-100', iconText: 'text-red-600', valueBg: '', }, } const c = colorMap[color] return (
{icon}
{label}
{value}
) } function ComplianceRing({ score, size = 64 }: { score: number; size?: number }) { const strokeWidth = 5 const radius = (size - strokeWidth) / 2 const circumference = 2 * Math.PI * radius const progress = Math.max(0, Math.min(100, score)) const offset = circumference - (progress / 100) * circumference const color = getScoreColor(score) return (
{score}
) } function Modal({ open, onClose, title, children, maxWidth = 'max-w-lg', }: { open: boolean onClose: () => void title: string children: React.ReactNode maxWidth?: string }) { if (!open) return null return (

{title}

{children}
) } // ============================================================================= // TENANT CARD // ============================================================================= function TenantCard({ tenant, onEdit, onViewDetails, onSwitchTenant, }: { tenant: TenantOverview onEdit: (t: TenantOverview) => void onViewDetails: (t: TenantOverview) => void onSwitchTenant: (t: TenantOverview) => void }) { const [expanded, setExpanded] = useState(false) const statusInfo = getStatusBadge(tenant.status) return (
{/* Card Header */}

{tenant.name}

{statusInfo.icon} {statusInfo.label}

{tenant.slug}

{/* Risk Level */}
{tenant.risk_level} {tenant.namespace_count} Namespace{tenant.namespace_count !== 1 ? 's' : ''} {formatDate(tenant.created_at)}
{/* Quick Metrics */}
{tenant.open_incidents} Vorfaelle
{tenant.open_reports} Meldungen
{tenant.pending_dsrs} DSRs
{tenant.training_completion_rate}% Training
{/* Vendor Risk Info */} {tenant.vendor_risk_high > 0 && (
{tenant.vendor_risk_high} Dienstleister mit hohem Risiko
)} {/* Actions */}
{/* Expanded Section */} {expanded && (
Max. Benutzer

{tenant.max_users.toLocaleString('de-DE')}

LLM Kontingent / Monat

{tenant.llm_quota_monthly.toLocaleString('de-DE')}

Compliance Score

{tenant.compliance_score} / 100

Aktualisiert

{formatDateTime(tenant.updated_at)}

)}
) } // ============================================================================= // CREATE TENANT MODAL // ============================================================================= function CreateTenantModal({ open, onClose, onCreated, }: { open: boolean onClose: () => void onCreated: () => void }) { const [form, setForm] = useState({ ...EMPTY_CREATE_FORM }) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) const [slugManual, setSlugManual] = useState(false) const handleNameChange = (name: string) => { setForm((prev) => ({ ...prev, name, slug: slugManual ? prev.slug : slugify(name), })) } const handleSlugChange = (slug: string) => { setSlugManual(true) setForm((prev) => ({ ...prev, slug: slugify(slug) })) } const isValid = form.name.trim().length >= 2 && form.slug.trim().length >= 2 const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!isValid) return setSaving(true) setError(null) try { await apiFetch('/tenants', { method: 'POST', body: JSON.stringify({ name: form.name.trim(), slug: form.slug.trim(), max_users: form.max_users, llm_quota_monthly: form.llm_quota_monthly, }), }) setForm({ ...EMPTY_CREATE_FORM }) setSlugManual(false) onCreated() onClose() } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Erstellen') } finally { setSaving(false) } } const handleClose = () => { setForm({ ...EMPTY_CREATE_FORM }) setSlugManual(false) setError(null) onClose() } return (
{error && (
{error}
)}
handleNameChange(e.target.value)} placeholder="z.B. Musterfirma GmbH" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" autoFocus />
handleSlugChange(e.target.value)} placeholder="musterfirma-gmbh" className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" />

Kleinbuchstaben, keine Leerzeichen. Wird automatisch aus dem Namen generiert.

setForm((prev) => ({ ...prev, max_users: parseInt(e.target.value) || 0 }))} min={1} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" />
setForm((prev) => ({ ...prev, llm_quota_monthly: parseInt(e.target.value) || 0 }))} min={0} step={1000} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" />
) } // ============================================================================= // EDIT TENANT MODAL // ============================================================================= function EditTenantModal({ open, onClose, tenant, onUpdated, }: { open: boolean onClose: () => void tenant: TenantOverview | null onUpdated: () => void }) { const [form, setForm] = useState({ name: '', max_users: 100, llm_quota_monthly: 10000, status: 'active', }) const [saving, setSaving] = useState(false) const [error, setError] = useState(null) useEffect(() => { if (tenant) { setForm({ name: tenant.name, max_users: tenant.max_users, llm_quota_monthly: tenant.llm_quota_monthly, status: tenant.status, }) setError(null) } }, [tenant]) const isValid = form.name.trim().length >= 2 const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() if (!isValid || !tenant) return setSaving(true) setError(null) try { await apiFetch(`/tenants/${tenant.id}`, { method: 'PUT', body: JSON.stringify({ name: form.name.trim(), max_users: form.max_users, llm_quota_monthly: form.llm_quota_monthly, status: form.status, }), }) onUpdated() onClose() } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Aktualisieren') } finally { setSaving(false) } } return (
{error && (
{error}
)}
setForm((prev) => ({ ...prev, name: e.target.value }))} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" />
setForm((prev) => ({ ...prev, max_users: parseInt(e.target.value) || 0 }))} min={1} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" />
setForm((prev) => ({ ...prev, llm_quota_monthly: parseInt(e.target.value) || 0 }))} min={0} step={1000} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" />
) } // ============================================================================= // TENANT DETAIL MODAL // ============================================================================= function TenantDetailModal({ open, onClose, tenant, onSwitchTenant, }: { open: boolean onClose: () => void tenant: TenantOverview | null onSwitchTenant: (t: TenantOverview) => void }) { const [namespaces, setNamespaces] = useState([]) const [namespacesLoading, setNamespacesLoading] = useState(false) const [namespacesError, setNamespacesError] = useState(null) const [showCreateNs, setShowCreateNs] = useState(false) const [nsForm, setNsForm] = useState({ ...EMPTY_NAMESPACE_FORM }) const [nsCreating, setNsCreating] = useState(false) const [nsError, setNsError] = useState(null) const loadNamespaces = useCallback(async () => { if (!tenant) return setNamespacesLoading(true) setNamespacesError(null) try { const data = await apiFetch<{ namespaces: TenantNamespace[]; total: number }>( `/tenants/${tenant.id}/namespaces` ) setNamespaces(data.namespaces || []) } catch (err) { setNamespacesError(err instanceof Error ? err.message : 'Fehler beim Laden') } finally { setNamespacesLoading(false) } }, [tenant]) useEffect(() => { if (open && tenant) { loadNamespaces() setShowCreateNs(false) setNsForm({ ...EMPTY_NAMESPACE_FORM }) setNsError(null) } }, [open, tenant, loadNamespaces]) const handleNsNameChange = (name: string) => { setNsForm((prev) => ({ ...prev, name, slug: slugify(name), })) } const handleCreateNamespace = async (e: React.FormEvent) => { e.preventDefault() if (!tenant || nsForm.name.trim().length < 2) return setNsCreating(true) setNsError(null) try { await apiFetch(`/tenants/${tenant.id}/namespaces`, { method: 'POST', body: JSON.stringify({ name: nsForm.name.trim(), slug: nsForm.slug.trim(), isolation_level: nsForm.isolation_level, data_classification: nsForm.data_classification, }), }) setNsForm({ ...EMPTY_NAMESPACE_FORM }) setShowCreateNs(false) loadNamespaces() } catch (err) { setNsError(err instanceof Error ? err.message : 'Fehler beim Erstellen') } finally { setNsCreating(false) } } if (!tenant) return null const statusInfo = getStatusBadge(tenant.status) const scoreColor = getScoreColor(tenant.compliance_score) return (
{/* Overview Section */}

{tenant.name}

{statusInfo.icon} {statusInfo.label}

{tenant.slug}

{tenant.risk_level} Erstellt: {formatDate(tenant.created_at)}
{/* Compliance Breakdown */}

Compliance-Uebersicht

Offene Vorfaelle

{tenant.open_incidents}

Hinweisgebermeldungen

{tenant.open_reports}

Ausstehende DSRs

{tenant.pending_dsrs}

Schulungsquote

{tenant.training_completion_rate}%

Hochrisiko-Dienstleister

{tenant.vendor_risk_high}

Compliance-Score

{tenant.compliance_score}/100

{/* Settings */}

Mandanten-Einstellungen

Max. Benutzer

{tenant.max_users.toLocaleString('de-DE')}

LLM Kontingent / Monat

{tenant.llm_quota_monthly.toLocaleString('de-DE')}

{/* Namespaces */}

Namespaces ({namespaces.length})

{/* Create Namespace Form */} {showCreateNs && (
{nsError && (
{nsError}
)}
handleNsNameChange(e.target.value)} placeholder="z.B. Produktion" className="w-full px-2.5 py-1.5 border border-slate-300 rounded-lg text-xs focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" autoFocus />
)} {/* Namespaces List */} {namespacesLoading ? (
{Array.from({ length: 3 }).map((_, i) => (
))}
) : namespacesError ? (
{namespacesError}
) : namespaces.length === 0 ? (
Noch keine Namespaces vorhanden.
) : (
{namespaces.map((ns) => (

{ns.name}

{ns.slug}

{ns.isolation_level} {ns.data_classification}
))}
)}
{/* Switch Tenant Action */}
) } // ============================================================================= // MAIN PAGE COMPONENT // ============================================================================= export default function MultiTenantPage() { // Data state const [tenants, setTenants] = useState([]) const [totalTenants, setTotalTenants] = useState(0) const [averageScore, setAverageScore] = useState(0) const [generatedAt, setGeneratedAt] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // UI state const [searchQuery, setSearchQuery] = useState('') const [statusFilter, setStatusFilter] = useState('all') const [sortField, setSortField] = useState('name') const [refreshing, setRefreshing] = useState(false) // Modal state const [showCreate, setShowCreate] = useState(false) const [editTenant, setEditTenant] = useState(null) const [detailTenant, setDetailTenant] = useState(null) // Switch tenant notification const [switchNotification, setSwitchNotification] = useState(null) // --------------------------------------------------------------------------- // DATA LOADING // --------------------------------------------------------------------------- const loadOverview = useCallback(async (showRefresh = false) => { if (showRefresh) setRefreshing(true) else setLoading(true) setError(null) try { const data = await apiFetch('/overview') setTenants(data.tenants || []) setTotalTenants(data.total || 0) setAverageScore(data.average_score || 0) setGeneratedAt(data.generated_at || null) } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Laden der Mandanten') } finally { setLoading(false) setRefreshing(false) } }, []) useEffect(() => { loadOverview() }, [loadOverview]) // --------------------------------------------------------------------------- // FILTERING, SEARCHING, SORTING // --------------------------------------------------------------------------- const filteredTenants = React.useMemo(() => { let result = [...tenants] // Filter by status if (statusFilter !== 'all') { result = result.filter((t) => t.status === statusFilter) } // Filter by search query if (searchQuery.trim()) { const q = searchQuery.toLowerCase().trim() result = result.filter( (t) => t.name.toLowerCase().includes(q) || t.slug.toLowerCase().includes(q) ) } // Sort result.sort((a, b) => { switch (sortField) { case 'name': return a.name.localeCompare(b.name, 'de-DE') case 'score': return b.compliance_score - a.compliance_score case 'risk': return (RISK_ORDER[b.risk_level] || 0) - (RISK_ORDER[a.risk_level] || 0) default: return 0 } }) return result }, [tenants, statusFilter, searchQuery, sortField]) // --------------------------------------------------------------------------- // DERIVED STATS // --------------------------------------------------------------------------- const activeTenants = React.useMemo( () => tenants.filter((t) => t.status === 'active').length, [tenants] ) const criticalRisks = React.useMemo( () => tenants.filter((t) => t.risk_level === 'HIGH' || t.risk_level === 'CRITICAL').length, [tenants] ) // --------------------------------------------------------------------------- // HANDLERS // --------------------------------------------------------------------------- const handleRefresh = () => loadOverview(true) const handleSwitchTenant = async (tenant: TenantOverview) => { try { await apiFetch<{ tenant: { id: string; name: string } }>('/switch', { method: 'POST', body: JSON.stringify({ tenant_id: tenant.id }), }) if (typeof window !== 'undefined') { localStorage.setItem('sdk-tenant-id', tenant.id) } setSwitchNotification(`Mandant gewechselt zu: ${tenant.name}`) setTimeout(() => setSwitchNotification(null), 4000) } catch (err) { // Fallback: just set in localStorage even if API call fails if (typeof window !== 'undefined') { localStorage.setItem('sdk-tenant-id', tenant.id) } setSwitchNotification(`Mandant gewechselt zu: ${tenant.name}`) setTimeout(() => setSwitchNotification(null), 4000) } } const handleCreated = () => { loadOverview(true) } const handleUpdated = () => { loadOverview(true) } // --------------------------------------------------------------------------- // RENDER: LOADING STATE // --------------------------------------------------------------------------- if (loading) { return (
) } // --------------------------------------------------------------------------- // RENDER: ERROR STATE // --------------------------------------------------------------------------- if (error && tenants.length === 0) { return (

Fehler beim Laden

{error}

) } // --------------------------------------------------------------------------- // RENDER: MAIN PAGE // --------------------------------------------------------------------------- return (
{/* Switch Notification */} {switchNotification && (
{switchNotification}
)} {/* ================================================================= */} {/* HEADER */} {/* ================================================================= */}

Multi-Tenant Verwaltung

Mandanten verwalten und Compliance-Status ueberwachen

{/* Generated At */} {generatedAt && (

Zuletzt aktualisiert: {formatDateTime(generatedAt)}

)} {/* ================================================================= */} {/* STATS OVERVIEW */} {/* ================================================================= */}
} color="indigo" /> } color="green" /> } color="blue" /> } color="red" />
{/* ================================================================= */} {/* SEARCH & FILTER BAR */} {/* ================================================================= */}
{/* Search Input */}
setSearchQuery(e.target.value)} placeholder="Mandant suchen (Name oder Slug)..." className="w-full pl-9 pr-9 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500" /> {searchQuery && ( )}
{/* Status Filter */}
{FILTER_OPTIONS.map((opt) => ( ))}
{/* Sort */}
Sortierung:
{/* ================================================================= */} {/* REFRESH ERROR BANNER */} {/* ================================================================= */} {error && tenants.length > 0 && (
Fehler beim Aktualisieren: {error}
)} {/* ================================================================= */} {/* TENANT CARDS GRID */} {/* ================================================================= */} {filteredTenants.length === 0 ? ( tenants.length === 0 ? ( } title="Keine Mandanten vorhanden" description="Erstellen Sie Ihren ersten Mandanten, um die Multi-Tenant-Verwaltung zu nutzen." action={ } /> ) : ( } title="Keine Ergebnisse" description={`Kein Mandant gefunden fuer "${searchQuery}"${statusFilter !== 'all' ? ` mit Status "${FILTER_OPTIONS.find((f) => f.value === statusFilter)?.label}"` : ''}.`} action={ } /> ) ) : ( <>

{filteredTenants.length} von {tenants.length} Mandant{tenants.length !== 1 ? 'en' : ''}

{filteredTenants.map((tenant) => ( ))}
)} {/* ================================================================= */} {/* MODALS */} {/* ================================================================= */} setShowCreate(false)} onCreated={handleCreated} /> setEditTenant(null)} tenant={editTenant} onUpdated={handleUpdated} /> setDetailTenant(null)} tenant={detailTenant} onSwitchTenant={handleSwitchTenant} />
) }