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>
335 lines
14 KiB
TypeScript
335 lines
14 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* VVT - Verarbeitungsverzeichnis
|
|
*
|
|
* Art. 30 DSGVO - Verzeichnis von Verarbeitungstaetigkeiten
|
|
*/
|
|
|
|
import { useState } from 'react'
|
|
import { PagePurpose } from '@/components/common/PagePurpose'
|
|
|
|
interface ProcessingActivity {
|
|
id: string
|
|
name: string
|
|
purpose: string
|
|
legalBasis: string
|
|
legalBasisDetail: string
|
|
categories: string[]
|
|
recipients: string[]
|
|
thirdCountryTransfer: boolean
|
|
thirdCountryDetails?: string
|
|
retentionPeriod: string
|
|
technicalMeasures: string[]
|
|
lastReview: string
|
|
status: 'active' | 'inactive' | 'review_needed'
|
|
}
|
|
|
|
export default function VVTPage() {
|
|
const [expandedActivity, setExpandedActivity] = useState<string | null>('user_accounts')
|
|
const [filterStatus, setFilterStatus] = useState<string>('all')
|
|
|
|
const processingActivities: ProcessingActivity[] = [
|
|
{
|
|
id: 'user_accounts',
|
|
name: 'Nutzerkontenverwaltung',
|
|
purpose: 'Bereitstellung und Verwaltung von Benutzerkonten fuer die Plattform-Nutzung',
|
|
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO',
|
|
legalBasisDetail: 'Vertragserfuellung - Notwendig zur Bereitstellung des Dienstes',
|
|
categories: ['Name', 'E-Mail-Adresse', 'Passwort (gehasht)', 'Profilbild (optional)'],
|
|
recipients: ['Keine externen Empfaenger'],
|
|
thirdCountryTransfer: false,
|
|
retentionPeriod: '3 Jahre nach Kontolöschung',
|
|
technicalMeasures: ['Verschluesselung', 'Zugriffskontrolle', 'Audit-Logging'],
|
|
lastReview: '2024-12-01',
|
|
status: 'active'
|
|
},
|
|
{
|
|
id: 'consent_management',
|
|
name: 'Einwilligungsverwaltung',
|
|
purpose: 'Verwaltung und Dokumentation von Einwilligungen gemaess DSGVO',
|
|
legalBasis: 'Art. 6 Abs. 1 lit. c DSGVO',
|
|
legalBasisDetail: 'Rechtliche Verpflichtung - Nachweis der Einwilligung',
|
|
categories: ['Benutzer-ID', 'Einwilligungstyp', 'Zeitstempel', 'IP-Adresse', 'Version'],
|
|
recipients: ['DSB (Datenschutzbeauftragter)', 'Aufsichtsbehoerden bei Anfrage'],
|
|
thirdCountryTransfer: false,
|
|
retentionPeriod: '6 Jahre nach Widerruf (Nachweispflicht)',
|
|
technicalMeasures: ['Unveraenderbarkeit', 'Zeitstempel', 'Audit-Trail'],
|
|
lastReview: '2024-12-01',
|
|
status: 'active'
|
|
},
|
|
{
|
|
id: 'learning_analytics',
|
|
name: 'Lernfortschrittsanalyse',
|
|
purpose: 'Analyse des Lernfortschritts zur Verbesserung der Lernerfahrung',
|
|
legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO',
|
|
legalBasisDetail: 'Einwilligung - Nutzer stimmt der Analyse explizit zu',
|
|
categories: ['Benutzer-ID', 'Lernaktivitaeten', 'Testergebnisse', 'Zeitaufwand'],
|
|
recipients: ['Lehrer (aggregiert)', 'Eltern (mit Einwilligung)'],
|
|
thirdCountryTransfer: false,
|
|
retentionPeriod: 'Bis zum Ende des Schuljahres + 1 Jahr',
|
|
technicalMeasures: ['Pseudonymisierung', 'Verschluesselung', 'Zugriffsbeschraenkung'],
|
|
lastReview: '2024-11-15',
|
|
status: 'active'
|
|
},
|
|
{
|
|
id: 'ai_processing',
|
|
name: 'KI-gestuetzte Verarbeitung',
|
|
purpose: 'Automatische Korrektur und Feedback-Generierung mittels KI',
|
|
legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO',
|
|
legalBasisDetail: 'Einwilligung - Explizite Zustimmung zur KI-Verarbeitung',
|
|
categories: ['Benutzer-ID', 'Eingabetexte', 'Generierte Bewertungen'],
|
|
recipients: ['Ollama (lokal)', 'Optional: Cloud-LLM (mit Einwilligung)'],
|
|
thirdCountryTransfer: true,
|
|
thirdCountryDetails: 'OpenAI (USA) - nur bei expliziter Einwilligung, Standardvertragsklauseln',
|
|
retentionPeriod: 'Sofortige Loeschung nach Verarbeitung (keine Speicherung)',
|
|
technicalMeasures: ['Anonymisierung wo moeglich', 'Keine Speicherung bei Drittanbietern'],
|
|
lastReview: '2024-12-01',
|
|
status: 'review_needed'
|
|
},
|
|
{
|
|
id: 'support_requests',
|
|
name: 'Support-Anfragen',
|
|
purpose: 'Bearbeitung von Support- und Hilfe-Anfragen',
|
|
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO',
|
|
legalBasisDetail: 'Vertragserfuellung - Teil des Service-Angebots',
|
|
categories: ['Name', 'E-Mail', 'Anfrage-Inhalt', 'Anhaenge'],
|
|
recipients: ['Support-Team', 'Entwickler (bei technischen Problemen)'],
|
|
thirdCountryTransfer: false,
|
|
retentionPeriod: '2 Jahre nach Abschluss des Tickets',
|
|
technicalMeasures: ['Zugriffskontrolle', 'Verschluesselung'],
|
|
lastReview: '2024-10-01',
|
|
status: 'active'
|
|
},
|
|
{
|
|
id: 'newsletter',
|
|
name: 'Newsletter-Versand',
|
|
purpose: 'Information ueber Updates, Features und relevante Bildungsthemen',
|
|
legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO',
|
|
legalBasisDetail: 'Einwilligung - Double-Opt-In Verfahren',
|
|
categories: ['E-Mail-Adresse', 'Anrede', 'Praeferenzen'],
|
|
recipients: ['E-Mail-Provider (Mailpit/SMTP)'],
|
|
thirdCountryTransfer: false,
|
|
retentionPeriod: 'Bis zum Widerruf',
|
|
technicalMeasures: ['Abmelde-Link in jeder E-Mail', 'Verschluesselung'],
|
|
lastReview: '2024-11-01',
|
|
status: 'active'
|
|
},
|
|
{
|
|
id: 'logging',
|
|
name: 'System-Logging',
|
|
purpose: 'Sicherheit, Fehleranalyse und Betrieb der Plattform',
|
|
legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO',
|
|
legalBasisDetail: 'Berechtigtes Interesse - Sicherheit und Betrieb',
|
|
categories: ['IP-Adresse', 'Zeitstempel', 'Anfrage-Details', 'User-Agent'],
|
|
recipients: ['IT-Administratoren', 'Bei Sicherheitsvorfaellen: Behoerden'],
|
|
thirdCountryTransfer: false,
|
|
retentionPeriod: '90 Tage (Standard-Logs), 2 Jahre (Security-Logs)',
|
|
technicalMeasures: ['IP-Anonymisierung nach 7 Tagen', 'Zugriffsbeschraenkung'],
|
|
lastReview: '2024-12-01',
|
|
status: 'active'
|
|
},
|
|
]
|
|
|
|
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 'inactive':
|
|
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-600">Inaktiv</span>
|
|
case 'review_needed':
|
|
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Pruefung erforderlich</span>
|
|
default:
|
|
return null
|
|
}
|
|
}
|
|
|
|
const filteredActivities = filterStatus === 'all'
|
|
? processingActivities
|
|
: processingActivities.filter(a => a.status === filterStatus)
|
|
|
|
return (
|
|
<div>
|
|
<PagePurpose
|
|
title="Verarbeitungsverzeichnis (VVT)"
|
|
purpose="Verzeichnis aller Verarbeitungstaetigkeiten gemaess Art. 30 DSGVO. Dokumentiert Zweck, Rechtsgrundlage, Kategorien und Loeschfristen."
|
|
audience={['DSB', 'Auditoren', 'Aufsichtsbehoerden']}
|
|
gdprArticles={['Art. 30 (Verzeichnis von Verarbeitungstaetigkeiten)']}
|
|
architecture={{
|
|
services: ['consent-service (Go)', 'backend (Python)'],
|
|
databases: ['PostgreSQL'],
|
|
}}
|
|
relatedPages={[
|
|
{ name: 'DSMS', href: '/compliance/dsms', description: 'Datenschutz-Management-System' },
|
|
{ name: 'TOMs', href: '/compliance/tom', description: 'Technische Massnahmen' },
|
|
{ name: 'DSFA', href: '/compliance/dsfa', description: 'Datenschutz-Folgenabschaetzung' },
|
|
]}
|
|
collapsible={true}
|
|
defaultCollapsed={true}
|
|
/>
|
|
|
|
{/* Header */}
|
|
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div>
|
|
<h2 className="text-lg font-semibold text-slate-900">Verarbeitungstaetigkeiten</h2>
|
|
<p className="text-sm text-slate-500 mt-1">{processingActivities.length} dokumentierte Taetigkeiten</p>
|
|
</div>
|
|
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700">
|
|
+ Neue Taetigkeit
|
|
</button>
|
|
</div>
|
|
|
|
{/* Filter */}
|
|
<div className="flex gap-2">
|
|
{[
|
|
{ value: 'all', label: 'Alle' },
|
|
{ value: 'active', label: 'Aktiv' },
|
|
{ value: 'review_needed', label: 'Pruefung erforderlich' },
|
|
{ value: 'inactive', label: 'Inaktiv' },
|
|
].map(filter => (
|
|
<button
|
|
key={filter.value}
|
|
onClick={() => setFilterStatus(filter.value)}
|
|
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
|
filterStatus === filter.value
|
|
? 'bg-purple-600 text-white'
|
|
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
|
}`}
|
|
>
|
|
{filter.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Activities List */}
|
|
<div className="space-y-4">
|
|
{filteredActivities.map((activity) => (
|
|
<div key={activity.id} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
|
<button
|
|
onClick={() => setExpandedActivity(expandedActivity === activity.id ? null : activity.id)}
|
|
className="w-full px-6 py-4 flex items-center justify-between hover:bg-slate-50 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-4">
|
|
<div className="text-left">
|
|
<div className="flex items-center gap-3">
|
|
<h3 className="font-semibold text-slate-900">{activity.name}</h3>
|
|
{getStatusBadge(activity.status)}
|
|
{activity.thirdCountryTransfer && (
|
|
<span className="px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
|
|
Drittland-Transfer
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-slate-500 mt-1">{activity.purpose}</p>
|
|
</div>
|
|
</div>
|
|
<svg
|
|
className={`w-5 h-5 text-slate-400 transition-transform ${expandedActivity === activity.id ? 'rotate-180' : ''}`}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
|
|
{expandedActivity === activity.id && (
|
|
<div className="px-6 pb-6 border-t border-slate-100">
|
|
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{/* Left Column */}
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Rechtsgrundlage</h4>
|
|
<p className="font-semibold text-slate-900">{activity.legalBasis}</p>
|
|
<p className="text-sm text-slate-600">{activity.legalBasisDetail}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Datenkategorien</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
{activity.categories.map((cat, idx) => (
|
|
<span key={idx} className="px-2 py-1 bg-slate-100 text-slate-700 rounded text-sm">
|
|
{cat}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Empfaenger</h4>
|
|
<ul className="text-sm text-slate-700 list-disc list-inside">
|
|
{activity.recipients.map((rec, idx) => (
|
|
<li key={idx}>{rec}</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Column */}
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Loeschfrist</h4>
|
|
<p className="text-slate-700">{activity.retentionPeriod}</p>
|
|
</div>
|
|
|
|
{activity.thirdCountryTransfer && activity.thirdCountryDetails && (
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Drittland-Transfer</h4>
|
|
<p className="text-slate-700">{activity.thirdCountryDetails}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Technische Massnahmen</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
{activity.technicalMeasures.map((measure, idx) => (
|
|
<span key={idx} className="px-2 py-1 bg-green-100 text-green-700 rounded text-sm">
|
|
{measure}
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Letzte Pruefung</h4>
|
|
<p className="text-slate-700">{activity.lastReview}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 pt-4 border-t border-slate-100 flex gap-2">
|
|
<button className="px-3 py-1.5 text-sm text-purple-600 hover:text-purple-700 font-medium">
|
|
Bearbeiten
|
|
</button>
|
|
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-700">
|
|
PDF exportieren
|
|
</button>
|
|
</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">Pflicht zur Fuehrung</h4>
|
|
<p className="text-sm text-purple-800 mt-1">
|
|
Gemaess Art. 30 DSGVO ist jeder Verantwortliche verpflichtet, ein Verzeichnis aller
|
|
Verarbeitungstaetigkeiten zu fuehren. Dieses Verzeichnis muss der Aufsichtsbehoerde
|
|
auf Anfrage zur Verfuegung gestellt werden.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|