All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 19s
Kaputtes (admin) Layout geloescht (Role-Selection, 404-Sidebar, localhost-Dashboard). SDK-Flow nach /sdk/sdk-flow verschoben. Route-Gruppe (sdk) aufgeloest. Root-Seite redirected auf /sdk. ~25 ungenutzte Dateien/Verzeichnisse entfernt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
514 lines
22 KiB
TypeScript
514 lines
22 KiB
TypeScript
'use client'
|
|
|
|
import React, { useState, useEffect } from 'react'
|
|
import { useSDK } from '@/lib/sdk'
|
|
import { StepHeader } from '@/components/sdk/StepHeader'
|
|
import {
|
|
ExecutiveReport,
|
|
RiskLevel,
|
|
DeadlineSeverity,
|
|
RISK_LEVEL_INFO,
|
|
DEADLINE_SEVERITY_INFO,
|
|
getScoreColor,
|
|
getScoreBgColor,
|
|
} from '@/lib/sdk/reporting/types'
|
|
import { getExecutiveReport } from '@/lib/sdk/reporting/api'
|
|
|
|
// =============================================================================
|
|
// TYPES
|
|
// =============================================================================
|
|
|
|
type TabId = 'overview' | 'risks' | 'deadlines' | 'modules' | 'activity'
|
|
|
|
interface Tab {
|
|
id: TabId
|
|
label: string
|
|
}
|
|
|
|
// =============================================================================
|
|
// HELPER COMPONENTS
|
|
// =============================================================================
|
|
|
|
function TabNavigation({
|
|
tabs,
|
|
activeTab,
|
|
onTabChange,
|
|
}: {
|
|
tabs: Tab[]
|
|
activeTab: TabId
|
|
onTabChange: (tab: TabId) => void
|
|
}) {
|
|
return (
|
|
<div className="border-b border-gray-200">
|
|
<nav className="flex gap-1 -mb-px" aria-label="Tabs">
|
|
{tabs.map(tab => (
|
|
<button
|
|
key={tab.id}
|
|
onClick={() => onTabChange(tab.id)}
|
|
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
|
|
activeTab === tab.id
|
|
? 'border-purple-600 text-purple-600'
|
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
|
}`}
|
|
>
|
|
{tab.label}
|
|
</button>
|
|
))}
|
|
</nav>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ScoreCircle({ score }: { score: number }) {
|
|
const circumference = 2 * Math.PI * 60
|
|
const strokeDashoffset = circumference - (score / 100) * circumference
|
|
|
|
return (
|
|
<div className="relative w-36 h-36">
|
|
<svg className="w-full h-full -rotate-90" viewBox="0 0 128 128">
|
|
<circle
|
|
cx="64" cy="64" r="60"
|
|
stroke="#e5e7eb" strokeWidth="8" fill="none"
|
|
/>
|
|
<circle
|
|
cx="64" cy="64" r="60"
|
|
stroke={score >= 80 ? '#22c55e' : score >= 60 ? '#eab308' : score >= 40 ? '#f97316' : '#ef4444'}
|
|
strokeWidth="8" fill="none"
|
|
strokeLinecap="round"
|
|
strokeDasharray={circumference}
|
|
strokeDashoffset={strokeDashoffset}
|
|
className="transition-all duration-1000"
|
|
/>
|
|
</svg>
|
|
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
|
<span className={`text-3xl font-bold ${getScoreColor(score)}`}>{score}%</span>
|
|
<span className="text-xs text-gray-500">Compliance</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function StatCard({
|
|
label,
|
|
value,
|
|
color = 'gray',
|
|
subtitle,
|
|
}: {
|
|
label: string
|
|
value: number | string
|
|
color?: 'gray' | 'blue' | 'yellow' | 'red' | 'green' | 'purple' | 'orange'
|
|
subtitle?: string
|
|
}) {
|
|
const colorClasses: Record<string, string> = {
|
|
gray: 'border-gray-200 text-gray-900',
|
|
blue: 'border-blue-200 text-blue-600',
|
|
yellow: 'border-yellow-200 text-yellow-600',
|
|
red: 'border-red-200 text-red-600',
|
|
green: 'border-green-200 text-green-600',
|
|
purple: 'border-purple-200 text-purple-600',
|
|
orange: 'border-orange-200 text-orange-600',
|
|
}
|
|
|
|
return (
|
|
<div className={`bg-white rounded-xl border-2 ${colorClasses[color]} p-4`}>
|
|
<div className="text-sm text-gray-500 mb-1">{label}</div>
|
|
<div className={`text-2xl font-bold ${colorClasses[color]?.split(' ')[1] || ''}`}>{value}</div>
|
|
{subtitle && <div className="text-xs text-gray-400 mt-1">{subtitle}</div>}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function RiskBadge({ level }: { level: RiskLevel }) {
|
|
const info = RISK_LEVEL_INFO[level]
|
|
return (
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${info.bgColor} ${info.color}`}>
|
|
{info.label}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function DeadlineBadge({ severity }: { severity: DeadlineSeverity }) {
|
|
const info = DEADLINE_SEVERITY_INFO[severity]
|
|
return (
|
|
<span className={`px-2 py-1 rounded-full text-xs font-medium ${info.bgColor} ${info.color}`}>
|
|
{info.label}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// TAB CONTENTS
|
|
// =============================================================================
|
|
|
|
function OverviewTab({ report }: { report: ExecutiveReport }) {
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Score + Key Metrics */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
<div className="lg:col-span-1 bg-white rounded-xl border border-gray-200 p-6 flex flex-col items-center justify-center">
|
|
<ScoreCircle score={report.complianceScore} />
|
|
<div className="mt-3 flex items-center gap-2">
|
|
<span className="text-sm text-gray-500">Risikolevel:</span>
|
|
<RiskBadge level={report.riskOverview.overallLevel} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="lg:col-span-3 grid grid-cols-2 md:grid-cols-3 gap-4">
|
|
<StatCard label="Verarbeitungstaetigkeiten" value={report.dsgvo.processingActivities} color="blue" subtitle={`${report.dsgvo.activeProcessings} aktiv`} />
|
|
<StatCard label="TOMs umgesetzt" value={`${report.dsgvo.tomsImplemented}/${report.dsgvo.tomsTotal}`} color={report.dsgvo.completionPercent >= 80 ? 'green' : 'yellow'} subtitle={`${report.dsgvo.completionPercent}% vollstaendig`} />
|
|
<StatCard label="Offene Betroffenenanfragen" value={report.dsgvo.openDSRs} color={report.dsgvo.overdueDSRs > 0 ? 'red' : 'green'} subtitle={report.dsgvo.overdueDSRs > 0 ? `${report.dsgvo.overdueDSRs} ueberfaellig` : 'Keine ueberfaelligen'} />
|
|
<StatCard label="Offene Vorfaelle" value={report.incidents.openIncidents} color={report.incidents.criticalIncidents > 0 ? 'red' : 'green'} subtitle={report.incidents.criticalIncidents > 0 ? `${report.incidents.criticalIncidents} kritisch` : 'Keine kritischen'} />
|
|
<StatCard label="Schulungsquote" value={`${Math.round(report.academy.completionRate)}%`} color={report.academy.completionRate >= 80 ? 'green' : 'yellow'} subtitle={`${report.academy.overdueCount} ueberfaellig`} />
|
|
<StatCard label="Lieferanten" value={report.vendors.totalVendors} color="purple" subtitle={`${report.vendors.pendingReviews} ausstehende Pruefungen`} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Deadlines Summary */}
|
|
{report.upcomingDeadlines.length > 0 && (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Naechste Fristen</h3>
|
|
<div className="space-y-3">
|
|
{report.upcomingDeadlines.slice(0, 5).map((dl, i) => (
|
|
<div key={i} className="flex items-center justify-between py-2 border-b border-gray-100 last:border-0">
|
|
<div className="flex items-center gap-3">
|
|
<DeadlineBadge severity={dl.severity} />
|
|
<div>
|
|
<div className="text-sm font-medium text-gray-900">{dl.description}</div>
|
|
<div className="text-xs text-gray-500">{dl.module} · {dl.type}</div>
|
|
</div>
|
|
</div>
|
|
<div className={`text-sm font-medium ${dl.daysLeft <= 0 ? 'text-red-600' : dl.daysLeft <= 7 ? 'text-orange-600' : 'text-gray-600'}`}>
|
|
{dl.daysLeft <= 0 ? `${Math.abs(dl.daysLeft)} Tage ueberfaellig` : `${dl.daysLeft} Tage`}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function RisksTab({ report }: { report: ExecutiveReport }) {
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Overall Risk */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">Gesamt-Risikobewertung</h3>
|
|
<RiskBadge level={report.riskOverview.overallLevel} />
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<StatCard label="Offene Findings" value={report.riskOverview.openFindings} color={report.riskOverview.openFindings > 5 ? 'orange' : 'gray'} />
|
|
<StatCard label="Kritische Findings" value={report.riskOverview.criticalFindings} color={report.riskOverview.criticalFindings > 0 ? 'red' : 'green'} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Per-Module Risks */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Risiken nach Modul</h3>
|
|
<div className="space-y-3">
|
|
{report.riskOverview.moduleRisks.map((mr, i) => (
|
|
<div key={i} className="flex items-center justify-between py-3 border-b border-gray-100 last:border-0">
|
|
<div className="flex items-center gap-3">
|
|
<span className="text-sm font-medium text-gray-900 w-32">{mr.module}</span>
|
|
<RiskBadge level={mr.level} />
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-32 h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full rounded-full ${getScoreBgColor(mr.score)}`}
|
|
style={{ width: `${mr.score}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-sm text-gray-500 w-12 text-right">{mr.score}%</span>
|
|
<span className="text-xs text-gray-400">{mr.issues} Issues</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function DeadlinesTab({ report }: { report: ExecutiveReport }) {
|
|
return (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Alle Fristen ({report.upcomingDeadlines.length})</h3>
|
|
{report.upcomingDeadlines.length === 0 ? (
|
|
<p className="text-gray-500 text-sm">Keine bevorstehenden Fristen.</p>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-sm">
|
|
<thead>
|
|
<tr className="border-b border-gray-200">
|
|
<th className="text-left py-2 font-medium text-gray-500">Status</th>
|
|
<th className="text-left py-2 font-medium text-gray-500">Modul</th>
|
|
<th className="text-left py-2 font-medium text-gray-500">Typ</th>
|
|
<th className="text-left py-2 font-medium text-gray-500">Beschreibung</th>
|
|
<th className="text-left py-2 font-medium text-gray-500">Faellig am</th>
|
|
<th className="text-right py-2 font-medium text-gray-500">Verbleibend</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{report.upcomingDeadlines.map((dl, i) => (
|
|
<tr key={i} className="border-b border-gray-50 hover:bg-gray-50">
|
|
<td className="py-2"><DeadlineBadge severity={dl.severity} /></td>
|
|
<td className="py-2 text-gray-700">{dl.module}</td>
|
|
<td className="py-2 text-gray-500">{dl.type}</td>
|
|
<td className="py-2 text-gray-900">{dl.description}</td>
|
|
<td className="py-2 text-gray-500">{new Date(dl.dueDate).toLocaleDateString('de-DE')}</td>
|
|
<td className={`py-2 text-right font-medium ${dl.daysLeft <= 0 ? 'text-red-600' : dl.daysLeft <= 7 ? 'text-orange-600' : 'text-gray-600'}`}>
|
|
{dl.daysLeft <= 0 ? `${Math.abs(dl.daysLeft)}d ueberfaellig` : `${dl.daysLeft}d`}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ModulesTab({ report }: { report: ExecutiveReport }) {
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* DSGVO Module */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">DSGVO-Compliance</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<StatCard label="Verarbeitungen" value={report.dsgvo.processingActivities} color="blue" />
|
|
<StatCard label="TOM-Umsetzung" value={`${report.dsgvo.completionPercent}%`} color={report.dsgvo.completionPercent >= 80 ? 'green' : 'yellow'} />
|
|
<StatCard label="DSFAs abgeschlossen" value={report.dsgvo.dsfasCompleted} color="purple" />
|
|
<StatCard label="Loeschfristen definiert" value={report.dsgvo.retentionPolicies} color="gray" />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Vendors */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Lieferanten-Compliance</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<StatCard label="Lieferanten gesamt" value={report.vendors.totalVendors} color="purple" />
|
|
<StatCard label="Aktive Lieferanten" value={report.vendors.activeVendors} color="blue" />
|
|
<StatCard label="Ausstehende Pruefungen" value={report.vendors.pendingReviews} color={report.vendors.pendingReviews > 0 ? 'yellow' : 'green'} />
|
|
<StatCard label="Abgelaufene Vertraege" value={report.vendors.expiredContracts} color={report.vendors.expiredContracts > 0 ? 'red' : 'green'} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Incidents */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Datenschutzvorfaelle</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<StatCard label="Vorfaelle gesamt" value={report.incidents.totalIncidents} color="gray" />
|
|
<StatCard label="Offen" value={report.incidents.openIncidents} color={report.incidents.openIncidents > 0 ? 'orange' : 'green'} />
|
|
<StatCard label="Kritisch" value={report.incidents.criticalIncidents} color={report.incidents.criticalIncidents > 0 ? 'red' : 'green'} />
|
|
<StatCard label="Meldungen ausstehend" value={report.incidents.notificationsPending} color={report.incidents.notificationsPending > 0 ? 'red' : 'green'} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Whistleblower */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Hinweisgebersystem</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<StatCard label="Meldungen gesamt" value={report.whistleblower.totalReports} color="gray" />
|
|
<StatCard label="Offen" value={report.whistleblower.openReports} color={report.whistleblower.openReports > 0 ? 'yellow' : 'green'} />
|
|
<StatCard label="Eingangsbestaetigung ueberfaellig" value={report.whistleblower.overdueAcknowledgments} color={report.whistleblower.overdueAcknowledgments > 0 ? 'red' : 'green'} />
|
|
<StatCard label="Rueckmeldung ueberfaellig" value={report.whistleblower.overdueFeedbacks} color={report.whistleblower.overdueFeedbacks > 0 ? 'red' : 'green'} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Academy */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Compliance Academy</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<StatCard label="Kurse" value={report.academy.totalCourses} color="purple" />
|
|
<StatCard label="Einschreibungen" value={report.academy.totalEnrollments} color="blue" />
|
|
<StatCard label="Abschlussrate" value={`${Math.round(report.academy.completionRate)}%`} color={report.academy.completionRate >= 80 ? 'green' : 'yellow'} />
|
|
<StatCard label="Ueberfaellig" value={report.academy.overdueCount} color={report.academy.overdueCount > 0 ? 'red' : 'green'} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function ActivityTab({ report }: { report: ExecutiveReport }) {
|
|
return (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">Letzte Aktivitaeten</h3>
|
|
{report.recentActivity.length === 0 ? (
|
|
<p className="text-gray-500 text-sm">Keine Aktivitaeten vorhanden.</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{report.recentActivity.map((entry, i) => (
|
|
<div key={i} className="flex items-start gap-3 py-2 border-b border-gray-100 last:border-0">
|
|
<div className="w-2 h-2 rounded-full bg-purple-400 mt-2 flex-shrink-0" />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="text-sm text-gray-900">{entry.description}</div>
|
|
<div className="text-xs text-gray-500 flex gap-2 mt-0.5">
|
|
<span>{entry.module}</span>
|
|
<span>·</span>
|
|
<span>{entry.action}</span>
|
|
<span>·</span>
|
|
<span>{new Date(entry.timestamp).toLocaleString('de-DE')}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// =============================================================================
|
|
// MOCK DATA (used when backend is unavailable)
|
|
// =============================================================================
|
|
|
|
function getMockReport(): ExecutiveReport {
|
|
return {
|
|
generatedAt: new Date().toISOString(),
|
|
tenantId: 'demo',
|
|
complianceScore: 72,
|
|
dsgvo: {
|
|
processingActivities: 24,
|
|
activeProcessings: 18,
|
|
tomsImplemented: 31,
|
|
tomsPlanned: 7,
|
|
tomsTotal: 42,
|
|
completionPercent: 74,
|
|
openDSRs: 3,
|
|
overdueDSRs: 1,
|
|
dsfasCompleted: 4,
|
|
retentionPolicies: 12,
|
|
},
|
|
vendors: {
|
|
totalVendors: 15,
|
|
activeVendors: 12,
|
|
byRiskLevel: { LOW: 8, MEDIUM: 4, HIGH: 2, CRITICAL: 1 },
|
|
pendingReviews: 3,
|
|
expiredContracts: 1,
|
|
},
|
|
incidents: {
|
|
totalIncidents: 7,
|
|
openIncidents: 2,
|
|
criticalIncidents: 0,
|
|
notificationsPending: 0,
|
|
avgResolutionHours: 48.5,
|
|
},
|
|
whistleblower: {
|
|
totalReports: 4,
|
|
openReports: 1,
|
|
overdueAcknowledgments: 0,
|
|
overdueFeedbacks: 0,
|
|
avgResolutionDays: 21.3,
|
|
},
|
|
academy: {
|
|
totalCourses: 5,
|
|
totalEnrollments: 47,
|
|
completionRate: 68.5,
|
|
overdueCount: 4,
|
|
avgCompletionDays: 14.2,
|
|
},
|
|
riskOverview: {
|
|
overallLevel: 'MEDIUM',
|
|
moduleRisks: [
|
|
{ module: 'DSGVO', level: 'MEDIUM', score: 74, issues: 8 },
|
|
{ module: 'Lieferanten', level: 'HIGH', score: 55, issues: 5 },
|
|
{ module: 'Vorfaelle', level: 'LOW', score: 85, issues: 2 },
|
|
{ module: 'Hinweisgeberschutz', level: 'LOW', score: 90, issues: 1 },
|
|
{ module: 'Schulungen', level: 'MEDIUM', score: 68, issues: 4 },
|
|
],
|
|
openFindings: 12,
|
|
criticalFindings: 2,
|
|
},
|
|
upcomingDeadlines: [
|
|
{ module: 'DSGVO', type: 'Betroffenenanfrage', description: 'Auskunftsersuchen Max Mustermann', dueDate: new Date(Date.now() + 2 * 86400000).toISOString(), daysLeft: 2, severity: 'URGENT' },
|
|
{ module: 'Lieferanten', type: 'Vertragspruefung', description: 'AWS AVV-Erneuerung', dueDate: new Date(Date.now() + 14 * 86400000).toISOString(), daysLeft: 14, severity: 'WARNING' },
|
|
{ module: 'Schulungen', type: 'Pflichtschulung', description: 'DSGVO-Jahresschulung Q1 2026', dueDate: new Date(Date.now() + 30 * 86400000).toISOString(), daysLeft: 30, severity: 'INFO' },
|
|
{ module: 'Vorfaelle', type: 'Aufsichtsbehoerde', description: 'Meldung Datenpanne #7 an LfDI', dueDate: new Date(Date.now() - 1 * 86400000).toISOString(), daysLeft: -1, severity: 'OVERDUE' },
|
|
],
|
|
recentActivity: [
|
|
{ timestamp: new Date(Date.now() - 3600000).toISOString(), module: 'Academy', action: 'completed', description: 'IT-Sicherheitsschulung von Anna Mueller abgeschlossen' },
|
|
{ timestamp: new Date(Date.now() - 7200000).toISOString(), module: 'Incidents', action: 'created', description: 'Neuer Vorfall: USB-Stick mit Kundendaten verloren' },
|
|
{ timestamp: new Date(Date.now() - 86400000).toISOString(), module: 'DSGVO', action: 'updated', description: 'TOM IT-05 (Firewall-Policy) als umgesetzt markiert' },
|
|
{ timestamp: new Date(Date.now() - 172800000).toISOString(), module: 'Vendors', action: 'reviewed', description: 'Lieferanten-Assessment: Mailchimp abgeschlossen' },
|
|
],
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// MAIN COMPONENT
|
|
// =============================================================================
|
|
|
|
export default function ReportingPage() {
|
|
const { state } = useSDK()
|
|
const [activeTab, setActiveTab] = useState<TabId>('overview')
|
|
const [report, setReport] = useState<ExecutiveReport | null>(null)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
let cancelled = false
|
|
|
|
async function loadReport() {
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const data = await getExecutiveReport()
|
|
if (!cancelled) setReport(data)
|
|
} catch (err) {
|
|
console.warn('Backend nicht erreichbar, verwende Demo-Daten:', err)
|
|
if (!cancelled) setReport(getMockReport())
|
|
} finally {
|
|
if (!cancelled) setLoading(false)
|
|
}
|
|
}
|
|
|
|
loadReport()
|
|
return () => { cancelled = true }
|
|
}, [])
|
|
|
|
const tabs: Tab[] = [
|
|
{ id: 'overview', label: 'Uebersicht' },
|
|
{ id: 'risks', label: 'Risiken' },
|
|
{ id: 'deadlines', label: 'Fristen' },
|
|
{ id: 'modules', label: 'Module' },
|
|
{ id: 'activity', label: 'Aktivitaeten' },
|
|
]
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<StepHeader stepId="reporting" />
|
|
|
|
<TabNavigation tabs={tabs} activeTab={activeTab} onTabChange={setActiveTab} />
|
|
|
|
{loading ? (
|
|
<div className="flex items-center justify-center py-20">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
|
<span className="ml-3 text-gray-500">Bericht wird generiert...</span>
|
|
</div>
|
|
) : report ? (
|
|
<>
|
|
{/* Generated timestamp */}
|
|
<div className="text-xs text-gray-400 text-right">
|
|
Generiert: {new Date(report.generatedAt).toLocaleString('de-DE')}
|
|
</div>
|
|
|
|
{activeTab === 'overview' && <OverviewTab report={report} />}
|
|
{activeTab === 'risks' && <RisksTab report={report} />}
|
|
{activeTab === 'deadlines' && <DeadlinesTab report={report} />}
|
|
{activeTab === 'modules' && <ModulesTab report={report} />}
|
|
{activeTab === 'activity' && <ActivityTab report={report} />}
|
|
</>
|
|
) : (
|
|
<div className="text-center py-20 text-gray-500">
|
|
<p>Bericht konnte nicht geladen werden.</p>
|
|
{error && <p className="text-sm text-red-500 mt-2">{error}</p>}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|