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>
598 lines
25 KiB
TypeScript
598 lines
25 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* VVT - Verarbeitungsverzeichnis
|
|
*
|
|
* Art. 30 DSGVO - Verzeichnis von Verarbeitungstaetigkeiten
|
|
* Integriert mit AI Compliance SDK
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { PagePurpose } from '@/components/common/PagePurpose'
|
|
|
|
interface ProcessingActivity {
|
|
id: string
|
|
tenant_id: string
|
|
name: string
|
|
description: string
|
|
purpose: string
|
|
legal_basis: string
|
|
legal_basis_details: string
|
|
data_categories: string[]
|
|
data_subject_categories: string[]
|
|
recipients: string[]
|
|
third_country_transfer: boolean
|
|
transfer_safeguards: string
|
|
retention_period: string
|
|
dsfa_required: boolean
|
|
responsible_person: string
|
|
responsible_department: string
|
|
systems: string[]
|
|
status: string
|
|
created_at: string
|
|
updated_at: string
|
|
}
|
|
|
|
const LEGAL_BASES = [
|
|
{ value: 'consent', label: 'Art. 6 Abs. 1 lit. a - Einwilligung' },
|
|
{ value: 'contract', label: 'Art. 6 Abs. 1 lit. b - Vertragserfüllung' },
|
|
{ value: 'legal_obligation', label: 'Art. 6 Abs. 1 lit. c - Rechtliche Verpflichtung' },
|
|
{ value: 'vital_interests', label: 'Art. 6 Abs. 1 lit. d - Lebenswichtige Interessen' },
|
|
{ value: 'public_interest', label: 'Art. 6 Abs. 1 lit. e - Öffentliches Interesse' },
|
|
{ value: 'legitimate_interests', label: 'Art. 6 Abs. 1 lit. f - Berechtigtes Interesse' },
|
|
]
|
|
|
|
export default function VVTPage() {
|
|
const [activities, setActivities] = useState<ProcessingActivity[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [expandedActivity, setExpandedActivity] = useState<string | null>(null)
|
|
const [filterStatus, setFilterStatus] = useState<string>('all')
|
|
const [showCreateModal, setShowCreateModal] = useState(false)
|
|
const [newActivity, setNewActivity] = useState<Partial<ProcessingActivity>>({
|
|
name: '',
|
|
purpose: '',
|
|
legal_basis: 'contract',
|
|
legal_basis_details: '',
|
|
data_categories: [],
|
|
data_subject_categories: [],
|
|
recipients: [],
|
|
third_country_transfer: false,
|
|
retention_period: '',
|
|
responsible_person: '',
|
|
responsible_department: '',
|
|
systems: [],
|
|
status: 'draft',
|
|
})
|
|
|
|
useEffect(() => {
|
|
loadActivities()
|
|
}, [])
|
|
|
|
async function loadActivities() {
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch('/sdk/v1/dsgvo/processing-activities', {
|
|
headers: {
|
|
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
|
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
|
}
|
|
})
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setActivities(data.processing_activities || [])
|
|
} else {
|
|
const errorData = await res.json().catch(() => ({}))
|
|
setError(errorData.error || 'Fehler beim Laden')
|
|
}
|
|
} catch (err) {
|
|
setError('Verbindungsfehler zum SDK')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
async function createActivity() {
|
|
try {
|
|
const res = await fetch('/sdk/v1/dsgvo/processing-activities', {
|
|
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(newActivity),
|
|
})
|
|
if (res.ok) {
|
|
setShowCreateModal(false)
|
|
setNewActivity({
|
|
name: '',
|
|
purpose: '',
|
|
legal_basis: 'contract',
|
|
data_categories: [],
|
|
status: 'draft',
|
|
})
|
|
loadActivities()
|
|
} else {
|
|
const errorData = await res.json().catch(() => ({}))
|
|
alert(errorData.error || 'Fehler beim Erstellen')
|
|
}
|
|
} catch (err) {
|
|
alert('Verbindungsfehler')
|
|
}
|
|
}
|
|
|
|
async function deleteActivity(id: string) {
|
|
if (!confirm('Verarbeitungstätigkeit wirklich löschen?')) return
|
|
try {
|
|
const res = await fetch(`/sdk/v1/dsgvo/processing-activities/${id}`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
|
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
|
},
|
|
})
|
|
if (res.ok) {
|
|
loadActivities()
|
|
}
|
|
} catch (err) {
|
|
alert('Fehler beim Löschen')
|
|
}
|
|
}
|
|
|
|
async function exportVVT(format: 'csv' | 'json') {
|
|
window.open(`/sdk/v1/dsgvo/export/vvt?format=${format}`, '_blank')
|
|
}
|
|
|
|
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-slate-100 text-slate-600">Entwurf</span>
|
|
case 'under_review':
|
|
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">In Prüfung</span>
|
|
case 'archived':
|
|
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">Archiviert</span>
|
|
default:
|
|
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-600">{status}</span>
|
|
}
|
|
}
|
|
|
|
const getLegalBasisLabel = (value: string) => {
|
|
const basis = LEGAL_BASES.find(b => b.value === value)
|
|
return basis?.label || value
|
|
}
|
|
|
|
const filteredActivities = filterStatus === 'all'
|
|
? activities
|
|
: activities.filter(a => a.status === filterStatus)
|
|
|
|
return (
|
|
<div>
|
|
<PagePurpose
|
|
title="Verarbeitungsverzeichnis (VVT)"
|
|
purpose="Verzeichnis aller Verarbeitungstätigkeiten gemäß Art. 30 DSGVO. Dokumentiert Zweck, Rechtsgrundlage, Kategorien und Löschfristen."
|
|
audience={['DSB', 'Auditoren', 'Aufsichtsbehörden']}
|
|
gdprArticles={['Art. 30 (Verzeichnis von Verarbeitungstätigkeiten)']}
|
|
architecture={{
|
|
services: ['AI Compliance SDK (Go)'],
|
|
databases: ['PostgreSQL'],
|
|
}}
|
|
relatedPages={[
|
|
{ name: 'TOM', href: '/dsgvo/tom', description: 'Technische Maßnahmen' },
|
|
{ name: 'DSFA', href: '/dsgvo/dsfa', description: 'Datenschutz-Folgenabschätzung' },
|
|
{ name: 'Löschfristen', href: '/dsgvo/loeschfristen', description: 'Aufbewahrungsfristen' },
|
|
]}
|
|
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">Verarbeitungstätigkeiten</h2>
|
|
<p className="text-sm text-slate-500 mt-1">{activities.length} dokumentierte Tätigkeiten</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button
|
|
onClick={() => exportVVT('csv')}
|
|
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-200"
|
|
>
|
|
CSV Export
|
|
</button>
|
|
<button
|
|
onClick={() => exportVVT('json')}
|
|
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-200"
|
|
>
|
|
JSON Export
|
|
</button>
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700"
|
|
>
|
|
+ Neue Tätigkeit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filter */}
|
|
<div className="flex gap-2">
|
|
{[
|
|
{ value: 'all', label: 'Alle' },
|
|
{ value: 'active', label: 'Aktiv' },
|
|
{ value: 'draft', label: 'Entwurf' },
|
|
{ value: 'under_review', label: 'In Prüfung' },
|
|
{ value: 'archived', label: 'Archiviert' },
|
|
].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>
|
|
|
|
{/* Loading / Error */}
|
|
{loading && (
|
|
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
|
|
<div className="animate-spin w-8 h-8 border-2 border-purple-600 border-t-transparent rounded-full mx-auto"></div>
|
|
<p className="mt-4 text-slate-500">Lade Verarbeitungstätigkeiten...</p>
|
|
</div>
|
|
)}
|
|
|
|
{error && (
|
|
<div className="bg-red-50 border border-red-200 rounded-xl p-4 mb-6">
|
|
<p className="text-red-700">{error}</p>
|
|
<button onClick={loadActivities} className="mt-2 text-sm text-red-600 underline">
|
|
Erneut versuchen
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Activities List */}
|
|
{!loading && !error && (
|
|
<div className="space-y-4">
|
|
{filteredActivities.length === 0 ? (
|
|
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
|
|
<p className="text-slate-500">Keine Verarbeitungstätigkeiten gefunden.</p>
|
|
<button
|
|
onClick={() => setShowCreateModal(true)}
|
|
className="mt-4 text-purple-600 font-medium hover:underline"
|
|
>
|
|
Erste Tätigkeit anlegen
|
|
</button>
|
|
</div>
|
|
) : (
|
|
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.third_country_transfer && (
|
|
<span className="px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
|
|
Drittland-Transfer
|
|
</span>
|
|
)}
|
|
{activity.dsfa_required && (
|
|
<span className="px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
|
DSFA erforderlich
|
|
</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">{getLegalBasisLabel(activity.legal_basis)}</p>
|
|
{activity.legal_basis_details && (
|
|
<p className="text-sm text-slate-600">{activity.legal_basis_details}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Datenkategorien</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
{(activity.data_categories || []).map((cat, idx) => (
|
|
<span key={idx} className="px-2 py-1 bg-slate-100 text-slate-700 rounded text-sm">
|
|
{cat}
|
|
</span>
|
|
))}
|
|
{(!activity.data_categories || activity.data_categories.length === 0) && (
|
|
<span className="text-slate-400 text-sm">Keine angegeben</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Betroffene Kategorien</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
{(activity.data_subject_categories || []).map((cat, idx) => (
|
|
<span key={idx} className="px-2 py-1 bg-blue-100 text-blue-700 rounded text-sm">
|
|
{cat}
|
|
</span>
|
|
))}
|
|
{(!activity.data_subject_categories || activity.data_subject_categories.length === 0) && (
|
|
<span className="text-slate-400 text-sm">Keine angegeben</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Empfänger</h4>
|
|
{activity.recipients && activity.recipients.length > 0 ? (
|
|
<ul className="text-sm text-slate-700 list-disc list-inside">
|
|
{activity.recipients.map((rec, idx) => (
|
|
<li key={idx}>{rec}</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<span className="text-slate-400 text-sm">Keine externen Empfänger</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Column */}
|
|
<div className="space-y-4">
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Löschfrist</h4>
|
|
<p className="text-slate-700">{activity.retention_period || 'Nicht definiert'}</p>
|
|
</div>
|
|
|
|
{activity.third_country_transfer && activity.transfer_safeguards && (
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Drittland-Schutzmaßnahmen</h4>
|
|
<p className="text-slate-700">{activity.transfer_safeguards}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Verantwortlich</h4>
|
|
<p className="text-slate-700">
|
|
{activity.responsible_person || 'k.A.'}
|
|
{activity.responsible_department && ` (${activity.responsible_department})`}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Systeme</h4>
|
|
<div className="flex flex-wrap gap-2">
|
|
{(activity.systems || []).map((sys, idx) => (
|
|
<span key={idx} className="px-2 py-1 bg-green-100 text-green-700 rounded text-sm">
|
|
{sys}
|
|
</span>
|
|
))}
|
|
{(!activity.systems || activity.systems.length === 0) && (
|
|
<span className="text-slate-400 text-sm">Keine angegeben</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<h4 className="text-sm font-medium text-slate-500 mb-1">Erstellt / Aktualisiert</h4>
|
|
<p className="text-slate-700 text-sm">
|
|
{new Date(activity.created_at).toLocaleDateString('de-DE')} / {new Date(activity.updated_at).toLocaleDateString('de-DE')}
|
|
</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
|
|
onClick={() => deleteActivity(activity.id)}
|
|
className="px-3 py-1.5 text-sm text-red-600 hover:text-red-700"
|
|
>
|
|
Löschen
|
|
</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 p-6 max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
|
<h3 className="text-lg font-semibold text-slate-900 mb-4">Neue Verarbeitungstätigkeit</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Name *</label>
|
|
<input
|
|
type="text"
|
|
value={newActivity.name || ''}
|
|
onChange={(e) => setNewActivity({ ...newActivity, name: e.target.value })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
placeholder="z.B. Benutzerverwaltung"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Zweck der Verarbeitung *</label>
|
|
<textarea
|
|
value={newActivity.purpose || ''}
|
|
onChange={(e) => setNewActivity({ ...newActivity, purpose: e.target.value })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
rows={2}
|
|
placeholder="Beschreiben Sie den Zweck der Datenverarbeitung"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Rechtsgrundlage *</label>
|
|
<select
|
|
value={newActivity.legal_basis || 'contract'}
|
|
onChange={(e) => setNewActivity({ ...newActivity, legal_basis: e.target.value })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
>
|
|
{LEGAL_BASES.map(basis => (
|
|
<option key={basis.value} value={basis.value}>{basis.label}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Details zur Rechtsgrundlage</label>
|
|
<input
|
|
type="text"
|
|
value={newActivity.legal_basis_details || ''}
|
|
onChange={(e) => setNewActivity({ ...newActivity, legal_basis_details: e.target.value })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
placeholder="Weitere Details zur Begründung"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Datenkategorien (kommasepariert)</label>
|
|
<input
|
|
type="text"
|
|
value={(newActivity.data_categories || []).join(', ')}
|
|
onChange={(e) => setNewActivity({ ...newActivity, data_categories: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
placeholder="Name, E-Mail, Adresse"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Betroffene (kommasepariert)</label>
|
|
<input
|
|
type="text"
|
|
value={(newActivity.data_subject_categories || []).join(', ')}
|
|
onChange={(e) => setNewActivity({ ...newActivity, data_subject_categories: e.target.value.split(',').map(s => s.trim()).filter(Boolean) })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
placeholder="Kunden, Mitarbeiter, Lieferanten"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Aufbewahrungsfrist</label>
|
|
<input
|
|
type="text"
|
|
value={newActivity.retention_period || ''}
|
|
onChange={(e) => setNewActivity({ ...newActivity, retention_period: e.target.value })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
placeholder="z.B. 10 Jahre (§ 147 AO)"
|
|
/>
|
|
</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={newActivity.responsible_person || ''}
|
|
onChange={(e) => setNewActivity({ ...newActivity, responsible_person: e.target.value })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-slate-700 mb-1">Abteilung</label>
|
|
<input
|
|
type="text"
|
|
value={newActivity.responsible_department || ''}
|
|
onChange={(e) => setNewActivity({ ...newActivity, responsible_department: e.target.value })}
|
|
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id="third_country"
|
|
checked={newActivity.third_country_transfer || false}
|
|
onChange={(e) => setNewActivity({ ...newActivity, third_country_transfer: e.target.checked })}
|
|
className="rounded"
|
|
/>
|
|
<label htmlFor="third_country" className="text-sm text-slate-700">Drittland-Transfer</label>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<input
|
|
type="checkbox"
|
|
id="dsfa_required"
|
|
checked={newActivity.dsfa_required || false}
|
|
onChange={(e) => setNewActivity({ ...newActivity, dsfa_required: e.target.checked })}
|
|
className="rounded"
|
|
/>
|
|
<label htmlFor="dsfa_required" className="text-sm text-slate-700">DSFA erforderlich</label>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3 mt-6">
|
|
<button
|
|
onClick={() => setShowCreateModal(false)}
|
|
className="px-4 py-2 text-slate-700 hover:bg-slate-100 rounded-lg text-sm font-medium"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
onClick={createActivity}
|
|
disabled={!newActivity.name || !newActivity.purpose}
|
|
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700 disabled:opacity-50"
|
|
>
|
|
Erstellen
|
|
</button>
|
|
</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 Führung</h4>
|
|
<p className="text-sm text-purple-800 mt-1">
|
|
Gemäß Art. 30 DSGVO ist jeder Verantwortliche verpflichtet, ein Verzeichnis aller
|
|
Verarbeitungstätigkeiten zu führen. Dieses Verzeichnis muss der Aufsichtsbehörde
|
|
auf Anfrage zur Verfügung gestellt werden.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|