Files
breakpilot-compliance/admin-compliance/app/sdk/einwilligungen/retention/page.tsx
Benjamin Admin 215b95adfa
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
refactor: Admin-Layout komplett entfernt — SDK als einziges Layout
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>
2026-03-04 11:43:00 +01:00

483 lines
17 KiB
TypeScript

'use client'
/**
* Retention Matrix Page (Loeschfristen)
*
* Zeigt die Loeschfristen-Matrix fuer alle Datenpunkte nach Kategorien.
*/
import { useState, useMemo } from 'react'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import { RetentionMatrix } from '@/components/sdk/einwilligungen'
import {
EinwilligungenProvider,
useEinwilligungen,
} from '@/lib/sdk/einwilligungen/context'
import { RETENTION_MATRIX } from '@/lib/sdk/einwilligungen/catalog/loader'
import {
SupportedLanguage,
RETENTION_PERIOD_INFO,
DataPointCategory,
} from '@/lib/sdk/einwilligungen/types'
import {
Clock,
Calendar,
AlertTriangle,
Info,
Download,
Filter,
ArrowLeft,
BarChart3,
Shield,
Scale,
} from 'lucide-react'
import Link from 'next/link'
// =============================================================================
// RETENTION STATS
// =============================================================================
interface RetentionStatsProps {
stats: Record<string, number>
}
function RetentionStats({ stats }: RetentionStatsProps) {
const shortTerm = (stats['24_HOURS'] || 0) + (stats['30_DAYS'] || 0)
const mediumTerm = (stats['90_DAYS'] || 0) + (stats['12_MONTHS'] || 0)
const longTerm = (stats['24_MONTHS'] || 0) + (stats['36_MONTHS'] || 0)
const legalTerm = (stats['6_YEARS'] || 0) + (stats['10_YEARS'] || 0)
const variable = (stats['UNTIL_REVOCATION'] || 0) +
(stats['UNTIL_PURPOSE_FULFILLED'] || 0) +
(stats['UNTIL_ACCOUNT_DELETION'] || 0)
const total = shortTerm + mediumTerm + longTerm + legalTerm + variable
return (
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
<div className="bg-green-50 rounded-xl p-4 border border-green-200">
<div className="flex items-center gap-2 text-green-600 mb-1">
<Clock className="w-4 h-4" />
<span className="text-sm font-medium">Kurzfristig</span>
</div>
<div className="text-2xl font-bold text-green-700">{shortTerm}</div>
<div className="text-xs text-green-600 mt-1">
30 Tage ({total > 0 ? Math.round((shortTerm / total) * 100) : 0}%)
</div>
</div>
<div className="bg-blue-50 rounded-xl p-4 border border-blue-200">
<div className="flex items-center gap-2 text-blue-600 mb-1">
<Calendar className="w-4 h-4" />
<span className="text-sm font-medium">Mittelfristig</span>
</div>
<div className="text-2xl font-bold text-blue-700">{mediumTerm}</div>
<div className="text-xs text-blue-600 mt-1">
90 Tage - 12 Monate ({total > 0 ? Math.round((mediumTerm / total) * 100) : 0}%)
</div>
</div>
<div className="bg-amber-50 rounded-xl p-4 border border-amber-200">
<div className="flex items-center gap-2 text-amber-600 mb-1">
<Calendar className="w-4 h-4" />
<span className="text-sm font-medium">Langfristig</span>
</div>
<div className="text-2xl font-bold text-amber-700">{longTerm}</div>
<div className="text-xs text-amber-600 mt-1">
2-3 Jahre ({total > 0 ? Math.round((longTerm / total) * 100) : 0}%)
</div>
</div>
<div className="bg-red-50 rounded-xl p-4 border border-red-200">
<div className="flex items-center gap-2 text-red-600 mb-1">
<Scale className="w-4 h-4" />
<span className="text-sm font-medium">Gesetzlich</span>
</div>
<div className="text-2xl font-bold text-red-700">{legalTerm}</div>
<div className="text-xs text-red-600 mt-1">
6-10 Jahre AO/HGB ({total > 0 ? Math.round((legalTerm / total) * 100) : 0}%)
</div>
</div>
<div className="bg-purple-50 rounded-xl p-4 border border-purple-200">
<div className="flex items-center gap-2 text-purple-600 mb-1">
<Shield className="w-4 h-4" />
<span className="text-sm font-medium">Variabel</span>
</div>
<div className="text-2xl font-bold text-purple-700">{variable}</div>
<div className="text-xs text-purple-600 mt-1">
Bis Widerruf/Zweck ({total > 0 ? Math.round((variable / total) * 100) : 0}%)
</div>
</div>
</div>
)
}
// =============================================================================
// LEGAL INFO PANEL
// =============================================================================
function LegalInfoPanel() {
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center gap-3 mb-4">
<Info className="w-5 h-5 text-indigo-500" />
<h3 className="font-semibold text-slate-900">Rechtliche Grundlagen</h3>
</div>
<div className="space-y-4 text-sm text-slate-600">
<div className="p-3 bg-slate-50 rounded-lg">
<h4 className="font-medium text-slate-900 mb-1">Art. 17 DSGVO - Loeschpflicht</h4>
<p>
Personenbezogene Daten muessen geloescht werden, sobald der Zweck der
Verarbeitung entfaellt und keine gesetzlichen Aufbewahrungspflichten
entgegenstehen.
</p>
</div>
<div className="p-3 bg-slate-50 rounded-lg">
<h4 className="font-medium text-slate-900 mb-1">§ 147 AO - Steuerliche Aufbewahrung</h4>
<p>
Buchungsbelege, Rechnungen und geschaeftsrelevante Unterlagen muessen
fuer <strong>10 Jahre</strong> aufbewahrt werden.
</p>
</div>
<div className="p-3 bg-slate-50 rounded-lg">
<h4 className="font-medium text-slate-900 mb-1">§ 257 HGB - Handelsrechtlich</h4>
<p>
Handelsbuecher und Inventare: <strong>10 Jahre</strong>. Handels- und
Geschaeftsbriefe: <strong>6 Jahre</strong>.
</p>
</div>
<div className="p-3 bg-amber-50 rounded-lg border border-amber-200">
<h4 className="font-medium text-amber-800 mb-1">Hinweis zur Umsetzung</h4>
<p className="text-amber-700">
Die Loeschfristen muessen technisch umgesetzt werden. Implementieren Sie
automatische Loeschprozesse oder Benachrichtigungen fuer manuelle Pruefungen.
</p>
</div>
</div>
</div>
)
}
// =============================================================================
// RETENTION TIMELINE
// =============================================================================
interface RetentionTimelineProps {
dataPoints: Array<{
id: string
code: string
name: { de: string; en: string }
retentionPeriod: string
}>
language: SupportedLanguage
}
function RetentionTimeline({ dataPoints, language }: RetentionTimelineProps) {
// Sort by retention period duration
const sortedDataPoints = useMemo(() => {
const getPeriodDays = (period: string): number => {
const info = RETENTION_PERIOD_INFO[period as keyof typeof RETENTION_PERIOD_INFO]
return info?.days ?? 99999
}
return [...dataPoints].sort((a, b) => {
return getPeriodDays(a.retentionPeriod) - getPeriodDays(b.retentionPeriod)
})
}, [dataPoints])
const getColorForPeriod = (period: string): string => {
const days = RETENTION_PERIOD_INFO[period as keyof typeof RETENTION_PERIOD_INFO]?.days
if (days === null) return 'bg-purple-500'
if (days <= 30) return 'bg-green-500'
if (days <= 365) return 'bg-blue-500'
if (days <= 1095) return 'bg-amber-500'
return 'bg-red-500'
}
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center gap-3 mb-4">
<BarChart3 className="w-5 h-5 text-indigo-500" />
<h3 className="font-semibold text-slate-900">Timeline der Loeschfristen</h3>
</div>
<div className="space-y-2">
{sortedDataPoints.slice(0, 15).map((dp) => {
const info = RETENTION_PERIOD_INFO[dp.retentionPeriod as keyof typeof RETENTION_PERIOD_INFO]
const maxDays = 3650 // 10 Jahre als Maximum
const width = info?.days !== null
? Math.min(100, ((info?.days || 0) / maxDays) * 100)
: 100
return (
<div key={dp.id} className="flex items-center gap-3">
<span className="w-10 text-xs font-mono text-slate-400 shrink-0">
{dp.code}
</span>
<div className="flex-1 min-w-0">
<div className="h-6 bg-slate-100 rounded-full overflow-hidden">
<div
className={`h-full ${getColorForPeriod(dp.retentionPeriod)} rounded-full flex items-center justify-end pr-2`}
style={{ width: `${Math.max(width, 15)}%` }}
>
<span className="text-xs text-white font-medium truncate">
{info?.label[language]}
</span>
</div>
</div>
</div>
</div>
)
})}
{sortedDataPoints.length > 15 && (
<p className="text-xs text-slate-500 text-center pt-2">
+ {sortedDataPoints.length - 15} weitere Datenpunkte
</p>
)}
</div>
{/* Legend */}
<div className="flex flex-wrap items-center gap-4 mt-4 pt-4 border-t border-slate-100 text-xs text-slate-600">
<span className="font-medium">Legende:</span>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-green-500" />
30 Tage
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-blue-500" />
90T-12M
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-amber-500" />
2-3 Jahre
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-red-500" />
6-10 Jahre
</div>
<div className="flex items-center gap-1.5">
<div className="w-3 h-3 rounded-full bg-purple-500" />
Variabel
</div>
</div>
</div>
)
}
// =============================================================================
// EXPORT OPTIONS
// =============================================================================
interface ExportOptionsProps {
onExport: (format: 'csv' | 'json' | 'pdf') => void
}
function ExportOptions({ onExport }: ExportOptionsProps) {
return (
<div className="flex items-center gap-2">
<button
onClick={() => onExport('csv')}
className="flex items-center gap-2 px-3 py-2 border border-slate-300 rounded-lg text-sm text-slate-700 hover:bg-slate-50"
>
<Download className="w-4 h-4" />
CSV
</button>
<button
onClick={() => onExport('json')}
className="flex items-center gap-2 px-3 py-2 border border-slate-300 rounded-lg text-sm text-slate-700 hover:bg-slate-50"
>
<Download className="w-4 h-4" />
JSON
</button>
<button
onClick={() => onExport('pdf')}
className="flex items-center gap-2 px-3 py-2 bg-indigo-600 text-white rounded-lg text-sm hover:bg-indigo-700"
>
<Download className="w-4 h-4" />
PDF
</button>
</div>
)
}
// =============================================================================
// MAIN CONTENT
// =============================================================================
function RetentionContent() {
const { state } = useSDK()
const { allDataPoints } = useEinwilligungen()
const [language, setLanguage] = useState<SupportedLanguage>('de')
const [filterCategory, setFilterCategory] = useState<DataPointCategory | 'ALL'>('ALL')
// Calculate stats
const stats = useMemo(() => {
const periodCounts: Record<string, number> = {}
for (const dp of allDataPoints) {
periodCounts[dp.retentionPeriod] = (periodCounts[dp.retentionPeriod] || 0) + 1
}
return periodCounts
}, [allDataPoints])
// Filter data points
const filteredDataPoints = useMemo(() => {
if (filterCategory === 'ALL') return allDataPoints
return allDataPoints.filter((dp) => dp.category === filterCategory)
}, [allDataPoints, filterCategory])
// Handle export
const handleExport = (format: 'csv' | 'json' | 'pdf') => {
if (format === 'csv') {
const headers = ['Code', 'Name', 'Kategorie', 'Loeschfrist', 'Rechtsgrundlage']
const rows = allDataPoints.map((dp) => [
dp.code,
dp.name[language],
dp.category,
RETENTION_PERIOD_INFO[dp.retentionPeriod as keyof typeof RETENTION_PERIOD_INFO]?.label[language] || dp.retentionPeriod,
dp.legalBasis,
])
const csv = [headers, ...rows].map((row) => row.join(';')).join('\n')
downloadFile(csv, 'loeschfristen.csv', 'text/csv')
} else if (format === 'json') {
const data = allDataPoints.map((dp) => ({
code: dp.code,
name: dp.name,
category: dp.category,
retentionPeriod: dp.retentionPeriod,
retentionLabel: RETENTION_PERIOD_INFO[dp.retentionPeriod as keyof typeof RETENTION_PERIOD_INFO]?.label,
legalBasis: dp.legalBasis,
}))
downloadFile(JSON.stringify(data, null, 2), 'loeschfristen.json', 'application/json')
} else {
alert('PDF-Export wird noch implementiert.')
}
}
const downloadFile = (content: string, filename: string, type: string) => {
const blob = new Blob([content], { type })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = filename
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
// 18 Kategorien (A-R)
const categories: Array<{ id: DataPointCategory | 'ALL'; label: string }> = [
{ id: 'ALL', label: 'Alle Kategorien' },
{ id: 'MASTER_DATA', label: 'A: Stammdaten' },
{ id: 'CONTACT_DATA', label: 'B: Kontaktdaten' },
{ id: 'AUTHENTICATION', label: 'C: Authentifizierung' },
{ id: 'CONSENT', label: 'D: Einwilligung' },
{ id: 'COMMUNICATION', label: 'E: Kommunikation' },
{ id: 'PAYMENT', label: 'F: Zahlung' },
{ id: 'USAGE_DATA', label: 'G: Nutzungsdaten' },
{ id: 'LOCATION', label: 'H: Standort' },
{ id: 'DEVICE_DATA', label: 'I: Geraetedaten' },
{ id: 'MARKETING', label: 'J: Marketing' },
{ id: 'ANALYTICS', label: 'K: Analyse' },
{ id: 'SOCIAL_MEDIA', label: 'L: Social Media' },
{ id: 'HEALTH_DATA', label: 'M: Gesundheit (Art. 9)' },
{ id: 'EMPLOYEE_DATA', label: 'N: Beschaeftigte (BDSG § 26)' },
{ id: 'CONTRACT_DATA', label: 'O: Vertraege' },
{ id: 'LOG_DATA', label: 'P: Protokolle' },
{ id: 'AI_DATA', label: 'Q: KI-Daten (AI Act)' },
{ id: 'SECURITY', label: 'R: Sicherheit' },
]
return (
<div className="space-y-6">
{/* Back Link */}
<Link
href="/sdk/einwilligungen/catalog"
className="inline-flex items-center gap-2 text-sm text-slate-600 hover:text-slate-900"
>
<ArrowLeft className="w-4 h-4" />
Zurueck zum Katalog
</Link>
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900">Loeschfristen-Matrix</h1>
<p className="text-slate-600 mt-1">
Uebersicht aller Aufbewahrungsfristen gemaess DSGVO, AO und HGB.
</p>
</div>
<ExportOptions onExport={handleExport} />
</div>
{/* Stats */}
<RetentionStats stats={stats} />
{/* Filters */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<select
value={filterCategory}
onChange={(e) => setFilterCategory(e.target.value as DataPointCategory | 'ALL')}
className="px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
{categories.map((cat) => (
<option key={cat.id} value={cat.id}>
{cat.label}
</option>
))}
</select>
<select
value={language}
onChange={(e) => setLanguage(e.target.value as SupportedLanguage)}
className="px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-indigo-500"
>
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</div>
<div className="text-sm text-slate-500">
{filteredDataPoints.length} Datenpunkte
</div>
</div>
{/* Two Column Layout */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Matrix */}
<div className="lg:col-span-2">
<RetentionMatrix
matrix={RETENTION_MATRIX}
dataPoints={filteredDataPoints}
language={language}
showDetails={true}
/>
</div>
{/* Right: Sidebar */}
<div className="space-y-6">
<RetentionTimeline dataPoints={filteredDataPoints} language={language} />
<LegalInfoPanel />
</div>
</div>
</div>
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function RetentionPage() {
return (
<EinwilligungenProvider>
<RetentionContent />
</EinwilligungenProvider>
)
}