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)/dsgvo/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

543 lines
22 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* Löschfristen - Data Retention Management
*
* Art. 17 DSGVO - Recht auf Löschung
* Art. 5 Abs. 1 lit. e DSGVO - Speicherbegrenzung
*
* Migriert auf SDK API: /sdk/v1/dsgvo/retention-policies
*/
import { useState, useEffect } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
interface RetentionPolicy {
id: string
tenant_id: string
namespace_id?: string
name: string
description: string
data_category: string
retention_period_days: number
retention_period_text: string
legal_basis: string
legal_reference?: string
deletion_method: string // automatic, manual, anonymization
deletion_procedure?: string
exception_criteria?: string
applicable_systems?: string[]
responsible_person: string
responsible_department: string
status: string // draft, active, archived
last_review_at?: string
next_review_at?: string
created_at: string
updated_at: string
}
export default function LoeschfristenPage() {
const [policies, setPolicies] = useState<RetentionPolicy[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [showCreateModal, setShowCreateModal] = useState(false)
const [newPolicy, setNewPolicy] = useState({
name: '',
description: '',
data_category: '',
retention_period_days: 365,
retention_period_text: '1 Jahr',
legal_basis: 'legal_requirement',
legal_reference: '',
deletion_method: 'automatic',
deletion_procedure: '',
responsible_person: '',
responsible_department: '',
status: 'draft'
})
useEffect(() => {
loadPolicies()
}, [])
async function loadPolicies() {
setLoading(true)
setError(null)
try {
const res = await fetch('/sdk/v1/dsgvo/retention-policies', {
headers: {
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
'X-User-ID': localStorage.getItem('bp_user_id') || '',
}
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const data = await res.json()
setPolicies(data.policies || [])
} catch (err) {
console.error('Failed to load retention policies:', err)
setError('Fehler beim Laden der Löschfristen')
} finally {
setLoading(false)
}
}
async function createPolicy() {
try {
const res = await fetch('/sdk/v1/dsgvo/retention-policies', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
'X-User-ID': localStorage.getItem('bp_user_id') || '',
},
body: JSON.stringify(newPolicy)
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
setShowCreateModal(false)
setNewPolicy({
name: '',
description: '',
data_category: '',
retention_period_days: 365,
retention_period_text: '1 Jahr',
legal_basis: 'legal_requirement',
legal_reference: '',
deletion_method: 'automatic',
deletion_procedure: '',
responsible_person: '',
responsible_department: '',
status: 'draft'
})
loadPolicies()
} catch (err) {
console.error('Failed to create policy:', err)
alert('Fehler beim Erstellen der Löschfrist')
}
}
async function exportPolicies(format: 'csv' | 'json') {
try {
const res = await fetch(`/sdk/v1/dsgvo/export/retention?format=${format}`, {
headers: {
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
'X-User-ID': localStorage.getItem('bp_user_id') || '',
}
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const blob = await res.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `loeschfristen-export.${format}`
a.click()
window.URL.revokeObjectURL(url)
} catch (err) {
console.error('Export failed:', err)
alert('Export fehlgeschlagen')
}
}
const getStatusBadge = (status: string) => {
switch (status) {
case 'active':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">Aktiv</span>
case 'draft':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Entwurf</span>
case 'archived':
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-600">Archiviert</span>
default:
return null
}
}
const getDeletionMethodBadge = (method: string) => {
switch (method) {
case 'automatic':
return <span className="px-2 py-0.5 bg-green-100 text-green-700 rounded text-xs">Auto-Löschung</span>
case 'manual':
return <span className="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">Manuell</span>
case 'anonymization':
return <span className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs">Anonymisierung</span>
default:
return null
}
}
const getLegalBasisLabel = (basis: string) => {
const labels: Record<string, string> = {
'legal_requirement': 'Gesetzliche Pflicht',
'consent': 'Einwilligung',
'legitimate_interest': 'Berechtigtes Interesse',
'contract': 'Vertragserfüllung',
}
return labels[basis] || basis
}
// Group policies by status
const activePolicies = policies.filter(p => p.status === 'active')
const draftPolicies = policies.filter(p => p.status === 'draft')
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-slate-500">Lade Löschfristen...</div>
</div>
)
}
return (
<div>
<PagePurpose
title="Löschfristen & Datenaufbewahrung"
purpose="Verwaltung von Aufbewahrungsfristen und automatischen Löschungen gemäß DSGVO Art. 5 (Speicherbegrenzung) und Art. 17 (Recht auf Löschung)."
audience={['DSB', 'IT-Admin', 'Compliance Officer']}
gdprArticles={['Art. 5 Abs. 1 lit. e (Speicherbegrenzung)', 'Art. 17 (Recht auf Löschung)']}
architecture={{
services: ['AI Compliance SDK (Go)', 'PostgreSQL'],
databases: ['PostgreSQL'],
}}
relatedPages={[
{ name: 'VVT', href: '/dsgvo/vvt', description: 'Verarbeitungsverzeichnis' },
{ name: 'DSR', href: '/dsgvo/dsr', description: 'Löschanfragen' },
{ name: 'TOM', href: '/dsgvo/tom', description: 'Technische Maßnahmen' },
]}
collapsible={true}
defaultCollapsed={true}
/>
{error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-xl p-4 text-red-700">
{error}
</div>
)}
{/* Header Actions */}
<div className="flex items-center justify-between mb-6">
<div></div>
<div className="flex items-center gap-3">
<button
onClick={() => exportPolicies('csv')}
className="px-4 py-2 border border-slate-300 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50"
>
CSV Export
</button>
<button
onClick={() => exportPolicies('json')}
className="px-4 py-2 border border-slate-300 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50"
>
JSON Export
</button>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700"
>
+ Neue Löschfrist
</button>
</div>
</div>
{/* 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">{policies.length}</div>
<div className="text-sm text-slate-500">Löschfristen gesamt</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-2xl font-bold text-green-600">{activePolicies.length}</div>
<div className="text-sm text-slate-500">Aktive Richtlinien</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-2xl font-bold text-yellow-600">{draftPolicies.length}</div>
<div className="text-sm text-slate-500">Entwürfe</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-2xl font-bold text-purple-600">
{policies.filter(p => p.deletion_method === 'automatic').length}
</div>
<div className="text-sm text-slate-500">Auto-Löschung</div>
</div>
</div>
{/* Policies List */}
{policies.length === 0 ? (
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center">
<div className="text-slate-400 text-4xl mb-4">🗑</div>
<h3 className="text-lg font-medium text-slate-800 mb-2">Keine Löschfristen definiert</h3>
<p className="text-slate-500 mb-4">Legen Sie Aufbewahrungsfristen für verschiedene Datenkategorien an.</p>
<button
onClick={() => setShowCreateModal(true)}
className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700"
>
Erste Löschfrist anlegen
</button>
</div>
) : (
<div className="bg-white rounded-xl border border-slate-200">
<div className="p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-6">Aufbewahrungsfristen</h2>
<div className="space-y-4">
{policies.map((policy) => (
<div key={policy.id} className="border border-slate-200 rounded-lg p-4 hover:border-primary-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.name}</h3>
{getStatusBadge(policy.status)}
{getDeletionMethodBadge(policy.deletion_method)}
</div>
{policy.description && (
<p className="text-sm text-slate-600 mb-3">{policy.description}</p>
)}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-slate-500">Datenkategorie:</span>
<span className="ml-1 font-medium text-slate-700">{policy.data_category}</span>
</div>
<div>
<span className="text-slate-500">Frist:</span>
<span className="ml-1 font-medium text-slate-700">{policy.retention_period_text}</span>
<span className="text-slate-400 ml-1">({policy.retention_period_days} Tage)</span>
</div>
<div>
<span className="text-slate-500">Rechtsgrundlage:</span>
<span className="ml-1 text-slate-600">{getLegalBasisLabel(policy.legal_basis)}</span>
</div>
{policy.legal_reference && (
<div>
<span className="text-slate-500">Referenz:</span>
<span className="ml-1 text-slate-600 font-mono text-xs">{policy.legal_reference}</span>
</div>
)}
</div>
<div className="mt-3 flex flex-wrap gap-4 text-xs text-slate-500">
{policy.responsible_person && (
<span>Verantwortlich: {policy.responsible_person}</span>
)}
{policy.responsible_department && (
<span>Abteilung: {policy.responsible_department}</span>
)}
{policy.last_review_at && (
<span>Letzte Prüfung: {new Date(policy.last_review_at).toLocaleDateString('de-DE')}</span>
)}
{policy.next_review_at && (
<span>Nächste Prüfung: {new Date(policy.next_review_at).toLocaleDateString('de-DE')}</span>
)}
</div>
{policy.applicable_systems && policy.applicable_systems.length > 0 && (
<div className="mt-3 flex flex-wrap gap-2">
{policy.applicable_systems.map((sys, idx) => (
<span key={idx} className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs">
{sys}
</span>
))}
</div>
)}
</div>
</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 dürfen nur so lange gespeichert werden, wie es für die Zwecke
erforderlich ist. Die automatische Löschung stellt die Einhaltung dieser Vorgabe sicher.
</p>
</div>
</div>
</div>
{/* Create Modal */}
{showCreateModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-slate-200">
<h3 className="text-lg font-semibold text-slate-900">Neue Löschfrist anlegen</h3>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Name *</label>
<input
type="text"
value={newPolicy.name}
onChange={(e) => setNewPolicy({ ...newPolicy, name: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="z.B. Aufbewahrung Nutzerkonten"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
<textarea
value={newPolicy.description}
onChange={(e) => setNewPolicy({ ...newPolicy, description: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2 h-20"
placeholder="Beschreibung der Löschfrist..."
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Datenkategorie *</label>
<input
type="text"
value={newPolicy.data_category}
onChange={(e) => setNewPolicy({ ...newPolicy, data_category: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="z.B. Nutzerdaten, Logs, Rechnungen"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Status</label>
<select
value={newPolicy.status}
onChange={(e) => setNewPolicy({ ...newPolicy, status: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
>
<option value="draft">Entwurf</option>
<option value="active">Aktiv</option>
<option value="archived">Archiviert</option>
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Aufbewahrungsfrist (Tage)</label>
<input
type="number"
value={newPolicy.retention_period_days}
onChange={(e) => setNewPolicy({ ...newPolicy, retention_period_days: parseInt(e.target.value) || 0 })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="365"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Aufbewahrungsfrist (Text)</label>
<input
type="text"
value={newPolicy.retention_period_text}
onChange={(e) => setNewPolicy({ ...newPolicy, retention_period_text: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="z.B. 3 Jahre, 10 Jahre nach Vertragsende"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Rechtsgrundlage</label>
<select
value={newPolicy.legal_basis}
onChange={(e) => setNewPolicy({ ...newPolicy, legal_basis: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
>
<option value="legal_requirement">Gesetzliche Pflicht</option>
<option value="consent">Einwilligung</option>
<option value="legitimate_interest">Berechtigtes Interesse</option>
<option value="contract">Vertragserfüllung</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Gesetzliche Referenz</label>
<input
type="text"
value={newPolicy.legal_reference}
onChange={(e) => setNewPolicy({ ...newPolicy, legal_reference: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="z.B. § 147 AO, § 257 HGB"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Löschmethode</label>
<select
value={newPolicy.deletion_method}
onChange={(e) => setNewPolicy({ ...newPolicy, deletion_method: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
>
<option value="automatic">Automatische Löschung</option>
<option value="manual">Manuelle Löschung</option>
<option value="anonymization">Anonymisierung</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Löschprozedur</label>
<input
type="text"
value={newPolicy.deletion_procedure}
onChange={(e) => setNewPolicy({ ...newPolicy, deletion_procedure: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="z.B. Cron-Job, Skript"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Verantwortliche Person</label>
<input
type="text"
value={newPolicy.responsible_person}
onChange={(e) => setNewPolicy({ ...newPolicy, responsible_person: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="Name"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Abteilung</label>
<input
type="text"
value={newPolicy.responsible_department}
onChange={(e) => setNewPolicy({ ...newPolicy, responsible_department: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="z.B. IT, Datenschutz"
/>
</div>
</div>
</div>
<div className="p-6 border-t border-slate-200 flex justify-end gap-3">
<button
onClick={() => setShowCreateModal(false)}
className="px-4 py-2 border border-slate-300 rounded-lg text-sm font-medium text-slate-700 hover:bg-slate-50"
>
Abbrechen
</button>
<button
onClick={createPolicy}
disabled={!newPolicy.name || !newPolicy.data_category}
className="px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Löschfrist anlegen
</button>
</div>
</div>
</div>
)}
</div>
)
}