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

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>
)
}