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>
483 lines
17 KiB
TypeScript
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>
|
|
)
|
|
}
|