feat: DSFA Modul — Backend, Proxy, Frontend-Migration, Tests + Mock-Daten entfernt
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 38s
CI / test-python-backend-compliance (push) Successful in 38s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 19s

- Migration 024: compliance_dsfas + compliance_dsfa_audit_log Tabellen
- dsfa_routes.py: CRUD + stats + audit-log + PATCH status Endpoints
- Proxy: /api/sdk/v1/dsfa/[[...path]] → backend-compliance:8002/api/v1/dsfa
- dsfa/page.tsx: mockDSFAs entfernt → echte API (loadDSFAs, handleCreateDSFA, handleStatusChange, handleDeleteDSFA)
- GeneratorWizard: kontrollierte Inputs + onSubmit-Handler
- reporting/page.tsx: getMockReport() Fallback entfernt → Fehlerstate
- dsr/[requestId]/page.tsx: mockCommunications entfernt → leeres Array (TODO: Backend fehlt)
- 52 neue Tests (680 gesamt, alle grün)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-04 22:41:05 +01:00
parent dc0d38ea40
commit a694b9d9ea
8 changed files with 1199 additions and 182 deletions

View File

@@ -0,0 +1,123 @@
/**
* DSFA API Proxy — Datenschutz-Folgenabschaetzung (Art. 35 DSGVO)
* Proxies /api/sdk/v1/dsfa/* → backend-compliance:8002/api/v1/dsfa/*
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend-compliance:8002'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${BACKEND_URL}/api/v1/dsfa`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
const clientUserId = request.headers.get('x-user-id')
const clientTenantId = request.headers.get('x-tenant-id')
headers['X-User-ID'] = (clientUserId && uuidRegex.test(clientUserId))
? clientUserId
: '00000000-0000-0000-0000-000000000001'
headers['X-Tenant-ID'] = (clientTenantId && uuidRegex.test(clientTenantId))
? clientTenantId
: (process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e')
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(60000),
}
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
const body = await request.text()
if (body) {
fetchOptions.body = body
}
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('DSFA API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Compliance Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PATCH')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -1,6 +1,6 @@
'use client'
import React, { useState, useCallback } from 'react'
import React, { useState, useCallback, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
@@ -15,8 +15,8 @@ interface DSFA {
title: string
description: string
status: 'draft' | 'in-review' | 'approved' | 'needs-update'
createdAt: Date
updatedAt: Date
createdAt: string
updatedAt: string
approvedBy: string | null
riskLevel: 'low' | 'medium' | 'high' | 'critical'
processingActivity: string
@@ -25,60 +25,19 @@ interface DSFA {
measures: string[]
}
// =============================================================================
// MOCK DATA
// =============================================================================
const mockDSFAs: DSFA[] = [
{
id: 'dsfa-1',
title: 'DSFA - Bewerber-Management-System',
description: 'Datenschutz-Folgenabschaetzung fuer das KI-gestuetzte Bewerber-Screening',
status: 'in-review',
createdAt: new Date('2024-01-10'),
updatedAt: new Date('2024-01-20'),
approvedBy: null,
riskLevel: 'high',
processingActivity: 'Automatisierte Bewertung von Bewerbungsunterlagen',
dataCategories: ['Kontaktdaten', 'Beruflicher Werdegang', 'Qualifikationen'],
recipients: ['HR-Abteilung', 'Fachabteilungen'],
measures: ['Verschluesselung', 'Zugriffskontrolle', 'Menschliche Pruefung'],
},
{
id: 'dsfa-2',
title: 'DSFA - Video-Ueberwachung Buero',
description: 'Datenschutz-Folgenabschaetzung fuer die Videoueberwachung im Buerogebaeude',
status: 'approved',
createdAt: new Date('2023-11-01'),
updatedAt: new Date('2023-12-15'),
approvedBy: 'DSB Mueller',
riskLevel: 'medium',
processingActivity: 'Videoueberwachung zu Sicherheitszwecken',
dataCategories: ['Bilddaten', 'Bewegungsdaten'],
recipients: ['Sicherheitsdienst'],
measures: ['Loeschfristen', 'Zugriffsbeschraenkung', 'Hinweisschilder'],
},
{
id: 'dsfa-3',
title: 'DSFA - Kundenanalyse',
description: 'Datenschutz-Folgenabschaetzung fuer Big-Data-Kundenanalysen',
status: 'draft',
createdAt: new Date('2024-01-22'),
updatedAt: new Date('2024-01-22'),
approvedBy: null,
riskLevel: 'high',
processingActivity: 'Analyse von Kundenverhalten fuer Marketing',
dataCategories: ['Kaufhistorie', 'Nutzungsverhalten', 'Praeferenzen'],
recipients: ['Marketing', 'Vertrieb'],
measures: [],
},
]
// =============================================================================
// COMPONENTS
// =============================================================================
function DSFACard({ dsfa }: { dsfa: DSFA }) {
function DSFACard({
dsfa,
onStatusChange,
onDelete,
}: {
dsfa: DSFA
onStatusChange: (id: string, status: string) => void
onDelete: (id: string) => void
}) {
const statusColors = {
draft: 'bg-gray-100 text-gray-600 border-gray-200',
'in-review': 'bg-yellow-100 text-yellow-700 border-yellow-200',
@@ -100,6 +59,10 @@ function DSFACard({ dsfa }: { dsfa: DSFA }) {
critical: 'bg-red-100 text-red-700',
}
const createdDate = dsfa.createdAt
? new Date(dsfa.createdAt).toLocaleDateString('de-DE')
: '—'
return (
<div className={`bg-white rounded-xl border-2 p-6 ${
dsfa.status === 'needs-update' ? 'border-orange-200' :
@@ -149,17 +112,33 @@ function DSFACard({ dsfa }: { dsfa: DSFA }) {
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between text-sm">
<div className="text-gray-500">
<span>Erstellt: {dsfa.createdAt.toLocaleDateString('de-DE')}</span>
<span>Erstellt: {createdDate}</span>
{dsfa.approvedBy && (
<span className="ml-4">Genehmigt von: {dsfa.approvedBy}</span>
)}
</div>
<div className="flex items-center gap-2">
<button className="px-3 py-1 text-purple-600 hover:bg-purple-50 rounded-lg transition-colors">
Bearbeiten
</button>
<button className="px-3 py-1 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Exportieren
{dsfa.status === 'draft' && (
<button
onClick={() => onStatusChange(dsfa.id, 'in-review')}
className="px-3 py-1 text-yellow-600 hover:bg-yellow-50 rounded-lg transition-colors text-xs"
>
Zur Pruefung
</button>
)}
{dsfa.status === 'in-review' && (
<button
onClick={() => onStatusChange(dsfa.id, 'approved')}
className="px-3 py-1 text-green-600 hover:bg-green-50 rounded-lg transition-colors text-xs"
>
Genehmigen
</button>
)}
<button
onClick={() => onDelete(dsfa.id)}
className="px-3 py-1 text-red-500 hover:bg-red-50 rounded-lg transition-colors text-xs"
>
Loeschen
</button>
</div>
</div>
@@ -167,8 +146,37 @@ function DSFACard({ dsfa }: { dsfa: DSFA }) {
)
}
function GeneratorWizard({ onClose }: { onClose: () => void }) {
function GeneratorWizard({ onClose, onSubmit }: { onClose: () => void; onSubmit: (data: Partial<DSFA>) => Promise<void> }) {
const [step, setStep] = useState(1)
const [saving, setSaving] = useState(false)
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [processingActivity, setProcessingActivity] = useState('')
const [selectedCategories, setSelectedCategories] = useState<string[]>([])
const [riskLevel, setRiskLevel] = useState<'low' | 'medium' | 'high' | 'critical'>('low')
const [selectedMeasures, setSelectedMeasures] = useState<string[]>([])
const riskMap: Record<string, 'low' | 'medium' | 'high' | 'critical'> = {
Niedrig: 'low', Mittel: 'medium', Hoch: 'high', Kritisch: 'critical',
}
const handleSubmit = async () => {
setSaving(true)
try {
await onSubmit({
title,
description,
processingActivity,
dataCategories: selectedCategories,
riskLevel,
measures: selectedMeasures,
status: 'draft',
})
onClose()
} finally {
setSaving(false)
}
}
return (
<div className="bg-white rounded-xl border border-gray-200 p-6">
@@ -208,6 +216,8 @@ function GeneratorWizard({ onClose }: { onClose: () => void }) {
<label className="block text-sm font-medium text-gray-700 mb-1">Titel der DSFA</label>
<input
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="z.B. DSFA - Mitarbeiter-Monitoring"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
/>
@@ -216,10 +226,22 @@ function GeneratorWizard({ onClose }: { onClose: () => void }) {
<label className="block text-sm font-medium text-gray-700 mb-1">Beschreibung der Verarbeitung</label>
<textarea
rows={3}
value={description}
onChange={e => setDescription(e.target.value)}
placeholder="Beschreiben Sie die geplante Datenverarbeitung..."
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Verarbeitungstaetigkeit</label>
<input
type="text"
value={processingActivity}
onChange={e => setProcessingActivity(e.target.value)}
placeholder="z.B. Automatisierte Auswertung von Kundendaten"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500"
/>
</div>
</div>
)}
{step === 2 && (
@@ -229,7 +251,14 @@ function GeneratorWizard({ onClose }: { onClose: () => void }) {
<div className="grid grid-cols-2 gap-2">
{['Kontaktdaten', 'Identifikationsdaten', 'Finanzdaten', 'Gesundheitsdaten', 'Standortdaten', 'Nutzungsdaten'].map(cat => (
<label key={cat} className="flex items-center gap-2 p-2 border rounded-lg hover:bg-gray-50">
<input type="checkbox" className="w-4 h-4 text-purple-600" />
<input
type="checkbox"
className="w-4 h-4 text-purple-600"
checked={selectedCategories.includes(cat)}
onChange={e => setSelectedCategories(prev =>
e.target.checked ? [...prev, cat] : prev.filter(c => c !== cat)
)}
/>
<span className="text-sm">{cat}</span>
</label>
))}
@@ -243,9 +272,15 @@ function GeneratorWizard({ onClose }: { onClose: () => void }) {
<label className="block text-sm font-medium text-gray-700 mb-2">Risikobewertung</label>
<p className="text-sm text-gray-500 mb-4">Bewerten Sie die Risiken fuer die Rechte und Freiheiten der Betroffenen.</p>
<div className="space-y-2">
{['Niedrig', 'Mittel', 'Hoch', 'Kritisch'].map(level => (
{(['Niedrig', 'Mittel', 'Hoch', 'Kritisch'] as const).map(level => (
<label key={level} className="flex items-center gap-3 p-3 border rounded-lg hover:bg-gray-50 cursor-pointer">
<input type="radio" name="risk" className="w-4 h-4 text-purple-600" />
<input
type="radio"
name="risk"
className="w-4 h-4 text-purple-600"
checked={riskLevel === riskMap[level]}
onChange={() => setRiskLevel(riskMap[level])}
/>
<span className="text-sm font-medium">{level}</span>
</label>
))}
@@ -260,7 +295,14 @@ function GeneratorWizard({ onClose }: { onClose: () => void }) {
<div className="grid grid-cols-2 gap-2">
{['Verschluesselung', 'Pseudonymisierung', 'Zugriffskontrolle', 'Loeschkonzept', 'Schulungen', 'Menschliche Pruefung'].map(m => (
<label key={m} className="flex items-center gap-2 p-2 border rounded-lg hover:bg-gray-50">
<input type="checkbox" className="w-4 h-4 text-purple-600" />
<input
type="checkbox"
className="w-4 h-4 text-purple-600"
checked={selectedMeasures.includes(m)}
onChange={e => setSelectedMeasures(prev =>
e.target.checked ? [...prev, m] : prev.filter(x => x !== m)
)}
/>
<span className="text-sm">{m}</span>
</label>
))}
@@ -275,14 +317,16 @@ function GeneratorWizard({ onClose }: { onClose: () => void }) {
<button
onClick={() => step > 1 ? setStep(step - 1) : onClose()}
className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
disabled={saving}
>
{step === 1 ? 'Abbrechen' : 'Zurueck'}
</button>
<button
onClick={() => step < 4 ? setStep(step + 1) : onClose()}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
onClick={() => step < 4 ? setStep(step + 1) : handleSubmit()}
disabled={saving || (step === 1 && !title.trim())}
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
>
{step === 4 ? 'DSFA erstellen' : 'Weiter'}
{step === 4 ? (saving ? 'Wird erstellt...' : 'DSFA erstellen') : 'Weiter'}
</button>
</div>
</div>
@@ -296,10 +340,81 @@ function GeneratorWizard({ onClose }: { onClose: () => void }) {
export default function DSFAPage() {
const router = useRouter()
const { state } = useSDK()
const [dsfas] = useState<DSFA[]>(mockDSFAs)
const [dsfas, setDsfas] = useState<DSFA[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [showGenerator, setShowGenerator] = useState(false)
const [filter, setFilter] = useState<string>('all')
const loadDSFAs = useCallback(async () => {
setIsLoading(true)
setError(null)
try {
const res = await fetch('/api/sdk/v1/dsfa?tenant_id=default')
if (!res.ok) throw new Error(`Fehler: ${res.status}`)
const data = await res.json()
const mapped: DSFA[] = (Array.isArray(data) ? data : []).map((d: Record<string, unknown>) => ({
id: d.id as string,
title: d.title as string,
description: (d.description as string) || '',
status: (d.status as DSFA['status']) || 'draft',
createdAt: d.created_at as string,
updatedAt: d.updated_at as string,
approvedBy: (d.approved_by as string) || null,
riskLevel: (d.risk_level as DSFA['riskLevel']) || 'low',
processingActivity: (d.processing_activity as string) || '',
dataCategories: (d.data_categories as string[]) || [],
recipients: (d.recipients as string[]) || [],
measures: (d.measures as string[]) || [],
}))
setDsfas(mapped)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
} finally {
setIsLoading(false)
}
}, [])
useEffect(() => {
loadDSFAs()
}, [loadDSFAs])
const handleCreateDSFA = useCallback(async (data: Partial<DSFA>) => {
const res = await fetch('/api/sdk/v1/dsfa?tenant_id=default', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
title: data.title,
description: data.description || '',
processing_activity: data.processingActivity || '',
data_categories: data.dataCategories || [],
recipients: data.recipients || [],
measures: data.measures || [],
risk_level: data.riskLevel || 'low',
status: data.status || 'draft',
}),
})
if (!res.ok) throw new Error(`Fehler beim Erstellen: ${res.status}`)
await loadDSFAs()
}, [loadDSFAs])
const handleStatusChange = useCallback(async (id: string, status: string) => {
const res = await fetch(`/api/sdk/v1/dsfa/${id}/status?tenant_id=default`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status }),
})
if (!res.ok) throw new Error(`Statuswechsel fehlgeschlagen: ${res.status}`)
await loadDSFAs()
}, [loadDSFAs])
const handleDeleteDSFA = useCallback(async (id: string) => {
if (!confirm('DSFA wirklich loeschen?')) return
const res = await fetch(`/api/sdk/v1/dsfa/${id}?tenant_id=default`, { method: 'DELETE' })
if (!res.ok) throw new Error(`Loeschen fehlgeschlagen: ${res.status}`)
await loadDSFAs()
}, [loadDSFAs])
// Handle uploaded document
const handleDocumentProcessed = useCallback((doc: UploadedDocument) => {
console.log('[DSFA Page] Document processed:', doc)
@@ -345,7 +460,10 @@ export default function DSFAPage() {
{/* Generator */}
{showGenerator && (
<GeneratorWizard onClose={() => setShowGenerator(false)} />
<GeneratorWizard
onClose={() => setShowGenerator(false)}
onSubmit={handleCreateDSFA}
/>
)}
{/* Document Upload Section */}
@@ -375,6 +493,14 @@ export default function DSFAPage() {
</div>
</div>
{/* Error */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-xl p-4 text-red-700 text-sm">
Fehler beim Laden: {error}
<button onClick={loadDSFAs} className="ml-4 underline">Erneut versuchen</button>
</div>
)}
{/* Filter */}
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">Filter:</span>
@@ -396,14 +522,26 @@ export default function DSFAPage() {
))}
</div>
{/* DSFA List */}
<div className="space-y-4">
{filteredDSFAs.map(dsfa => (
<DSFACard key={dsfa.id} dsfa={dsfa} />
))}
</div>
{/* Loading */}
{isLoading && (
<div className="text-center py-12 text-gray-500">Lade DSFAs...</div>
)}
{filteredDSFAs.length === 0 && !showGenerator && (
{/* DSFA List */}
{!isLoading && (
<div className="space-y-4">
{filteredDSFAs.map(dsfa => (
<DSFACard
key={dsfa.id}
dsfa={dsfa}
onStatusChange={handleStatusChange}
onDelete={handleDeleteDSFA}
/>
))}
</div>
)}
{!isLoading && filteredDSFAs.length === 0 && !showGenerator && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-purple-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">

View File

@@ -26,32 +26,7 @@ import {
// MOCK COMMUNICATIONS
// =============================================================================
const mockCommunications: DSRCommunication[] = [
{
id: 'comm-001',
dsrId: 'dsr-001',
type: 'outgoing',
channel: 'email',
subject: 'Eingangsbestaetigung Ihrer Anfrage',
content: 'Sehr geehrte(r) Antragsteller(in),\n\nwir bestaetigen den Eingang Ihrer Anfrage und werden diese innerhalb der gesetzlichen Frist bearbeiten.\n\nMit freundlichen Gruessen\nIhr Datenschutz-Team',
sentAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
sentBy: 'System',
createdAt: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'System'
},
{
id: 'comm-002',
dsrId: 'dsr-001',
type: 'outgoing',
channel: 'email',
subject: 'Identitaetspruefung erforderlich',
content: 'Sehr geehrte(r) Antragsteller(in),\n\nbitte senden Sie uns zur Bearbeitung Ihrer Anfrage einen Identitaetsnachweis zu.\n\nMit freundlichen Gruessen',
sentAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
sentBy: 'DSB Mueller',
createdAt: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'DSB Mueller'
}
]
// TODO: Backend fehlt — Communications API noch nicht implementiert
// =============================================================================
// COMPONENTS
@@ -262,8 +237,8 @@ export default function DSRDetailPage() {
const found = await fetchSDKDSR(requestId)
if (found) {
setRequest(found)
// Communications are loaded as mock for now (no backend API yet)
setCommunications(mockCommunications.filter(c => c.dsrId === requestId))
// TODO: Backend fehlt — Communications API noch nicht implementiert
setCommunications([])
}
} catch (error) {
console.error('Failed to load DSR:', error)

View File

@@ -362,82 +362,6 @@ function ActivityTab({ report }: { report: ExecutiveReport }) {
)
}
// =============================================================================
// 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
// =============================================================================
@@ -459,8 +383,8 @@ export default function ReportingPage() {
const data = await getExecutiveReport()
if (!cancelled) setReport(data)
} catch (err) {
console.warn('Backend nicht erreichbar, verwende Demo-Daten:', err)
if (!cancelled) setReport(getMockReport())
console.warn('Backend nicht erreichbar:', err)
if (!cancelled) setError(err instanceof Error ? err.message : 'Verbindung zum Backend fehlgeschlagen')
} finally {
if (!cancelled) setLoading(false)
}