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>
512 lines
22 KiB
TypeScript
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>
|
|
)
|
|
}
|