refactor: Admin-Layout komplett entfernt — SDK als einziges Layout
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
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>
This commit is contained in:
482
admin-compliance/app/sdk/einwilligungen/retention/page.tsx
Normal file
482
admin-compliance/app/sdk/einwilligungen/retention/page.tsx
Normal file
@@ -0,0 +1,482 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user