This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/admin-v2/app/(admin)/compliance/loeschfristen/page.tsx
BreakPilot Dev 660295e218 fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit
restores all missing components:

- Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education,
  infrastructure, communication, development, onboarding, rbac
- SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen,
  vendor-compliance, tom-generator, dsr, and more
- Developer portal (25 pages): API docs, SDK guides, frameworks
- All components, lib files, hooks, and types
- Updated package.json with all dependencies

The issue was caused by incomplete initial repository state - the full
admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2
but was never fully synced to the main admin-v2 directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-08 23:40:15 -08:00

512 lines
22 KiB
TypeScript

'use client'
/**
* Loeschfristen - Data Retention Management
*
* Art. 17 DSGVO - Recht auf Loeschung
* Art. 5 Abs. 1 lit. e DSGVO - Speicherbegrenzung
*
* Verwaltet:
* - Aufbewahrungsfristen
* - Consent-Deadlines
* - Automatische Loeschungen
*/
import { useState, useEffect } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
const API_BASE = '/api/admin/consent'
interface RetentionPolicy {
id: string
dataCategory: string
retentionPeriod: string
legalBasis: string
autoDelete: boolean
lastRun?: string
nextRun?: string
itemsToDelete?: number
}
interface ConsentDeadline {
id: string
userId: string
documentName: string
versionNumber: string
deadlineAt: string
reminderCount: number
daysRemaining: number
status: 'pending' | 'overdue' | 'completed'
}
interface DeletionJob {
id: string
dataCategory: string
scheduledAt: string
status: 'scheduled' | 'running' | 'completed' | 'failed'
itemsProcessed: number
itemsTotal: number
completedAt?: string
}
export default function LoeschfristenPage() {
const [activeTab, setActiveTab] = useState<'policies' | 'deadlines' | 'jobs' | 'manual'>('policies')
const [loading, setLoading] = useState(false)
const [processing, setProcessing] = useState(false)
// Mock data - in production, this comes from API
const retentionPolicies: RetentionPolicy[] = [
{
id: 'pol_1',
dataCategory: 'Nutzerkonten (inaktiv)',
retentionPeriod: '3 Jahre nach letzter Aktivitaet',
legalBasis: 'Art. 5 Abs. 1 lit. e DSGVO',
autoDelete: true,
lastRun: '2024-12-01',
nextRun: '2025-01-01',
itemsToDelete: 23
},
{
id: 'pol_2',
dataCategory: 'Consent-Nachweise',
retentionPeriod: '6 Jahre nach Widerruf',
legalBasis: 'Nachweispflicht',
autoDelete: true,
lastRun: '2024-12-01',
nextRun: '2025-01-01',
itemsToDelete: 0
},
{
id: 'pol_3',
dataCategory: 'System-Logs',
retentionPeriod: '90 Tage',
legalBasis: 'Berechtigtes Interesse (IT-Sicherheit)',
autoDelete: true,
lastRun: '2024-12-14',
nextRun: '2024-12-15',
itemsToDelete: 15420
},
{
id: 'pol_4',
dataCategory: 'Security-Logs',
retentionPeriod: '2 Jahre',
legalBasis: 'Berechtigtes Interesse (Sicherheit)',
autoDelete: true,
lastRun: '2024-12-01',
nextRun: '2025-01-01',
itemsToDelete: 0
},
{
id: 'pol_5',
dataCategory: 'Lernfortschrittsdaten',
retentionPeriod: 'Ende Schuljahr + 1 Jahr',
legalBasis: 'Vertragserfuellung',
autoDelete: false,
itemsToDelete: 45
},
{
id: 'pol_6',
dataCategory: 'KI-Verarbeitungsdaten',
retentionPeriod: 'Sofortige Loeschung',
legalBasis: 'Datenminimierung',
autoDelete: true,
lastRun: '2024-12-15',
nextRun: 'Kontinuierlich',
itemsToDelete: 0
},
]
const consentDeadlines: ConsentDeadline[] = [
{ id: 'dl_1', userId: 'usr_456', documentName: 'AGB', versionNumber: 'v2.1.0', deadlineAt: '2025-01-15', reminderCount: 2, daysRemaining: 20, status: 'pending' },
{ id: 'dl_2', userId: 'usr_789', documentName: 'Datenschutz', versionNumber: 'v3.0.0', deadlineAt: '2024-12-28', reminderCount: 3, daysRemaining: 3, status: 'pending' },
{ id: 'dl_3', userId: 'usr_012', documentName: 'AGB', versionNumber: 'v2.1.0', deadlineAt: '2024-12-10', reminderCount: 4, daysRemaining: -5, status: 'overdue' },
]
const deletionJobs: DeletionJob[] = [
{ id: 'job_1', dataCategory: 'System-Logs', scheduledAt: '2024-12-14T02:00:00', status: 'completed', itemsProcessed: 12500, itemsTotal: 12500, completedAt: '2024-12-14T02:15:00' },
{ id: 'job_2', dataCategory: 'Inaktive Sessions', scheduledAt: '2024-12-14T03:00:00', status: 'completed', itemsProcessed: 450, itemsTotal: 450, completedAt: '2024-12-14T03:02:00' },
{ id: 'job_3', dataCategory: 'System-Logs', scheduledAt: '2024-12-15T02:00:00', status: 'scheduled', itemsProcessed: 0, itemsTotal: 15420 },
]
async function triggerDeadlineProcessing() {
setProcessing(true)
try {
const token = localStorage.getItem('bp_admin_token')
const res = await fetch(`${API_BASE}/deadlines`, {
method: 'POST',
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
})
if (res.ok) {
alert('Deadline-Verarbeitung gestartet')
} else {
alert('Fehler bei der Verarbeitung')
}
} catch {
alert('Verbindungsfehler')
} finally {
setProcessing(false)
}
}
const tabs = [
{ id: 'policies', label: 'Aufbewahrungsfristen' },
{ id: 'deadlines', label: 'Consent-Deadlines' },
{ id: 'jobs', label: 'Loeschjobs' },
{ id: 'manual', label: 'Manuelle Loeschung' },
]
const getStatusBadge = (status: string) => {
switch (status) {
case 'completed':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">Abgeschlossen</span>
case 'running':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">Laeuft</span>
case 'scheduled':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Geplant</span>
case 'failed':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">Fehlgeschlagen</span>
case 'pending':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Ausstehend</span>
case 'overdue':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">Ueberfaellig</span>
default:
return null
}
}
return (
<div>
<PagePurpose
title="Loeschfristen & Datenaufbewahrung"
purpose="Verwaltung von Aufbewahrungsfristen, automatischen Loeschungen und Consent-Deadlines gemaess DSGVO Art. 5 (Speicherbegrenzung) und Art. 17 (Recht auf Loeschung)."
audience={['DSB', 'IT-Admin', 'Compliance Officer']}
gdprArticles={['Art. 5 Abs. 1 lit. e (Speicherbegrenzung)', 'Art. 17 (Recht auf Loeschung)']}
architecture={{
services: ['consent-service (Go)', 'cron-jobs'],
databases: ['PostgreSQL'],
}}
relatedPages={[
{ name: 'VVT', href: '/compliance/vvt', description: 'Verarbeitungsverzeichnis' },
{ name: 'DSR', href: '/compliance/dsr', description: 'Loeschanfragen' },
{ name: 'Einwilligungen', href: '/compliance/einwilligungen', description: 'Consent-Uebersicht' },
]}
collapsible={true}
defaultCollapsed={true}
/>
{/* Quick Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-2xl font-bold text-slate-900">{retentionPolicies.length}</div>
<div className="text-sm text-slate-500">Aufbewahrungsrichtlinien</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-2xl font-bold text-yellow-600">
{consentDeadlines.filter(d => d.status === 'pending').length}
</div>
<div className="text-sm text-slate-500">Offene Deadlines</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-2xl font-bold text-red-600">
{consentDeadlines.filter(d => d.status === 'overdue').length}
</div>
<div className="text-sm text-slate-500">Ueberfaellige</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-2xl font-bold text-purple-600">
{retentionPolicies.reduce((sum, p) => sum + (p.itemsToDelete || 0), 0).toLocaleString()}
</div>
<div className="text-sm text-slate-500">Zur Loeschung vorgemerkt</div>
</div>
</div>
{/* Tabs */}
<div className="mb-6">
<div className="flex gap-1 bg-slate-100 p-1 rounded-lg w-fit">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as typeof activeTab)}
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
activeTab === tab.id
? 'bg-white text-slate-900 shadow-sm'
: 'text-slate-600 hover:text-slate-900'
}`}
>
{tab.label}
</button>
))}
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200">
{/* Policies Tab */}
{activeTab === 'policies' && (
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-slate-900">Aufbewahrungsfristen</h2>
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium">
+ Neue Richtlinie
</button>
</div>
<div className="space-y-4">
{retentionPolicies.map((policy) => (
<div key={policy.id} className="border border-slate-200 rounded-lg p-4 hover:border-purple-300 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-grow">
<div className="flex items-center gap-3 mb-2">
<h3 className="font-semibold text-slate-900">{policy.dataCategory}</h3>
{policy.autoDelete ? (
<span className="px-2 py-0.5 bg-green-100 text-green-700 rounded text-xs">Auto-Loeschung</span>
) : (
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs">Manuell</span>
)}
{(policy.itemsToDelete || 0) > 0 && (
<span className="px-2 py-0.5 bg-orange-100 text-orange-700 rounded text-xs">
{policy.itemsToDelete} zur Loeschung
</span>
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-slate-500">Frist:</span>
<span className="ml-1 font-medium text-slate-700">{policy.retentionPeriod}</span>
</div>
<div>
<span className="text-slate-500">Rechtsgrundlage:</span>
<span className="ml-1 text-slate-600">{policy.legalBasis}</span>
</div>
{policy.lastRun && (
<div>
<span className="text-slate-500">Letzter Lauf:</span>
<span className="ml-1 text-slate-600">{policy.lastRun}</span>
</div>
)}
{policy.nextRun && (
<div>
<span className="text-slate-500">Naechster Lauf:</span>
<span className="ml-1 text-slate-600">{policy.nextRun}</span>
</div>
)}
</div>
</div>
<div className="flex gap-2">
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg">
Bearbeiten
</button>
{(policy.itemsToDelete || 0) > 0 && (
<button className="px-3 py-1.5 text-sm text-white bg-red-600 hover:bg-red-700 rounded-lg">
Jetzt loeschen
</button>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
{/* Deadlines Tab */}
{activeTab === 'deadlines' && (
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-slate-900">Consent-Deadlines</h2>
<button
onClick={triggerDeadlineProcessing}
disabled={processing}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium disabled:opacity-50"
>
{processing ? 'Verarbeite...' : 'Deadlines verarbeiten'}
</button>
</div>
<div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<p className="text-sm text-yellow-800">
Nutzer haben 30 Tage Zeit, neue Pflichtdokumente zu akzeptieren.
Nach Ablauf wird der Account gesperrt, bis die Zustimmung erteilt wird.
</p>
</div>
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Nutzer</th>
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Dokument</th>
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Deadline</th>
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Erinnerungen</th>
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Status</th>
<th className="text-right py-3 px-4 text-sm font-medium text-slate-500">Aktionen</th>
</tr>
</thead>
<tbody>
{consentDeadlines.map((deadline) => (
<tr key={deadline.id} className="border-b border-slate-100 hover:bg-slate-50">
<td className="py-3 px-4 font-mono text-sm">{deadline.userId}</td>
<td className="py-3 px-4">
<div>{deadline.documentName}</div>
<div className="text-xs text-slate-500">{deadline.versionNumber}</div>
</td>
<td className="py-3 px-4">
<div>{deadline.deadlineAt}</div>
<div className={`text-xs ${deadline.daysRemaining < 0 ? 'text-red-600' : deadline.daysRemaining <= 7 ? 'text-orange-600' : 'text-slate-500'}`}>
{deadline.daysRemaining < 0
? `${Math.abs(deadline.daysRemaining)} Tage ueberfaellig`
: `${deadline.daysRemaining} Tage verbleibend`}
</div>
</td>
<td className="py-3 px-4">
<span className="px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs">
{deadline.reminderCount} gesendet
</span>
</td>
<td className="py-3 px-4">{getStatusBadge(deadline.status)}</td>
<td className="py-3 px-4 text-right">
<button className="text-purple-600 hover:text-purple-700 text-sm font-medium mr-3">
Erinnerung senden
</button>
{deadline.status === 'overdue' && (
<button className="text-red-600 hover:text-red-700 text-sm font-medium">
Account sperren
</button>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Jobs Tab */}
{activeTab === 'jobs' && (
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-slate-900">Loeschjobs</h2>
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium">
+ Neuer Job
</button>
</div>
<div className="space-y-4">
{deletionJobs.map((job) => (
<div key={job.id} className="border border-slate-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<h3 className="font-medium text-slate-900">{job.dataCategory}</h3>
{getStatusBadge(job.status)}
</div>
<span className="text-sm text-slate-500">
Geplant: {new Date(job.scheduledAt).toLocaleString('de-DE')}
</span>
</div>
<div className="flex items-center gap-4">
<div className="flex-grow h-2 bg-slate-100 rounded-full overflow-hidden">
<div
className={`h-full ${job.status === 'completed' ? 'bg-green-500' : job.status === 'running' ? 'bg-blue-500' : 'bg-slate-300'}`}
style={{ width: `${job.itemsTotal > 0 ? (job.itemsProcessed / job.itemsTotal) * 100 : 0}%` }}
/>
</div>
<span className="text-sm text-slate-600 whitespace-nowrap">
{job.itemsProcessed.toLocaleString()} / {job.itemsTotal.toLocaleString()}
</span>
</div>
{job.completedAt && (
<div className="mt-2 text-xs text-slate-500">
Abgeschlossen: {new Date(job.completedAt).toLocaleString('de-DE')}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Manual Tab */}
{activeTab === 'manual' && (
<div className="p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-6">Manuelle Loeschung</h2>
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
<div className="flex gap-3">
<svg className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<div>
<h4 className="font-semibold text-red-900">Achtung: Manuelle Loeschung</h4>
<p className="text-sm text-red-800 mt-1">
Manuelle Loeschungen sind unwiderruflich. Stellen Sie sicher, dass keine gesetzlichen
Aufbewahrungsfristen verletzt werden und alle notwendigen Backups erstellt wurden.
</p>
</div>
</div>
</div>
<div className="space-y-6">
<div className="border border-slate-200 rounded-lg p-4">
<h3 className="font-medium text-slate-900 mb-3">Nutzer-Daten loeschen</h3>
<div className="flex gap-3">
<input
type="text"
placeholder="Nutzer-ID eingeben..."
className="flex-grow px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
<button className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium">
Daten loeschen
</button>
</div>
<p className="text-xs text-slate-500 mt-2">
Loescht alle personenbezogenen Daten eines Nutzers (Art. 17 DSGVO)
</p>
</div>
<div className="border border-slate-200 rounded-lg p-4">
<h3 className="font-medium text-slate-900 mb-3">Alte Logs bereinigen</h3>
<div className="flex gap-3">
<select className="px-3 py-2 border border-slate-300 rounded-lg text-sm">
<option value="system">System-Logs</option>
<option value="audit">Audit-Logs</option>
<option value="access">Zugriffs-Logs</option>
</select>
<input
type="number"
placeholder="Aelter als (Tage)"
className="w-40 px-3 py-2 border border-slate-300 rounded-lg text-sm"
defaultValue={90}
/>
<button className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium">
Logs bereinigen
</button>
</div>
</div>
</div>
</div>
)}
</div>
{/* Info */}
<div className="mt-6 bg-purple-50 border border-purple-200 rounded-xl p-4">
<div className="flex gap-3">
<svg className="w-5 h-5 text-purple-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="font-semibold text-purple-900">Speicherbegrenzung (Art. 5)</h4>
<p className="text-sm text-purple-800 mt-1">
Personenbezogene Daten duerfen nur so lange gespeichert werden, wie es fuer die Zwecke
erforderlich ist. Die automatische Loeschung stellt die Einhaltung dieser Vorgabe sicher.
</p>
</div>
</div>
</div>
</div>
)
}