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/dsr/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

712 lines
28 KiB
TypeScript

'use client'
/**
* DSR (Data Subject Requests) Admin Page
*
* GDPR Article 15-21 Request Management
*
* Migriert auf SDK API: /sdk/v1/dsgvo/dsr
*/
import { useEffect, useState, useCallback } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
interface DSRRequest {
id: string
tenant_id: string
namespace_id?: string
request_type: string // access, rectification, erasure, restriction, portability, objection
status: string // received, verified, in_progress, completed, rejected, extended
subject_name: string
subject_email: string
subject_identifier?: string
request_description: string
request_channel: string // email, form, phone, letter
received_at: string
verified_at?: string
verification_method?: string
deadline_at: string
extended_deadline_at?: string
extension_reason?: string
completed_at?: string
response_sent: boolean
response_sent_at?: string
response_method?: string
rejection_reason?: string
notes?: string
affected_systems?: string[]
assigned_to?: string
created_at: string
updated_at: string
}
interface DSRStats {
total: number
received: number
in_progress: number
completed: number
overdue: number
}
export default function DSRPage() {
const [requests, setRequests] = useState<DSRRequest[]>([])
const [stats, setStats] = useState<DSRStats | null>(null)
const [loading, setLoading] = useState(true)
const [selectedRequest, setSelectedRequest] = useState<DSRRequest | null>(null)
const [filter, setFilter] = useState<string>('all')
const [error, setError] = useState<string | null>(null)
const [showCreateModal, setShowCreateModal] = useState(false)
const [newRequest, setNewRequest] = useState({
request_type: 'access',
subject_name: '',
subject_email: '',
subject_identifier: '',
request_description: '',
request_channel: 'email',
notes: ''
})
useEffect(() => {
loadRequests()
}, [])
async function loadRequests() {
setLoading(true)
setError(null)
try {
const res = await fetch('/sdk/v1/dsgvo/dsr', {
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()
const allRequests = data.dsrs || []
setRequests(allRequests)
// Calculate stats
const now = new Date()
setStats({
total: allRequests.length,
received: allRequests.filter((r: DSRRequest) => r.status === 'received' || r.status === 'verified').length,
in_progress: allRequests.filter((r: DSRRequest) => r.status === 'in_progress').length,
completed: allRequests.filter((r: DSRRequest) => r.status === 'completed').length,
overdue: allRequests.filter((r: DSRRequest) => {
const deadline = r.extended_deadline_at ? new Date(r.extended_deadline_at) : new Date(r.deadline_at)
return deadline < now && r.status !== 'completed' && r.status !== 'rejected'
}).length,
})
} catch (err) {
console.error('Failed to load DSRs:', err)
setError('Fehler beim Laden der Anfragen')
} finally {
setLoading(false)
}
}
async function createRequest() {
try {
const res = await fetch('/sdk/v1/dsgvo/dsr', {
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(newRequest)
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
setShowCreateModal(false)
setNewRequest({
request_type: 'access',
subject_name: '',
subject_email: '',
subject_identifier: '',
request_description: '',
request_channel: 'email',
notes: ''
})
loadRequests()
} catch (err) {
console.error('Failed to create DSR:', err)
alert('Fehler beim Erstellen der Anfrage')
}
}
async function updateStatus(id: string, status: string) {
try {
const res = await fetch(`/sdk/v1/dsgvo/dsr/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
'X-User-ID': localStorage.getItem('bp_user_id') || '',
},
body: JSON.stringify({ status })
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
setSelectedRequest(null)
loadRequests()
} catch (err) {
console.error('Failed to update DSR:', err)
alert('Fehler beim Aktualisieren')
}
}
async function exportDSRs(format: 'csv' | 'json') {
try {
const res = await fetch(`/sdk/v1/dsgvo/export/dsr?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 = `dsr-export.${format}`
a.click()
window.URL.revokeObjectURL(url)
} catch (err) {
console.error('Export failed:', err)
alert('Export fehlgeschlagen')
}
}
// Get status badge color
const getStatusColor = (status: string) => {
switch (status) {
case 'received':
return 'bg-slate-100 text-slate-800'
case 'verified':
return 'bg-yellow-100 text-yellow-800'
case 'in_progress':
return 'bg-blue-100 text-blue-800'
case 'completed':
return 'bg-green-100 text-green-800'
case 'rejected':
return 'bg-red-100 text-red-800'
case 'extended':
return 'bg-orange-100 text-orange-800'
default:
return 'bg-slate-100 text-slate-800'
}
}
const getStatusLabel = (status: string) => {
const labels: Record<string, string> = {
'received': 'Eingegangen',
'verified': 'Verifiziert',
'in_progress': 'In Bearbeitung',
'completed': 'Abgeschlossen',
'rejected': 'Abgelehnt',
'extended': 'Verlängert'
}
return labels[status] || status
}
// Get request type label
const getTypeLabel = (type: string) => {
const labels: Record<string, string> = {
'access': 'Auskunft (Art. 15)',
'rectification': 'Berichtigung (Art. 16)',
'erasure': 'Löschung (Art. 17)',
'restriction': 'Einschränkung (Art. 18)',
'portability': 'Datenübertragbarkeit (Art. 20)',
'objection': 'Widerspruch (Art. 21)',
}
return labels[type] || type
}
const getChannelLabel = (channel: string) => {
const labels: Record<string, string> = {
'email': 'E-Mail',
'form': 'Formular',
'phone': 'Telefon',
'letter': 'Brief',
}
return labels[channel] || channel
}
// Filter requests
const filteredRequests = requests.filter(r => {
if (filter === 'all') return true
if (filter === 'overdue') {
const deadline = r.extended_deadline_at ? new Date(r.extended_deadline_at) : new Date(r.deadline_at)
return deadline < new Date() && r.status !== 'completed' && r.status !== 'rejected'
}
if (filter === 'open') {
return r.status === 'received' || r.status === 'verified'
}
return r.status === filter
})
// Format date
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})
}
// Check if overdue
const isOverdue = (request: DSRRequest) => {
const deadline = request.extended_deadline_at ? new Date(request.extended_deadline_at) : new Date(request.deadline_at)
return deadline < new Date() && request.status !== 'completed' && request.status !== 'rejected'
}
// Calculate days until deadline
const daysUntilDeadline = (request: DSRRequest) => {
const deadline = request.extended_deadline_at ? new Date(request.extended_deadline_at) : new Date(request.deadline_at)
const now = new Date()
const diff = Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24))
return diff
}
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-slate-500">Lade Anfragen...</div>
</div>
)
}
return (
<div>
{/* Page Purpose */}
<PagePurpose
title="Datenschutzanfragen (DSR)"
purpose="Verwalten Sie alle Betroffenenanfragen nach DSGVO Art. 15-21. Hier bearbeiten Sie Auskunfts-, Lösch- und Berichtigungsanfragen mit automatischer Fristüberwachung."
audience={['DSB', 'Compliance Officer', 'Support']}
gdprArticles={[
'Art. 15 (Auskunftsrecht)',
'Art. 16 (Berichtigung)',
'Art. 17 (Löschung)',
'Art. 18 (Einschränkung)',
'Art. 20 (Datenübertragbarkeit)',
'Art. 21 (Widerspruch)',
]}
architecture={{
services: ['AI Compliance SDK (Go)', 'PostgreSQL'],
databases: ['PostgreSQL'],
}}
relatedPages={[
{ name: 'VVT', href: '/dsgvo/vvt', description: 'Verarbeitungsverzeichnis' },
{ name: 'Löschfristen', href: '/dsgvo/loeschfristen', description: 'Aufbewahrungsfristen' },
{ name: 'TOM', href: '/dsgvo/tom', description: 'Technische Maßnahmen' },
]}
collapsible={true}
defaultCollapsed={true}
/>
{/* Error Message */}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg mb-6">
{error}
</div>
)}
{/* Header Actions */}
<div className="flex items-center justify-between mb-6">
<div></div>
<div className="flex items-center gap-3">
<button
onClick={() => exportDSRs('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={() => exportDSRs('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 Anfrage
</button>
</div>
</div>
{/* Stats */}
{stats && (
<div className="grid grid-cols-2 md:grid-cols-5 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">{stats.total}</div>
<div className="text-sm text-slate-500">Gesamt</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-2xl font-bold text-yellow-600">{stats.received}</div>
<div className="text-sm text-slate-500">Offen</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-2xl font-bold text-blue-600">{stats.in_progress}</div>
<div className="text-sm text-slate-500">In Bearbeitung</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-2xl font-bold text-green-600">{stats.completed}</div>
<div className="text-sm text-slate-500">Abgeschlossen</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className={`text-2xl font-bold ${stats.overdue > 0 ? 'text-red-600' : 'text-slate-400'}`}>
{stats.overdue}
</div>
<div className="text-sm text-slate-500">Überfällig</div>
</div>
</div>
)}
{/* Filter Tabs */}
<div className="flex gap-2 mb-4 overflow-x-auto">
{[
{ value: 'all', label: 'Alle' },
{ value: 'open', label: 'Offen' },
{ value: 'in_progress', label: 'In Bearbeitung' },
{ value: 'completed', label: 'Abgeschlossen' },
{ value: 'overdue', label: 'Überfällig' },
].map((tab) => (
<button
key={tab.value}
onClick={() => setFilter(tab.value)}
className={`px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors ${
filter === tab.value
? 'bg-primary-600 text-white'
: 'bg-white text-slate-700 border border-slate-200 hover:bg-slate-50'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Requests Table */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Typ</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Betroffener</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Status</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Kanal</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Frist</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Aktionen</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredRequests.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-8 text-center text-slate-500">
Keine Anfragen gefunden
</td>
</tr>
) : (
filteredRequests.map((request) => (
<tr key={request.id} className={isOverdue(request) ? 'bg-red-50' : ''}>
<td className="px-4 py-3 text-sm text-slate-700">{getTypeLabel(request.request_type)}</td>
<td className="px-4 py-3">
<div className="text-sm text-slate-900">{request.subject_name}</div>
<div className="text-xs text-slate-500">{request.subject_email}</div>
</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(request.status)}`}>
{getStatusLabel(request.status)}
</span>
</td>
<td className="px-4 py-3 text-sm text-slate-600">
{getChannelLabel(request.request_channel)}
</td>
<td className="px-4 py-3">
<div className={`text-sm ${isOverdue(request) ? 'text-red-600 font-medium' : 'text-slate-700'}`}>
{formatDate(request.extended_deadline_at || request.deadline_at)}
</div>
{request.status !== 'completed' && request.status !== 'rejected' && (
<div className={`text-xs ${daysUntilDeadline(request) < 0 ? 'text-red-500' : daysUntilDeadline(request) <= 7 ? 'text-orange-500' : 'text-slate-400'}`}>
{daysUntilDeadline(request) < 0
? `${Math.abs(daysUntilDeadline(request))} Tage überfällig`
: `${daysUntilDeadline(request)} Tage verbleibend`}
</div>
)}
</td>
<td className="px-4 py-3">
<button
onClick={() => setSelectedRequest(request)}
className="text-primary-600 hover:text-primary-700 text-sm font-medium"
>
Details
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
{/* Detail Modal */}
{selectedRequest && (
<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="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
<h3 className="font-semibold text-slate-900">
{getTypeLabel(selectedRequest.request_type)}
</h3>
<button
onClick={() => setSelectedRequest(null)}
className="text-slate-400 hover:text-slate-600"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-6 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<div className="text-sm text-slate-500">Betroffener</div>
<div className="font-medium text-slate-900">{selectedRequest.subject_name}</div>
<div className="text-sm text-slate-500">{selectedRequest.subject_email}</div>
</div>
<div>
<div className="text-sm text-slate-500">Status</div>
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(selectedRequest.status)}`}>
{getStatusLabel(selectedRequest.status)}
</span>
</div>
<div>
<div className="text-sm text-slate-500">Eingegangen am</div>
<div className="font-medium text-slate-900">{formatDate(selectedRequest.received_at)}</div>
</div>
<div>
<div className="text-sm text-slate-500">Frist</div>
<div className={`font-medium ${isOverdue(selectedRequest) ? 'text-red-600' : 'text-slate-900'}`}>
{formatDate(selectedRequest.extended_deadline_at || selectedRequest.deadline_at)}
{selectedRequest.extended_deadline_at && (
<span className="text-xs text-orange-600 ml-2">(verlängert)</span>
)}
</div>
</div>
<div>
<div className="text-sm text-slate-500">Kanal</div>
<div className="font-medium text-slate-900">{getChannelLabel(selectedRequest.request_channel)}</div>
</div>
{selectedRequest.subject_identifier && (
<div>
<div className="text-sm text-slate-500">Kunden-ID</div>
<div className="font-medium text-slate-900 font-mono">{selectedRequest.subject_identifier}</div>
</div>
)}
</div>
{selectedRequest.request_description && (
<div>
<div className="text-sm text-slate-500 mb-1">Beschreibung</div>
<div className="bg-slate-50 rounded-lg p-3 text-sm text-slate-700">
{selectedRequest.request_description}
</div>
</div>
)}
{selectedRequest.notes && (
<div>
<div className="text-sm text-slate-500 mb-1">Notizen</div>
<div className="bg-slate-50 rounded-lg p-3 text-sm text-slate-700">
{selectedRequest.notes}
</div>
</div>
)}
{selectedRequest.affected_systems && selectedRequest.affected_systems.length > 0 && (
<div>
<div className="text-sm text-slate-500 mb-1">Betroffene Systeme</div>
<div className="flex flex-wrap gap-2">
{selectedRequest.affected_systems.map((sys, idx) => (
<span key={idx} className="px-2 py-1 bg-slate-100 text-slate-700 text-xs rounded">
{sys}
</span>
))}
</div>
</div>
)}
<div className="flex gap-2 pt-4 border-t border-slate-200">
{selectedRequest.status === 'received' && (
<button
onClick={() => updateStatus(selectedRequest.id, 'verified')}
className="px-4 py-2 bg-yellow-600 text-white rounded-lg text-sm font-medium hover:bg-yellow-700"
>
Verifizieren
</button>
)}
{(selectedRequest.status === 'received' || selectedRequest.status === 'verified') && (
<button
onClick={() => updateStatus(selectedRequest.id, 'in_progress')}
className="px-4 py-2 bg-blue-600 text-white rounded-lg text-sm font-medium hover:bg-blue-700"
>
Bearbeitung starten
</button>
)}
{selectedRequest.status === 'in_progress' && (
<>
<button
onClick={() => updateStatus(selectedRequest.id, 'completed')}
className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700"
>
Abschließen
</button>
<button
onClick={() => updateStatus(selectedRequest.id, 'rejected')}
className="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-medium hover:bg-red-700"
>
Ablehnen
</button>
</>
)}
</div>
</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 Anfrage erfassen</h3>
</div>
<div className="p-6 space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Typ *</label>
<select
value={newRequest.request_type}
onChange={(e) => setNewRequest({ ...newRequest, request_type: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
>
<option value="access">Auskunft (Art. 15)</option>
<option value="rectification">Berichtigung (Art. 16)</option>
<option value="erasure">Löschung (Art. 17)</option>
<option value="restriction">Einschränkung (Art. 18)</option>
<option value="portability">Datenübertragbarkeit (Art. 20)</option>
<option value="objection">Widerspruch (Art. 21)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Kanal</label>
<select
value={newRequest.request_channel}
onChange={(e) => setNewRequest({ ...newRequest, request_channel: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
>
<option value="email">E-Mail</option>
<option value="form">Formular</option>
<option value="phone">Telefon</option>
<option value="letter">Brief</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">Name des Betroffenen *</label>
<input
type="text"
value={newRequest.subject_name}
onChange={(e) => setNewRequest({ ...newRequest, subject_name: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="Max Mustermann"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">E-Mail *</label>
<input
type="email"
value={newRequest.subject_email}
onChange={(e) => setNewRequest({ ...newRequest, subject_email: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="max@example.com"
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Kunden-ID (optional)</label>
<input
type="text"
value={newRequest.subject_identifier}
onChange={(e) => setNewRequest({ ...newRequest, subject_identifier: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2"
placeholder="z.B. CUST-12345"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung der Anfrage</label>
<textarea
value={newRequest.request_description}
onChange={(e) => setNewRequest({ ...newRequest, request_description: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2 h-24"
placeholder="Was genau wird angefragt..."
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Interne Notizen</label>
<textarea
value={newRequest.notes}
onChange={(e) => setNewRequest({ ...newRequest, notes: e.target.value })}
className="w-full border border-slate-300 rounded-lg px-3 py-2 h-20"
placeholder="Interne Anmerkungen..."
/>
</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={createRequest}
disabled={!newRequest.subject_name || !newRequest.subject_email}
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"
>
Anfrage erfassen
</button>
</div>
</div>
</div>
)}
{/* Info Box */}
<div className="mt-6 bg-purple-50 border border-purple-200 rounded-xl p-4">
<h4 className="font-semibold text-purple-900 mb-2">DSGVO-Fristen</h4>
<ul className="text-sm text-purple-800 space-y-1">
<li>Art. 15 (Auskunft): 1 Monat, verlängerbar auf 3 Monate</li>
<li>Art. 16 (Berichtigung): Unverzüglich</li>
<li>Art. 17 (Löschung): Unverzüglich</li>
<li>Art. 18 (Einschränkung): Unverzüglich</li>
<li>Art. 20 (Datenübertragbarkeit): 1 Monat</li>
<li>Art. 21 (Widerspruch): Unverzüglich</li>
</ul>
</div>
</div>
)
}