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>
499 lines
22 KiB
TypeScript
499 lines
22 KiB
TypeScript
'use client'
|
|
|
|
/**
|
|
* Einwilligungsverwaltung - User Consent Management
|
|
*
|
|
* Zentrale Uebersicht aller Nutzer-Einwilligungen aus:
|
|
* - Website
|
|
* - App
|
|
* - PWA
|
|
*
|
|
* Kategorien: Marketing, Statistik, Cookies, Rechtliche Dokumente
|
|
*/
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import { PagePurpose } from '@/components/common/PagePurpose'
|
|
|
|
const API_BASE = '/api/admin/consent'
|
|
|
|
type Tab = 'overview' | 'documents' | 'cookies' | 'marketing' | 'audit'
|
|
|
|
interface ConsentStats {
|
|
total_users: number
|
|
consented_users: number
|
|
consent_rate: number
|
|
pending_consents: number
|
|
}
|
|
|
|
interface AuditEntry {
|
|
id: string
|
|
user_id: string
|
|
action: string
|
|
entity_type: string
|
|
entity_id: string
|
|
details: Record<string, unknown>
|
|
ip_address: string
|
|
created_at: string
|
|
}
|
|
|
|
interface ConsentSummary {
|
|
category: string
|
|
total: number
|
|
accepted: number
|
|
declined: number
|
|
pending: number
|
|
rate: number
|
|
}
|
|
|
|
export default function EinwilligungenPage() {
|
|
const [activeTab, setActiveTab] = useState<Tab>('overview')
|
|
const [stats, setStats] = useState<ConsentStats | null>(null)
|
|
const [auditLog, setAuditLog] = useState<AuditEntry[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [authToken, setAuthToken] = useState<string>('')
|
|
|
|
useEffect(() => {
|
|
const token = localStorage.getItem('bp_admin_token')
|
|
if (token) {
|
|
setAuthToken(token)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
if (activeTab === 'overview') {
|
|
loadStats()
|
|
} else if (activeTab === 'audit') {
|
|
loadAuditLog()
|
|
}
|
|
}, [activeTab, authToken])
|
|
|
|
async function loadStats() {
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch(`${API_BASE}/stats`, {
|
|
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
|
|
})
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setStats(data)
|
|
} else {
|
|
setError('Fehler beim Laden der Statistiken')
|
|
}
|
|
} catch {
|
|
setError('Verbindungsfehler')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
async function loadAuditLog() {
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const res = await fetch(`${API_BASE}/audit-log?limit=50`, {
|
|
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
|
|
})
|
|
if (res.ok) {
|
|
const data = await res.json()
|
|
setAuditLog(data.entries || [])
|
|
} else {
|
|
setError('Fehler beim Laden des Audit-Logs')
|
|
}
|
|
} catch {
|
|
setError('Verbindungsfehler')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
// Mock data for consent summary (in production, this comes from API)
|
|
const consentSummary: ConsentSummary[] = [
|
|
{ category: 'AGB', total: 1250, accepted: 1248, declined: 0, pending: 2, rate: 99.8 },
|
|
{ category: 'Datenschutz', total: 1250, accepted: 1245, declined: 3, pending: 2, rate: 99.6 },
|
|
{ category: 'Cookies (Notwendig)', total: 1250, accepted: 1250, declined: 0, pending: 0, rate: 100 },
|
|
{ category: 'Cookies (Analyse)', total: 1250, accepted: 892, declined: 358, pending: 0, rate: 71.4 },
|
|
{ category: 'Cookies (Marketing)', total: 1250, accepted: 456, declined: 794, pending: 0, rate: 36.5 },
|
|
{ category: 'Newsletter', total: 1250, accepted: 312, declined: 938, pending: 0, rate: 25.0 },
|
|
]
|
|
|
|
const tabs: { id: Tab; label: string }[] = [
|
|
{ id: 'overview', label: 'Uebersicht' },
|
|
{ id: 'documents', label: 'Dokumenten-Consents' },
|
|
{ id: 'cookies', label: 'Cookie-Consents' },
|
|
{ id: 'marketing', label: 'Marketing-Consents' },
|
|
{ id: 'audit', label: 'Audit-Trail' },
|
|
]
|
|
|
|
const getActionLabel = (action: string) => {
|
|
const labels: Record<string, string> = {
|
|
'consent_given': 'Zustimmung erteilt',
|
|
'consent_withdrawn': 'Zustimmung widerrufen',
|
|
'cookie_consent_updated': 'Cookie-Einstellungen aktualisiert',
|
|
'data_access': 'Datenzugriff',
|
|
'data_export_requested': 'Datenexport angefordert',
|
|
'data_deletion_requested': 'Loeschung angefordert',
|
|
'account_suspended': 'Account gesperrt',
|
|
'account_restored': 'Account wiederhergestellt',
|
|
}
|
|
return labels[action] || action
|
|
}
|
|
|
|
const getActionColor = (action: string) => {
|
|
if (action.includes('given') || action.includes('restored')) return 'bg-green-100 text-green-700'
|
|
if (action.includes('withdrawn') || action.includes('deleted') || action.includes('suspended')) return 'bg-red-100 text-red-700'
|
|
return 'bg-blue-100 text-blue-700'
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<PagePurpose
|
|
title="Einwilligungsverwaltung"
|
|
purpose="Zentrale Uebersicht aller Nutzer-Einwilligungen. Hier sehen Sie alle Zustimmungen zu rechtlichen Dokumenten, Cookies, Marketing und Statistik - erfasst ueber Website, App und PWA."
|
|
audience={['DSB', 'Compliance Officer', 'Marketing']}
|
|
gdprArticles={['Art. 6 (Rechtmaessigkeit)', 'Art. 7 (Einwilligung)', 'Art. 21 (Widerspruch)']}
|
|
architecture={{
|
|
services: ['consent-service (Go)'],
|
|
databases: ['PostgreSQL (user_consents, cookie_consents)'],
|
|
}}
|
|
relatedPages={[
|
|
{ name: 'Consent Dokumente', href: '/compliance/consent', description: 'Rechtliche Dokumente verwalten' },
|
|
{ name: 'DSMS', href: '/compliance/dsms', description: 'Datenschutz-Management' },
|
|
{ name: 'DSR', href: '/compliance/dsr', description: 'Betroffenenanfragen' },
|
|
]}
|
|
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">{stats?.total_users || 1250}</div>
|
|
<div className="text-sm text-slate-500">Registrierte Nutzer</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<div className="text-2xl font-bold text-green-600">{stats?.consented_users || 1245}</div>
|
|
<div className="text-sm text-slate-500">Mit Zustimmung</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<div className="text-2xl font-bold text-yellow-600">{stats?.pending_consents || 5}</div>
|
|
<div className="text-sm text-slate-500">Ausstehend</div>
|
|
</div>
|
|
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
|
<div className="text-2xl font-bold text-purple-600">{stats?.consent_rate?.toFixed(1) || 99.6}%</div>
|
|
<div className="text-sm text-slate-500">Zustimmungsrate</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)}
|
|
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>
|
|
|
|
{error && (
|
|
<div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
|
|
{error}
|
|
<button onClick={() => setError(null)} className="ml-4 text-red-500 hover:text-red-700">X</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Content */}
|
|
<div className="bg-white rounded-xl border border-slate-200">
|
|
{/* Overview Tab */}
|
|
{activeTab === 'overview' && (
|
|
<div className="p-6">
|
|
<h2 className="text-lg font-semibold text-slate-900 mb-6">Consent-Uebersicht nach Kategorie</h2>
|
|
|
|
<div className="space-y-4">
|
|
{consentSummary.map((item) => (
|
|
<div key={item.category} className="border border-slate-200 rounded-lg p-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h3 className="font-medium text-slate-900">{item.category}</h3>
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
|
item.rate >= 90 ? 'bg-green-100 text-green-700' :
|
|
item.rate >= 50 ? 'bg-yellow-100 text-yellow-700' :
|
|
'bg-red-100 text-red-700'
|
|
}`}>
|
|
{item.rate}% Zustimmung
|
|
</span>
|
|
</div>
|
|
|
|
<div className="h-2 bg-slate-100 rounded-full overflow-hidden mb-3">
|
|
<div
|
|
className={`h-full ${item.rate >= 90 ? 'bg-green-500' : item.rate >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
|
style={{ width: `${item.rate}%` }}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-4 gap-4 text-sm">
|
|
<div>
|
|
<span className="text-slate-500">Gesamt:</span>
|
|
<span className="ml-1 font-medium">{item.total}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-green-600">Akzeptiert:</span>
|
|
<span className="ml-1 font-medium">{item.accepted}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-red-600">Abgelehnt:</span>
|
|
<span className="ml-1 font-medium">{item.declined}</span>
|
|
</div>
|
|
<div>
|
|
<span className="text-yellow-600">Ausstehend:</span>
|
|
<span className="ml-1 font-medium">{item.pending}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* Export Button */}
|
|
<div className="mt-6 pt-6 border-t border-slate-200">
|
|
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium">
|
|
Consent-Report exportieren (CSV)
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Documents Tab */}
|
|
{activeTab === 'documents' && (
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-lg font-semibold text-slate-900">Dokumenten-Einwilligungen</h2>
|
|
<div className="flex gap-2">
|
|
<select className="px-3 py-2 border border-slate-300 rounded-lg text-sm">
|
|
<option value="">Alle Dokumente</option>
|
|
<option value="terms">AGB</option>
|
|
<option value="privacy">Datenschutz</option>
|
|
<option value="cookies">Cookies</option>
|
|
</select>
|
|
<select className="px-3 py-2 border border-slate-300 rounded-lg text-sm">
|
|
<option value="">Alle Status</option>
|
|
<option value="active">Aktiv</option>
|
|
<option value="withdrawn">Widerrufen</option>
|
|
</select>
|
|
</div>
|
|
</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-ID</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">Version</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Status</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Datum</th>
|
|
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Quelle</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{/* Sample data - in production, this comes from API */}
|
|
{[
|
|
{ id: 'usr_123', doc: 'AGB', version: 'v2.1.0', status: 'active', date: '2024-12-15', source: 'Website' },
|
|
{ id: 'usr_124', doc: 'Datenschutz', version: 'v3.0.0', status: 'active', date: '2024-12-15', source: 'App' },
|
|
{ id: 'usr_125', doc: 'AGB', version: 'v2.1.0', status: 'withdrawn', date: '2024-12-14', source: 'PWA' },
|
|
].map((consent, idx) => (
|
|
<tr key={idx} className="border-b border-slate-100 hover:bg-slate-50">
|
|
<td className="py-3 px-4 font-mono text-sm">{consent.id}</td>
|
|
<td className="py-3 px-4">{consent.doc}</td>
|
|
<td className="py-3 px-4 text-sm text-slate-500">{consent.version}</td>
|
|
<td className="py-3 px-4">
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
|
consent.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
|
}`}>
|
|
{consent.status === 'active' ? 'Aktiv' : 'Widerrufen'}
|
|
</span>
|
|
</td>
|
|
<td className="py-3 px-4 text-sm text-slate-500">{consent.date}</td>
|
|
<td className="py-3 px-4">
|
|
<span className="px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs">{consent.source}</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Cookies Tab */}
|
|
{activeTab === 'cookies' && (
|
|
<div className="p-6">
|
|
<h2 className="text-lg font-semibold text-slate-900 mb-6">Cookie-Einwilligungen</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
{[
|
|
{ name: 'Notwendige Cookies', key: 'necessary', mandatory: true, rate: 100, description: 'Erforderlich fuer Grundfunktionen' },
|
|
{ name: 'Funktionale Cookies', key: 'functional', mandatory: false, rate: 82.3, description: 'Verbesserte Nutzererfahrung' },
|
|
{ name: 'Analyse Cookies', key: 'analytics', mandatory: false, rate: 71.4, description: 'Anonyme Nutzungsstatistiken' },
|
|
{ name: 'Marketing Cookies', key: 'marketing', mandatory: false, rate: 36.5, description: 'Personalisierte Werbung' },
|
|
].map((category) => (
|
|
<div key={category.key} className="border border-slate-200 rounded-xl p-5">
|
|
<div className="flex items-start justify-between mb-3">
|
|
<div>
|
|
<h3 className="font-semibold text-slate-900">{category.name}</h3>
|
|
<p className="text-sm text-slate-500">{category.description}</p>
|
|
</div>
|
|
{category.mandatory && (
|
|
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-medium">Pflicht</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-4">
|
|
<div className="flex-grow h-3 bg-slate-100 rounded-full overflow-hidden">
|
|
<div
|
|
className={`h-full ${category.rate >= 80 ? 'bg-green-500' : category.rate >= 50 ? 'bg-yellow-500' : 'bg-orange-500'}`}
|
|
style={{ width: `${category.rate}%` }}
|
|
/>
|
|
</div>
|
|
<span className="text-lg font-bold text-slate-900">{category.rate}%</span>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mt-6 p-4 bg-slate-50 rounded-lg">
|
|
<h4 className="font-medium text-slate-900 mb-2">Cookie-Banner Einstellungen</h4>
|
|
<p className="text-sm text-slate-600">
|
|
Das Cookie-Banner wird auf allen Plattformen (Website, App, PWA) einheitlich angezeigt.
|
|
Nutzer koennen ihre Praeferenzen jederzeit in den Einstellungen aendern.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Marketing Tab */}
|
|
{activeTab === 'marketing' && (
|
|
<div className="p-6">
|
|
<h2 className="text-lg font-semibold text-slate-900 mb-6">Marketing-Einwilligungen</h2>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
|
{[
|
|
{ name: 'E-Mail Newsletter', rate: 25.0, total: 1250, subscribed: 312 },
|
|
{ name: 'Push-Benachrichtigungen', rate: 45.2, total: 1250, subscribed: 565 },
|
|
{ name: 'Personalisierte Werbung', rate: 18.5, total: 1250, subscribed: 231 },
|
|
].map((channel) => (
|
|
<div key={channel.name} className="bg-white border border-slate-200 rounded-xl p-5">
|
|
<h3 className="font-semibold text-slate-900 mb-2">{channel.name}</h3>
|
|
<div className="text-3xl font-bold text-purple-600 mb-1">{channel.rate}%</div>
|
|
<div className="text-sm text-slate-500">{channel.subscribed} von {channel.total} Nutzern</div>
|
|
|
|
<div className="mt-4 h-2 bg-slate-100 rounded-full overflow-hidden">
|
|
<div className="h-full bg-purple-500" style={{ width: `${channel.rate}%` }} />
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
<div className="border border-slate-200 rounded-lg p-4">
|
|
<h4 className="font-medium text-slate-900 mb-3">Opt-Out Anfragen (letzte 30 Tage)</h4>
|
|
<div className="grid grid-cols-3 gap-4 text-center">
|
|
<div className="p-3 bg-slate-50 rounded-lg">
|
|
<div className="text-xl font-bold text-slate-900">23</div>
|
|
<div className="text-xs text-slate-500">Newsletter</div>
|
|
</div>
|
|
<div className="p-3 bg-slate-50 rounded-lg">
|
|
<div className="text-xl font-bold text-slate-900">45</div>
|
|
<div className="text-xs text-slate-500">Push</div>
|
|
</div>
|
|
<div className="p-3 bg-slate-50 rounded-lg">
|
|
<div className="text-xl font-bold text-slate-900">12</div>
|
|
<div className="text-xs text-slate-500">Werbung</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Audit Tab */}
|
|
{activeTab === 'audit' && (
|
|
<div className="p-6">
|
|
<div className="flex items-center justify-between mb-6">
|
|
<h2 className="text-lg font-semibold text-slate-900">Consent Audit-Trail</h2>
|
|
<div className="flex gap-2">
|
|
<select className="px-3 py-2 border border-slate-300 rounded-lg text-sm">
|
|
<option value="">Alle Aktionen</option>
|
|
<option value="consent_given">Zustimmung erteilt</option>
|
|
<option value="consent_withdrawn">Zustimmung widerrufen</option>
|
|
<option value="cookie_consent_updated">Cookie aktualisiert</option>
|
|
</select>
|
|
<button
|
|
onClick={loadAuditLog}
|
|
className="px-3 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm hover:bg-slate-200"
|
|
>
|
|
Aktualisieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<div className="text-center py-12 text-slate-500">Lade Audit-Log...</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{(auditLog.length > 0 ? auditLog : [
|
|
// Sample data
|
|
{ id: '1', user_id: 'usr_123', action: 'consent_given', entity_type: 'document', entity_id: 'doc_agb', details: {}, ip_address: '192.168.1.1', created_at: '2024-12-15T10:30:00Z' },
|
|
{ id: '2', user_id: 'usr_124', action: 'cookie_consent_updated', entity_type: 'cookie', entity_id: 'analytics', details: {}, ip_address: '192.168.1.2', created_at: '2024-12-15T10:25:00Z' },
|
|
{ id: '3', user_id: 'usr_125', action: 'consent_withdrawn', entity_type: 'document', entity_id: 'doc_newsletter', details: {}, ip_address: '192.168.1.3', created_at: '2024-12-15T10:20:00Z' },
|
|
]).map((entry) => (
|
|
<div key={entry.id} className="border border-slate-200 rounded-lg p-4 hover:bg-slate-50">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<span className={`px-2 py-1 rounded text-xs font-medium ${getActionColor(entry.action)}`}>
|
|
{getActionLabel(entry.action)}
|
|
</span>
|
|
<span className="font-mono text-sm text-slate-600">{entry.user_id}</span>
|
|
</div>
|
|
<span className="text-sm text-slate-400">
|
|
{new Date(entry.created_at).toLocaleString('de-DE')}
|
|
</span>
|
|
</div>
|
|
<div className="mt-2 text-sm text-slate-500">
|
|
<span className="text-slate-400">Entity:</span> {entry.entity_type} / {entry.entity_id}
|
|
<span className="mx-2 text-slate-300">|</span>
|
|
<span className="text-slate-400">IP:</span> {entry.ip_address}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* GDPR Notice */}
|
|
<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">DSGVO-Hinweis</h4>
|
|
<p className="text-sm text-purple-800 mt-1">
|
|
Alle Einwilligungen werden revisionssicher gespeichert und koennen jederzeit nachgewiesen werden.
|
|
Nutzer koennen ihre Einwilligungen gemaess Art. 7 Abs. 3 DSGVO jederzeit widerrufen.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|