'use client' import React, { useState, useEffect, useMemo } from 'react' import { Shield, FileText, AlertTriangle, Clock, Users, GraduationCap, Megaphone, Activity, Printer, RefreshCw, ChevronDown, ChevronUp, CalendarClock, TrendingUp, BarChart3, CheckCircle2, XCircle, Info } from 'lucide-react' import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, Cell } from 'recharts' // ============================================================================= // TYPES // ============================================================================= interface ExecutiveReport { generated_at: string tenant_id: string compliance_score: number dsgvo: { processing_activities: number active_processings: number toms_implemented: number toms_planned: number toms_total: number completion_percent: number open_dsrs: number overdue_dsrs: number dsfas_completed: number retention_policies: number } vendors: { total_vendors: number active_vendors: number by_risk_level: Record pending_reviews: number expired_contracts: number } incidents: { total_incidents: number open_incidents: number critical_incidents: number notifications_pending: number avg_resolution_hours: number } whistleblower: { total_reports: number open_reports: number overdue_acknowledgments: number overdue_feedbacks: number avg_resolution_days: number } academy: { total_courses: number total_enrollments: number completion_rate: number overdue_count: number avg_completion_days: number } risk_overview: { overall_level: string module_risks: Array<{ module: string level: string score: number issues: number }> open_findings: number critical_findings: number } upcoming_deadlines: Array<{ module: string type: string description: string due_date: string days_left: number severity: string }> recent_activity: Array<{ timestamp: string module: string action: string description: string user_id?: string }> } type SortDirection = 'asc' | 'desc' // ============================================================================= // 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/reporting/executive' const MODULE_LABELS: Record = { dsgvo: 'DSGVO', vendors: 'Dienstleister', incidents: 'Vorfaelle', whistleblower: 'Hinweisgeber', academy: 'Academy', contracts: 'Vertraege', dsr: 'Betroffenenrechte' } const MODULE_ICONS: Record = { dsgvo: , vendors: , incidents: , whistleblower: , academy: , contracts: , dsr: } const SEVERITY_STYLES: Record = { INFO: { bg: 'bg-blue-100 dark:bg-blue-900/40', text: 'text-blue-700 dark:text-blue-300', border: 'border-blue-300 dark:border-blue-700' }, WARNING: { bg: 'bg-yellow-100 dark:bg-yellow-900/40', text: 'text-yellow-700 dark:text-yellow-300', border: 'border-yellow-300 dark:border-yellow-700' }, URGENT: { bg: 'bg-orange-100 dark:bg-orange-900/40', text: 'text-orange-700 dark:text-orange-300', border: 'border-orange-300 dark:border-orange-700' }, OVERDUE: { bg: 'bg-red-100 dark:bg-red-900/40', text: 'text-red-700 dark:text-red-300', border: 'border-red-300 dark:border-red-700' } } const RISK_COLORS: Record = { LOW: '#22c55e', MEDIUM: '#eab308', HIGH: '#f97316', CRITICAL: '#ef4444' } // ============================================================================= // HELPER FUNCTIONS // ============================================================================= 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 getRiskColor(score: number): string { if (score >= 75) return '#22c55e' if (score >= 50) return '#eab308' if (score >= 25) return '#f97316' return '#ef4444' } function getRiskLevel(score: number): string { if (score >= 75) return 'LOW' if (score >= 50) return 'MEDIUM' if (score >= 25) return 'HIGH' return 'CRITICAL' } function getRiskBadgeClasses(level: string): string { switch (level.toUpperCase()) { case 'LOW': return 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300' case 'MEDIUM': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300' case 'HIGH': return 'bg-orange-100 text-orange-800 dark:bg-orange-900/50 dark:text-orange-300' case 'CRITICAL': return 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300' default: return 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300' } } function getModuleBorderColor(level: string): string { switch (level.toUpperCase()) { case 'LOW': return 'border-l-green-500' case 'MEDIUM': return 'border-l-yellow-500' case 'HIGH': return 'border-l-orange-500' case 'CRITICAL': return 'border-l-red-500' default: return 'border-l-gray-400' } } function formatDate(dateString: string): string { return new Date(dateString).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) } function formatDateTime(dateString: string): string { return new Date(dateString).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' }) } function formatRelativeTime(dateString: string): string { const now = new Date() const date = new Date(dateString) const diffMs = now.getTime() - date.getTime() const diffMinutes = Math.floor(diffMs / 60000) const diffHours = Math.floor(diffMs / 3600000) const diffDays = Math.floor(diffMs / 86400000) if (diffMinutes < 1) return 'Gerade eben' if (diffMinutes < 60) return `vor ${diffMinutes} Min.` if (diffHours < 24) return `vor ${diffHours} Std.` if (diffDays < 7) return `vor ${diffDays} Tag${diffDays !== 1 ? 'en' : ''}` return formatDate(dateString) } // ============================================================================= // SKELETON / LOADING COMPONENTS // ============================================================================= function SkeletonPulse({ className = '' }: { className?: string }) { return (
) } function LoadingSkeleton() { return (
{/* Header Skeleton */}
{/* Score Skeleton */}
{/* Module Grid Skeleton */}
{Array.from({ length: 5 }).map((_, i) => (
))}
{/* Chart Skeleton */} {/* Table Skeleton */}
{Array.from({ length: 4 }).map((_, i) => ( ))}
) } // ============================================================================= // COMPLIANCE SCORE RING // ============================================================================= function ComplianceScoreRing({ score, size = 200 }: { score: number; size?: number }) { const strokeWidth = 12 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 = getRiskColor(score) const level = getRiskLevel(score) return (
{/* Background circle */} {/* Progress circle */} {/* Center text */}
{score} von 100
Risikostufe: {level}
) } // ============================================================================= // MODULE CARD // ============================================================================= function ModuleCard({ module, icon, riskLevel, children }: { module: string icon: React.ReactNode riskLevel: string children: React.ReactNode }) { const borderColor = getModuleBorderColor(riskLevel) return (
{icon}

{module}

{riskLevel}
{children}
) } function MetricRow({ label, value, highlight = false }: { label: string; value: string | number; highlight?: boolean }) { return (
{label} {value}
) } function MiniProgressBar({ percent, color = 'purple' }: { percent: number; color?: string }) { const colorClasses: Record = { green: 'bg-green-500', yellow: 'bg-yellow-500', orange: 'bg-orange-500', red: 'bg-red-500', purple: 'bg-purple-500' } const barColor = colorClasses[color] || colorClasses.purple return (
) } function VendorRiskBar({ byRiskLevel }: { byRiskLevel: Record }) { const total = Object.values(byRiskLevel).reduce((sum, v) => sum + v, 0) if (total === 0) return Keine Daten const segments = [ { key: 'low', color: 'bg-green-500', count: byRiskLevel['low'] || byRiskLevel['LOW'] || 0 }, { key: 'medium', color: 'bg-yellow-500', count: byRiskLevel['medium'] || byRiskLevel['MEDIUM'] || 0 }, { key: 'high', color: 'bg-orange-500', count: byRiskLevel['high'] || byRiskLevel['HIGH'] || 0 }, { key: 'critical', color: 'bg-red-500', count: byRiskLevel['critical'] || byRiskLevel['CRITICAL'] || 0 } ] return (
{segments.map(seg => { const pct = (seg.count / total) * 100 if (pct <= 0) return null return (
) })}
{segments.filter(s => s.count > 0).map(seg => ( {seg.count} ))}
) } // ============================================================================= // RISK HEATMAP CHART // ============================================================================= function RiskHeatmapChart({ moduleRisks }: { moduleRisks: ExecutiveReport['risk_overview']['module_risks'] }) { const chartData = moduleRisks.map(mr => ({ name: MODULE_LABELS[mr.module] || mr.module, score: mr.score, issues: mr.issues, level: mr.level, fill: RISK_COLORS[mr.level.toUpperCase()] || '#9ca3af' })) return (

Risiko-Heatmap nach Modul

{ return [`${value}/100 (${props.payload.issues} offene Punkte)`, 'Risiko-Score'] }} /> {chartData.map((entry, index) => ( ))}
{/* Legend */}
{Object.entries(RISK_COLORS).map(([level, color]) => ( {level} ))}
) } // ============================================================================= // DEADLINES TABLE // ============================================================================= function DeadlinesTable({ deadlines }: { deadlines: ExecutiveReport['upcoming_deadlines'] }) { const [sortDir, setSortDir] = useState('asc') const sorted = useMemo(() => { return [...deadlines].sort((a, b) => { const diff = a.days_left - b.days_left return sortDir === 'asc' ? diff : -diff }) }, [deadlines, sortDir]) const toggleSort = () => { setSortDir(prev => prev === 'asc' ? 'desc' : 'asc') } if (deadlines.length === 0) { return (

Keine Fristen

Es stehen derzeit keine anstehenden Fristen aus.

) } return (

Anstehende Fristen

{deadlines.length} Eintraege
{sorted.map((dl, i) => { const severity = SEVERITY_STYLES[dl.severity.toUpperCase()] || SEVERITY_STYLES.INFO return ( ) })}
Modul Beschreibung Verbleibend Dringlichkeit
{MODULE_ICONS[dl.module.toLowerCase()] || } {MODULE_LABELS[dl.module.toLowerCase()] || dl.module} {dl.description} {formatDate(dl.due_date)} {dl.days_left <= 0 ? `${Math.abs(dl.days_left)} Tag${Math.abs(dl.days_left) !== 1 ? 'e' : ''} ueberfaellig` : `${dl.days_left} Tag${dl.days_left !== 1 ? 'e' : ''}` } {dl.severity}
) } // ============================================================================= // ACTIVITY TIMELINE // ============================================================================= function ActivityTimeline({ activities }: { activities: ExecutiveReport['recent_activity'] }) { const displayedActivities = activities.slice(0, 20) if (displayedActivities.length === 0) { return (

Keine Aktivitaeten

Es wurden noch keine Aktivitaeten verzeichnet.

) } return (

Letzte Aktivitaeten

{displayedActivities.length} von {activities.length}
{/* Vertical line */}
{displayedActivities.map((act, i) => (
{/* Timeline dot */}
{MODULE_ICONS[act.module.toLowerCase()] || }
{/* Content */}
{MODULE_LABELS[act.module.toLowerCase()] || act.module} {formatRelativeTime(act.timestamp)}

{act.action}

{act.description}

))}
) } // ============================================================================= // ERROR STATE // ============================================================================= function ErrorState({ error, onRetry }: { error: string; onRetry: () => void }) { return (

Bericht konnte nicht geladen werden

{error}

) } // ============================================================================= // MAIN PAGE // ============================================================================= export default function ExecutiveReportingPage() { const [report, setReport] = useState(null) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const fetchReport = async () => { setIsLoading(true) setError(null) try { const response = await fetch(API_BASE, { headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': getTenantId(), 'X-User-ID': getUserId() } }) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } const data: ExecutiveReport = await response.json() setReport(data) } catch (err) { const message = err instanceof Error ? err.message : 'Unbekannter Fehler' console.error('Fehler beim Laden des Executive Reports:', message) setError(message) } finally { setIsLoading(false) } } useEffect(() => { fetchReport() }, []) const handlePrint = () => { window.print() } const moduleRiskMap = useMemo(() => { if (!report) return {} const map: Record = {} for (const mr of report.risk_overview.module_risks) { map[mr.module.toLowerCase()] = mr.level } return map }, [report]) return (
{/* Header */}

Compliance-Bericht fuer die Geschaeftsfuehrung

{report ? `Erstellt am ${formatDateTime(report.generated_at)}` : 'Wird geladen...' }

{/* Loading State */} {isLoading && } {/* Error State */} {!isLoading && error && } {/* Report Content */} {!isLoading && !error && report && ( <> {/* ============================================================= */} {/* SECTION 1: Overall Compliance Score */} {/* ============================================================= */}

Gesamt-Compliance-Score

{report.risk_overview.open_findings} offene Feststellungen {report.risk_overview.critical_findings} kritisch
{/* ============================================================= */} {/* SECTION 2: Module Overview Grid */} {/* ============================================================= */}

Moduluebersicht

{/* DSGVO Module */} } riskLevel={moduleRiskMap['dsgvo'] || 'MEDIUM'} > = 75 ? 'green' : report.dsgvo.completion_percent >= 50 ? 'yellow' : 'red'} /> 0} /> {/* Vendors Module */} } riskLevel={moduleRiskMap['vendors'] || 'MEDIUM'} > 0} /> 0} /> {/* Incidents Module */} } riskLevel={moduleRiskMap['incidents'] || 'MEDIUM'} > 0} /> 0} /> 0} /> {/* Whistleblower Module */} } riskLevel={moduleRiskMap['whistleblower'] || 'MEDIUM'} > 0} /> 0} /> {/* Academy Module */} } riskLevel={moduleRiskMap['academy'] || 'MEDIUM'} > = 80 ? 'green' : report.academy.completion_rate >= 50 ? 'yellow' : 'red'} /> 0} />
{/* ============================================================= */} {/* SECTION 3: Risk Heatmap */} {/* ============================================================= */}
{/* ============================================================= */} {/* SECTION 4: Upcoming Deadlines */} {/* ============================================================= */}
{/* ============================================================= */} {/* SECTION 5: Recent Activity Timeline */} {/* ============================================================= */}
{/* ============================================================= */} {/* FOOTER: Info Note */} {/* ============================================================= */}

Hinweis zum Compliance-Bericht

Dieser Bericht wird automatisch aus den Daten aller aktiven Compliance-Module generiert. Der Compliance-Score berechnet sich aus dem gewichteten Mittel der einzelnen Modulbewertungen. Fuer einen rechtlich verbindlichen Nachweis konsultieren Sie bitte Ihren Datenschutzbeauftragten. Bericht-ID: {report.tenant_id} | Stichtag: {formatDateTime(report.generated_at)}

)} {/* ================================================================= */} {/* PRINT-FRIENDLY STYLES */} {/* ================================================================= */}
) }