Files
breakpilot-compliance/admin-compliance/app/sdk/consent-management/page.tsx
Benjamin Admin b7c1a5da1a
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 36s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 23s
CI / test-python-dsms-gateway (push) Successful in 18s
feat: Consent-Service Module nach Compliance migriert (DSR, E-Mail-Templates, Legal Docs, Banner)
5-Phasen-Migration: Go consent-service Proxies durch native Python/FastAPI ersetzt.

Phase 1 — DSR (Betroffenenrechte): 6 Tabellen, 30 Endpoints, Frontend-API umgestellt
Phase 2 — E-Mail-Templates: 5 Tabellen, 20 Endpoints, neues Frontend, SDK_STEPS erweitert
Phase 3 — Legal Documents Extension: User Consents, Audit Log, Cookie-Kategorien
Phase 4 — Banner Consent: Device-Consents, Site-Configs, Kategorien, Vendors
Phase 5 — Cleanup: DSR-Proxy aus main.py entfernt, Frontend-URLs aktualisiert

148 neue Tests (50 + 47 + 26 + 25), alle bestanden.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:36:24 +01:00

1304 lines
57 KiB
TypeScript

'use client'
/**
* Consent Management Page (SDK Version)
*
* Admin interface for managing:
* - Documents (AGB, Privacy, etc.)
* - Document Versions
* - Email Templates
* - GDPR Processes (Art. 15-21)
* - Statistics
*/
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import Link from 'next/link'
import { useSDK } from '@/lib/sdk'
import StepHeader from '@/components/sdk/StepHeader/StepHeader'
// API Proxy URL (avoids CORS issues)
const API_BASE = '/api/admin/consent'
type Tab = 'documents' | 'versions' | 'emails' | 'gdpr' | 'stats'
interface Document {
id: string
type: string
name: string
description: string
mandatory: boolean
created_at: string
updated_at: string
}
interface Version {
id: string
document_id: string
version: string
language: string
title: string
content: string
status: string
created_at: string
}
// Email template editor types
interface EmailTemplateData {
key: string
subject: string
body: string
}
// ============================================================================
// Helper sub-components for API-backed inline editors
// ============================================================================
function ApiTemplateEditor({
template,
saving,
onSave,
onPreview,
}: {
template: { id: string; template_key: string; subject: string; body: string; language: string; is_active: boolean }
saving: boolean
onSave: (subject: string, body: string) => void
onPreview: (subject: string, body: string) => void
}) {
const [subject, setSubject] = useState(template.subject)
const [body, setBody] = useState(template.body)
const [expanded, setExpanded] = useState(false)
return (
<div className="border border-slate-200 rounded-lg bg-white overflow-hidden">
<div className="p-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className={`w-2.5 h-2.5 rounded-full ${template.is_active ? 'bg-green-400' : 'bg-slate-300'}`} />
<div>
<span className="font-medium text-slate-900 font-mono text-sm">{template.template_key}</span>
<p className="text-sm text-slate-500 truncate max-w-xs">{subject}</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs uppercase">{template.language}</span>
<button
onClick={() => onPreview(subject, body)}
className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400"
>
Vorschau
</button>
<button
onClick={() => setExpanded(!expanded)}
className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400"
>
{expanded ? 'Schliessen' : 'Bearbeiten'}
</button>
</div>
</div>
{expanded && (
<div className="border-t border-slate-200 p-4 space-y-3 bg-slate-50">
<div>
<label className="block text-xs font-medium text-slate-700 mb-1">Betreff</label>
<input
type="text"
value={subject}
onChange={(e) => setSubject(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-700 mb-1">Inhalt</label>
<textarea
value={body}
onChange={(e) => setBody(e.target.value)}
rows={8}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono"
/>
</div>
<div className="bg-white rounded-lg p-3 border border-slate-200">
<div className="text-xs font-medium text-slate-500 mb-1">Verfuegbare Platzhalter:</div>
<div className="flex flex-wrap gap-2">
{['{{name}}', '{{email}}', '{{date}}', '{{deadline}}', '{{company}}'].map(v => (
<span key={v} className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs font-mono">{v}</span>
))}
</div>
</div>
<div className="flex justify-end">
<button
onClick={() => onSave(subject, body)}
disabled={saving}
className="px-4 py-2 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg font-medium disabled:opacity-60"
>
{saving ? 'Speichern...' : 'Speichern'}
</button>
</div>
</div>
)}
</div>
)
}
function ApiGdprProcessEditor({
process,
saving,
onSave,
}: {
process: { id: string; process_key: string; title: string; description: string; legal_basis: string; retention_days: number; is_active: boolean }
saving: boolean
onSave: (title: string, description: string) => void
}) {
const [title, setTitle] = useState(process.title)
const [description, setDescription] = useState(process.description || '')
const [expanded, setExpanded] = useState(false)
return (
<div className="border border-slate-200 rounded-xl bg-white overflow-hidden">
<div className="p-4 flex items-start justify-between">
<div className="flex items-start gap-3">
<div className="w-10 h-10 bg-purple-100 text-purple-700 rounded-lg flex items-center justify-center flex-shrink-0 font-mono text-xs font-bold">
{process.legal_basis?.replace('Art. ', '').replace(' DSGVO', '') || '?'}
</div>
<div>
<h4 className="font-semibold text-slate-900">{title}</h4>
<p className="text-sm text-slate-500">{description || 'Keine Beschreibung'}</p>
{process.retention_days && (
<span className="text-xs text-slate-400">Aufbewahrung: {process.retention_days} Tage</span>
)}
</div>
</div>
<button
onClick={() => setExpanded(!expanded)}
className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400 flex-shrink-0"
>
{expanded ? 'Schliessen' : 'Bearbeiten'}
</button>
</div>
{expanded && (
<div className="border-t border-slate-200 p-4 space-y-3 bg-slate-50">
<div>
<label className="block text-xs font-medium text-slate-700 mb-1">Titel</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div>
<label className="block text-xs font-medium text-slate-700 mb-1">Beschreibung</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
/>
</div>
<div className="flex justify-end">
<button
onClick={() => onSave(title, description)}
disabled={saving}
className="px-4 py-2 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg font-medium disabled:opacity-60"
>
{saving ? 'Speichern...' : 'Speichern'}
</button>
</div>
</div>
)}
</div>
)
}
// =============================================================================
// CONSENT TEMPLATE CREATE MODAL
// =============================================================================
function ConsentTemplateCreateModal({
onClose,
onSuccess,
}: {
onClose: () => void
onSuccess: () => void
}) {
const [templateKey, setTemplateKey] = useState('')
const [subject, setSubject] = useState('')
const [body, setBody] = useState('')
const [language, setLanguage] = useState('de')
const [saving, setSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
async function handleSave() {
if (!templateKey.trim()) {
setError('Template-Key ist erforderlich.')
return
}
setSaving(true)
setError(null)
try {
const res = await fetch('/api/sdk/v1/consent-templates', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
template_key: templateKey.trim(),
subject: subject.trim(),
body: body.trim(),
language,
}),
})
if (!res.ok) {
const data = await res.json().catch(() => ({}))
throw new Error(data.detail || data.message || `Fehler: ${res.status}`)
}
onSuccess()
} catch (e: unknown) {
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
} finally {
setSaving(false)
}
}
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-xl shadow-2xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
<h3 className="font-semibold text-slate-900">Neue E-Mail Vorlage</h3>
<button onClick={onClose} className="text-slate-400 hover:text-slate-600">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-6 space-y-4">
{error && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">{error}</div>
)}
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
Template-Key <span className="text-red-500">*</span>
</label>
<input
type="text"
value={templateKey}
onChange={e => setTemplateKey(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="z.B. dsr_confirmation"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Sprache</label>
<select
value={language}
onChange={e => setLanguage(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="de">Deutsch</option>
<option value="en">English</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Betreff</label>
<input
type="text"
value={subject}
onChange={e => setSubject(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
placeholder="E-Mail Betreff"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Inhalt</label>
<textarea
value={body}
onChange={e => setBody(e.target.value)}
rows={6}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500 focus:border-purple-500 resize-none"
placeholder="E-Mail Inhalt..."
/>
</div>
</div>
<div className="px-6 py-4 border-t border-slate-200 flex items-center justify-end gap-3">
<button
onClick={onClose}
className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
>
Abbrechen
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50"
>
{saving ? 'Speichern...' : 'Vorlage erstellen'}
</button>
</div>
</div>
</div>
)
}
export default function ConsentManagementPage() {
const { state } = useSDK()
const router = useRouter()
const [activeTab, setActiveTab] = useState<Tab>('documents')
const [showCreateTemplateModal, setShowCreateTemplateModal] = useState(false)
const [documents, setDocuments] = useState<Document[]>([])
const [versions, setVersions] = useState<Version[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [selectedDocument, setSelectedDocument] = useState<string>('')
// Stats state
const [consentStats, setConsentStats] = useState<{ activeConsents: number; documentCount: number; openDSRs: number }>({ activeConsents: 0, documentCount: 0, openDSRs: 0 })
// GDPR tab state
const [dsrCounts, setDsrCounts] = useState<Record<string, number>>({})
const [dsrOverview, setDsrOverview] = useState<{ open: number; completed: number; in_progress: number; overdue: number }>({ open: 0, completed: 0, in_progress: 0, overdue: 0 })
// Email template editor state
const [editingTemplate, setEditingTemplate] = useState<EmailTemplateData | null>(null)
const [previewTemplate, setPreviewTemplate] = useState<EmailTemplateData | null>(null)
const [savedTemplates, setSavedTemplates] = useState<Record<string, EmailTemplateData>>({})
// API-backed email templates and GDPR processes
const [apiEmailTemplates, setApiEmailTemplates] = useState<Array<{id: string; template_key: string; subject: string; body: string; language: string; is_active: boolean}>>([])
const [apiGdprProcesses, setApiGdprProcesses] = useState<Array<{id: string; process_key: string; title: string; description: string; legal_basis: string; retention_days: number; is_active: boolean}>>([])
const [templatesLoading, setTemplatesLoading] = useState(false)
const [gdprLoading, setGdprLoading] = useState(false)
const [savingTemplateId, setSavingTemplateId] = useState<string | null>(null)
const [savingProcessId, setSavingProcessId] = useState<string | null>(null)
// Auth token (in production, get from auth context)
const [authToken, setAuthToken] = useState<string>('')
useEffect(() => {
const token = localStorage.getItem('bp_admin_token')
if (token) {
setAuthToken(token)
}
// Load saved email templates from localStorage
try {
const saved = localStorage.getItem('sdk-email-templates')
if (saved) {
setSavedTemplates(JSON.parse(saved))
}
} catch { /* ignore */ }
}, [])
useEffect(() => {
if (activeTab === 'documents') {
loadDocuments()
} else if (activeTab === 'versions' && selectedDocument) {
loadVersions(selectedDocument)
} else if (activeTab === 'stats') {
loadStats()
} else if (activeTab === 'gdpr') {
loadGDPRData()
loadApiGdprProcesses()
} else if (activeTab === 'emails') {
loadApiEmailTemplates()
}
}, [activeTab, selectedDocument, authToken])
async function loadApiEmailTemplates() {
setTemplatesLoading(true)
try {
const res = await fetch('/api/sdk/v1/consent-templates')
if (res.ok) {
const data = await res.json()
setApiEmailTemplates(Array.isArray(data) ? data : [])
}
} catch (err) {
console.error('Failed to load email templates from API:', err)
} finally {
setTemplatesLoading(false)
}
}
async function loadApiGdprProcesses() {
setGdprLoading(true)
try {
const res = await fetch('/api/sdk/v1/consent-templates/gdpr-processes')
if (res.ok) {
const data = await res.json()
setApiGdprProcesses(Array.isArray(data) ? data : [])
}
} catch (err) {
console.error('Failed to load GDPR processes from API:', err)
} finally {
setGdprLoading(false)
}
}
async function saveApiEmailTemplate(template: {id: string; subject: string; body: string}) {
setSavingTemplateId(template.id)
try {
const res = await fetch(`/api/sdk/v1/consent-templates/${template.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ subject: template.subject, body: template.body }),
})
if (res.ok) {
const updated = await res.json()
setApiEmailTemplates(prev => prev.map(t => t.id === updated.id ? updated : t))
}
} catch (err) {
console.error('Failed to save email template:', err)
} finally {
setSavingTemplateId(null)
}
}
async function saveApiGdprProcess(process: {id: string; title: string; description: string}) {
setSavingProcessId(process.id)
try {
const res = await fetch(`/api/sdk/v1/consent-templates/gdpr-processes/${process.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: process.title, description: process.description }),
})
if (res.ok) {
const updated = await res.json()
setApiGdprProcesses(prev => prev.map(p => p.id === updated.id ? updated : p))
}
} catch (err) {
console.error('Failed to save GDPR process:', err)
} finally {
setSavingProcessId(null)
}
}
async function loadDocuments() {
setLoading(true)
setError(null)
try {
const res = await fetch(`${API_BASE}/documents`, {
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
})
if (res.ok) {
const data = await res.json()
setDocuments(data.documents || [])
} else {
const errorData = await res.json().catch(() => ({}))
setError(errorData.error || 'Fehler beim Laden der Dokumente')
}
} catch {
setError('Verbindungsfehler zum Server')
} finally {
setLoading(false)
}
}
async function loadVersions(docId: string) {
setLoading(true)
setError(null)
try {
const res = await fetch(`${API_BASE}/documents/${docId}/versions`, {
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
})
if (res.ok) {
const data = await res.json()
setVersions(data.versions || [])
} else {
const errorData = await res.json().catch(() => ({}))
setError(errorData.error || 'Fehler beim Laden der Versionen')
}
} catch {
setError('Verbindungsfehler zum Server')
} finally {
setLoading(false)
}
}
async function loadStats() {
try {
const token = localStorage.getItem('bp_admin_token')
const [statsRes, docsRes] = await Promise.all([
fetch(`${API_BASE}/stats`, {
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
}),
fetch(`${API_BASE}/documents`, {
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
}),
])
let activeConsents = 0
let documentCount = 0
let openDSRs = 0
if (statsRes.ok) {
const statsData = await statsRes.json()
activeConsents = statsData.total_consents || statsData.active_consents || 0
}
if (docsRes.ok) {
const docsData = await docsRes.json()
documentCount = (docsData.documents || []).length
}
// Try to get DSR count
try {
const dsrRes = await fetch('/api/sdk/v1/compliance/dsr', {
headers: {
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
'X-User-ID': localStorage.getItem('bp_user_id') || '',
}
})
if (dsrRes.ok) {
const dsrData = await dsrRes.json()
const dsrs = dsrData.dsrs || []
const now = new Date()
openDSRs = dsrs.filter((r: any) => r.status !== 'completed' && r.status !== 'rejected').length
}
} catch { /* DSR endpoint might not be available */ }
setConsentStats({ activeConsents, documentCount, openDSRs })
} catch (err) {
console.error('Failed to load stats:', err)
}
}
async function loadGDPRData() {
try {
const res = await fetch('/api/sdk/v1/compliance/dsr', {
headers: {
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
'X-User-ID': localStorage.getItem('bp_user_id') || '',
}
})
if (!res.ok) return
const data = await res.json()
const dsrs = data.dsrs || []
const now = new Date()
// Count per article type
const counts: Record<string, number> = {}
const typeMapping: Record<string, string> = {
'access': '15',
'rectification': '16',
'erasure': '17',
'restriction': '18',
'portability': '20',
'objection': '21',
}
for (const dsr of dsrs) {
if (dsr.status === 'completed' || dsr.status === 'rejected') continue
const article = typeMapping[dsr.request_type]
if (article) {
counts[article] = (counts[article] || 0) + 1
}
}
setDsrCounts(counts)
// Calculate overview
const open = dsrs.filter((r: any) => r.status === 'received' || r.status === 'verified').length
const completed = dsrs.filter((r: any) => r.status === 'completed').length
const in_progress = dsrs.filter((r: any) => r.status === 'in_progress').length
const overdue = dsrs.filter((r: any) => {
if (r.status === 'completed' || r.status === 'rejected') return false
const deadline = r.extended_deadline_at ? new Date(r.extended_deadline_at) : new Date(r.deadline_at)
return deadline < now
}).length
setDsrOverview({ open, completed, in_progress, overdue })
} catch (err) {
console.error('Failed to load GDPR data:', err)
}
}
function saveEmailTemplate(template: EmailTemplateData) {
const updated = { ...savedTemplates, [template.key]: template }
setSavedTemplates(updated)
localStorage.setItem('sdk-email-templates', JSON.stringify(updated))
setEditingTemplate(null)
}
const tabs: { id: Tab; label: string }[] = [
{ id: 'documents', label: 'Dokumente' },
{ id: 'versions', label: 'Versionen' },
{ id: 'emails', label: 'E-Mail Vorlagen' },
{ id: 'gdpr', label: 'DSGVO Prozesse' },
{ id: 'stats', label: 'Statistiken' },
]
// 16 Lifecycle Email Templates
const emailTemplates = [
{ name: 'Willkommens-E-Mail', key: 'welcome', category: 'onboarding', description: 'Wird bei Registrierung versendet' },
{ name: 'E-Mail Bestaetigung', key: 'email_verification', category: 'onboarding', description: 'Bestaetigungslink fuer E-Mail-Adresse' },
{ name: 'Konto aktiviert', key: 'account_activated', category: 'onboarding', description: 'Bestaetigung der Kontoaktivierung' },
{ name: 'Passwort zuruecksetzen', key: 'password_reset', category: 'security', description: 'Link zum Zuruecksetzen des Passworts' },
{ name: 'Passwort geaendert', key: 'password_changed', category: 'security', description: 'Bestaetigung der Passwortaenderung' },
{ name: 'Neue Anmeldung', key: 'new_login', category: 'security', description: 'Benachrichtigung ueber Anmeldung von neuem Geraet' },
{ name: '2FA aktiviert', key: '2fa_enabled', category: 'security', description: 'Bestaetigung der 2FA-Aktivierung' },
{ name: 'Neue Dokumentversion', key: 'new_document_version', category: 'consent', description: 'Info ueber neue Dokumentversion zur Zustimmung' },
{ name: 'Zustimmung erteilt', key: 'consent_given', category: 'consent', description: 'Bestaetigung der erteilten Zustimmung' },
{ name: 'Zustimmung widerrufen', key: 'consent_withdrawn', category: 'consent', description: 'Bestaetigung des Widerrufs' },
{ name: 'DSR Anfrage erhalten', key: 'dsr_received', category: 'gdpr', description: 'Bestaetigung des Eingangs einer DSGVO-Anfrage' },
{ name: 'Datenexport bereit', key: 'export_ready', category: 'gdpr', description: 'Benachrichtigung ueber fertigen Datenexport' },
{ name: 'Daten geloescht', key: 'data_deleted', category: 'gdpr', description: 'Bestaetigung der Datenloeschung' },
{ name: 'Daten berichtigt', key: 'data_rectified', category: 'gdpr', description: 'Bestaetigung der Datenberichtigung' },
{ name: 'Konto deaktiviert', key: 'account_deactivated', category: 'lifecycle', description: 'Konto wurde deaktiviert' },
{ name: 'Konto geloescht', key: 'account_deleted', category: 'lifecycle', description: 'Bestaetigung der Kontoloeschung' },
]
// GDPR Article 15-21 Processes
const gdprProcesses = [
{
article: '15',
title: 'Auskunftsrecht',
description: 'Recht auf Bestaetigung und Auskunft ueber verarbeitete personenbezogene Daten',
actions: ['Datenexport generieren', 'Verarbeitungszwecke auflisten', 'Empfaenger auflisten'],
sla: '30 Tage',
},
{
article: '16',
title: 'Recht auf Berichtigung',
description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten',
actions: ['Daten bearbeiten', 'Aenderungshistorie fuehren', 'Benachrichtigung senden'],
sla: '30 Tage',
},
{
article: '17',
title: 'Recht auf Loeschung ("Vergessenwerden")',
description: 'Recht auf Loeschung personenbezogener Daten unter bestimmten Voraussetzungen',
actions: ['Loeschantrag pruefen', 'Daten loeschen', 'Aufbewahrungsfristen pruefen', 'Loeschbestaetigung senden'],
sla: '30 Tage',
},
{
article: '18',
title: 'Recht auf Einschraenkung der Verarbeitung',
description: 'Recht auf Markierung von Daten zur eingeschraenkten Verarbeitung',
actions: ['Daten markieren', 'Verarbeitung einschraenken', 'Benachrichtigung bei Aufhebung'],
sla: '30 Tage',
},
{
article: '19',
title: 'Mitteilungspflicht',
description: 'Pflicht zur Mitteilung von Berichtigung, Loeschung oder Einschraenkung an Empfaenger',
actions: ['Empfaenger ermitteln', 'Mitteilungen versenden', 'Protokollierung'],
sla: 'Unverzueglich',
},
{
article: '20',
title: 'Recht auf Datenuebertragbarkeit',
description: 'Recht auf Erhalt der Daten in strukturiertem, maschinenlesbarem Format',
actions: ['JSON/CSV Export', 'API-Schnittstelle', 'Direkte Uebertragung'],
sla: '30 Tage',
},
{
article: '21',
title: 'Widerspruchsrecht',
description: 'Recht auf Widerspruch gegen Verarbeitung aus berechtigtem Interesse oder Direktwerbung',
actions: ['Widerspruch erfassen', 'Verarbeitung stoppen', 'Marketing-Opt-out'],
sla: 'Unverzueglich',
},
]
const emailCategories = [
{ key: 'onboarding', label: 'Onboarding', color: 'bg-blue-100 text-blue-700' },
{ key: 'security', label: 'Sicherheit', color: 'bg-red-100 text-red-700' },
{ key: 'consent', label: 'Zustimmung', color: 'bg-green-100 text-green-700' },
{ key: 'gdpr', label: 'DSGVO', color: 'bg-purple-100 text-purple-700' },
{ key: 'lifecycle', label: 'Lifecycle', color: 'bg-orange-100 text-orange-700' },
]
return (
<div className="space-y-6">
<StepHeader stepId="consent-management" showProgress={true} />
{/* Token Input */}
{!authToken && (
<div className="bg-white rounded-xl border border-slate-200 p-4">
<label className="block text-sm font-medium text-slate-700 mb-2">
Admin Token
</label>
<input
type="password"
placeholder="JWT Token eingeben..."
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
onChange={(e) => {
setAuthToken(e.target.value)
localStorage.setItem('bp_admin_token', e.target.value)
}}
/>
</div>
)}
{/* Tabs */}
<div>
<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>
{/* Content */}
<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>
)}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
{/* 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">Dokumente verwalten</h2>
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium">
+ Neues Dokument
</button>
</div>
{loading ? (
<div className="text-center py-12 text-slate-500">Lade Dokumente...</div>
) : documents.length === 0 ? (
<div className="text-center py-12 text-slate-500">
Keine Dokumente vorhanden
</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">Typ</th>
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Name</th>
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Beschreibung</th>
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Pflicht</th>
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Erstellt</th>
<th className="text-right py-3 px-4 text-sm font-medium text-slate-500">Aktionen</th>
</tr>
</thead>
<tbody>
{documents.map((doc) => (
<tr key={doc.id} className="border-b border-slate-100 hover:bg-slate-50">
<td className="py-3 px-4">
<span className="px-2 py-1 bg-slate-100 text-slate-700 rounded text-xs font-medium">
{doc.type}
</span>
</td>
<td className="py-3 px-4 font-medium text-slate-900">{doc.name}</td>
<td className="py-3 px-4 text-slate-600 text-sm">{doc.description}</td>
<td className="py-3 px-4">
{doc.mandatory ? (
<span className="text-green-600">Ja</span>
) : (
<span className="text-slate-400">Nein</span>
)}
</td>
<td className="py-3 px-4 text-sm text-slate-500">
{new Date(doc.created_at).toLocaleDateString('de-DE')}
</td>
<td className="py-3 px-4 text-right">
<button
onClick={() => {
setSelectedDocument(doc.id)
setActiveTab('versions')
}}
className="text-purple-600 hover:text-purple-700 text-sm font-medium mr-3"
>
Versionen
</button>
<button className="text-slate-500 hover:text-slate-700 text-sm">
Bearbeiten
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
{/* Versions Tab */}
{activeTab === 'versions' && (
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<h2 className="text-lg font-semibold text-slate-900">Versionen</h2>
<select
value={selectedDocument}
onChange={(e) => setSelectedDocument(e.target.value)}
className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="">Dokument auswaehlen...</option>
{documents.map((doc) => (
<option key={doc.id} value={doc.id}>
{doc.name}
</option>
))}
</select>
</div>
{selectedDocument && (
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium">
+ Neue Version
</button>
)}
</div>
{!selectedDocument ? (
<div className="text-center py-12 text-slate-500">
Bitte waehlen Sie ein Dokument aus
</div>
) : loading ? (
<div className="text-center py-12 text-slate-500">Lade Versionen...</div>
) : versions.length === 0 ? (
<div className="text-center py-12 text-slate-500">
Keine Versionen vorhanden
</div>
) : (
<div className="space-y-4">
{versions.map((version) => (
<div
key={version.id}
className="border border-slate-200 rounded-lg p-4 hover:border-purple-300 transition-colors"
>
<div className="flex items-start justify-between">
<div>
<div className="flex items-center gap-2 mb-1">
<span className="font-semibold text-slate-900">v{version.version}</span>
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs">
{version.language.toUpperCase()}
</span>
<span
className={`px-2 py-0.5 rounded text-xs ${
version.status === 'published'
? 'bg-green-100 text-green-700'
: version.status === 'draft'
? 'bg-yellow-100 text-yellow-700'
: 'bg-slate-100 text-slate-600'
}`}
>
{version.status}
</span>
</div>
<h3 className="text-slate-700">{version.title}</h3>
<p className="text-sm text-slate-500 mt-1">
Erstellt: {new Date(version.created_at).toLocaleDateString('de-DE')}
</p>
</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 hover:border-slate-400">
Bearbeiten
</button>
{version.status === 'draft' && (
<button className="px-3 py-1.5 text-sm text-white bg-green-600 hover:bg-green-700 rounded-lg">
Veroeffentlichen
</button>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
)}
{/* Emails Tab - API-backed + Lifecycle Templates */}
{activeTab === 'emails' && (
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-lg font-semibold text-slate-900">E-Mail Vorlagen</h2>
<p className="text-sm text-slate-500 mt-1">
{apiEmailTemplates.length > 0
? `${apiEmailTemplates.length} DSGVO-Vorlagen aus der Datenbank`
: '16 Lifecycle-Vorlagen fuer automatisierte Kommunikation'}
</p>
</div>
<button
onClick={() => setShowCreateTemplateModal(true)}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium"
>
+ Neue Vorlage
</button>
</div>
{/* API-backed templates section */}
{templatesLoading ? (
<div className="text-center py-8 text-slate-500">Lade Vorlagen aus der Datenbank...</div>
) : apiEmailTemplates.length > 0 ? (
<div className="space-y-4 mb-8">
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wider mb-3">DSGVO-Pflichtvorlagen</h3>
{apiEmailTemplates.map((template) => (
<ApiTemplateEditor
key={template.id}
template={template}
saving={savingTemplateId === template.id}
onSave={(subject, body) => saveApiEmailTemplate({ id: template.id, subject, body })}
onPreview={(subject, body) => setPreviewTemplate({ key: template.template_key, subject, body })}
/>
))}
</div>
) : null}
{/* Category Filter for static templates */}
{apiEmailTemplates.length === 0 && (
<>
<div className="flex flex-wrap gap-2 mb-6">
<span className="text-sm text-slate-500 py-1">Filter:</span>
{emailCategories.map((cat) => (
<span key={cat.key} className={`px-3 py-1 rounded-full text-xs font-medium ${cat.color}`}>
{cat.label}
</span>
))}
</div>
{/* Templates grouped by category (fallback when no API data) */}
{emailCategories.map((category) => (
<div key={category.key} className="mb-8">
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wider mb-3 flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${category.color.split(' ')[0]}`}></span>
{category.label}
</h3>
<div className="grid gap-3">
{emailTemplates
.filter((t) => t.category === category.key)
.map((template) => (
<div
key={template.key}
className="border border-slate-200 rounded-lg p-4 flex items-center justify-between hover:border-purple-300 transition-colors bg-white"
>
<div className="flex items-center gap-4">
<div className={`w-10 h-10 rounded-lg ${category.color} flex items-center justify-center text-lg`}>
</div>
<div>
<h4 className="font-medium text-slate-900">{template.name}</h4>
<p className="text-sm text-slate-500">{template.description}</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">
{savedTemplates[template.key] ? 'Angepasst' : 'Aktiv'}
</span>
<button
onClick={() => {
const existing = savedTemplates[template.key]
setEditingTemplate({
key: template.key,
subject: existing?.subject || `Betreff: ${template.name}`,
body: existing?.body || `Sehr geehrte(r) {{name}},\n\n${template.description}\n\nMit freundlichen Gruessen\nIhr Datenschutz-Team`,
})
}}
className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400"
>
Bearbeiten
</button>
<button
onClick={() => {
const existing = savedTemplates[template.key]
setPreviewTemplate({
key: template.key,
subject: existing?.subject || `Betreff: ${template.name}`,
body: existing?.body || `Sehr geehrte(r) {{name}},\n\n${template.description}\n\nMit freundlichen Gruessen\nIhr Datenschutz-Team`,
})
}}
className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400"
>
Vorschau
</button>
</div>
</div>
))}
</div>
</div>
))}
</>
)}
</div>
)}
{/* GDPR Processes Tab - Articles 15-21 */}
{activeTab === 'gdpr' && (
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div>
<h2 className="text-lg font-semibold text-slate-900">DSGVO Betroffenenrechte</h2>
<p className="text-sm text-slate-500 mt-1">Artikel 15-21 Prozesse und Vorlagen</p>
</div>
<button
onClick={() => router.push('/sdk/dsr')}
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium"
>
+ DSR Anfrage erstellen
</button>
</div>
{/* Info Banner */}
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4 mb-6">
<div className="flex items-start gap-3">
<span className="text-2xl">*</span>
<div>
<h4 className="font-medium text-purple-900">Data Subject Rights (DSR)</h4>
<p className="text-sm text-purple-700 mt-1">
Hier verwalten Sie alle DSGVO-Anfragen. Jeder Artikel hat definierte Prozesse, SLAs und automatisierte Workflows.
</p>
</div>
</div>
</div>
{/* API-backed GDPR Processes */}
{gdprLoading ? (
<div className="text-center py-8 text-slate-500">Lade DSGVO-Prozesse...</div>
) : apiGdprProcesses.length > 0 ? (
<div className="space-y-4 mb-8">
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wider mb-3">Konfigurierte Prozesse</h3>
{apiGdprProcesses.map((process) => (
<ApiGdprProcessEditor
key={process.id}
process={process}
saving={savingProcessId === process.id}
onSave={(title, description) => saveApiGdprProcess({ id: process.id, title, description })}
/>
))}
</div>
) : null}
{/* Static GDPR Process Cards (always shown as reference) */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wider mb-3">DSGVO Artikel-Uebersicht</h3>
{gdprProcesses.map((process) => (
<div
key={process.article}
className="border border-slate-200 rounded-xl p-5 hover:border-purple-300 transition-colors bg-white"
>
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<div className="w-12 h-12 bg-purple-100 text-purple-700 rounded-lg flex items-center justify-center font-bold text-lg">
{process.article}
</div>
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<h3 className="font-semibold text-slate-900">{process.title}</h3>
<span className="px-2 py-0.5 bg-green-100 text-green-700 rounded text-xs">Aktiv</span>
</div>
<p className="text-sm text-slate-600 mb-3">{process.description}</p>
{/* Actions */}
<div className="flex flex-wrap gap-2 mb-3">
{process.actions.map((action, idx) => (
<span key={idx} className="px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs">
{action}
</span>
))}
</div>
{/* SLA */}
<div className="flex items-center gap-4 text-sm">
<span className="text-slate-500">
SLA: <span className="font-medium text-slate-700">{process.sla}</span>
</span>
<span className="text-slate-300">|</span>
<span className="text-slate-500">
Offene Anfragen: <span className={`font-medium ${(dsrCounts[process.article] || 0) > 0 ? 'text-orange-600' : 'text-slate-700'}`}>{dsrCounts[process.article] || 0}</span>
</span>
</div>
</div>
</div>
<div className="flex flex-col gap-2">
<Link
href={`/sdk/dsr?type=${process.article === '15' ? 'access' : process.article === '16' ? 'rectification' : process.article === '17' ? 'erasure' : process.article === '18' ? 'restriction' : process.article === '20' ? 'portability' : 'objection'}`}
className="px-3 py-1.5 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg text-center"
>
Anfragen
</Link>
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400">
Vorlage
</button>
</div>
</div>
</div>
))}
</div>
{/* DSR Request Statistics */}
<div className="mt-8 pt-6 border-t border-slate-200">
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wider mb-4">DSR Uebersicht</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-slate-50 rounded-lg p-4 text-center">
<div className={`text-2xl font-bold ${dsrOverview.open > 0 ? 'text-blue-600' : 'text-slate-900'}`}>{dsrOverview.open}</div>
<div className="text-xs text-slate-500 mt-1">Offen</div>
</div>
<div className="bg-green-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-green-700">{dsrOverview.completed}</div>
<div className="text-xs text-slate-500 mt-1">Erledigt</div>
</div>
<div className="bg-yellow-50 rounded-lg p-4 text-center">
<div className="text-2xl font-bold text-yellow-700">{dsrOverview.in_progress}</div>
<div className="text-xs text-slate-500 mt-1">In Bearbeitung</div>
</div>
<div className="bg-red-50 rounded-lg p-4 text-center">
<div className={`text-2xl font-bold ${dsrOverview.overdue > 0 ? 'text-red-700' : 'text-slate-400'}`}>{dsrOverview.overdue}</div>
<div className="text-xs text-slate-500 mt-1">Ueberfaellig</div>
</div>
</div>
</div>
</div>
)}
{/* Stats Tab */}
{activeTab === 'stats' && (
<div className="p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-6">Statistiken</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-slate-50 rounded-xl p-6">
<div className="text-3xl font-bold text-slate-900">{consentStats.activeConsents}</div>
<div className="text-sm text-slate-500 mt-1">Aktive Zustimmungen</div>
</div>
<div className="bg-slate-50 rounded-xl p-6">
<div className="text-3xl font-bold text-slate-900">{consentStats.documentCount}</div>
<div className="text-sm text-slate-500 mt-1">Dokumente</div>
</div>
<div className="bg-slate-50 rounded-xl p-6">
<div className={`text-3xl font-bold ${consentStats.openDSRs > 0 ? 'text-orange-600' : 'text-slate-900'}`}>
{consentStats.openDSRs}
</div>
<div className="text-sm text-slate-500 mt-1">Offene DSR-Anfragen</div>
</div>
</div>
<div className="border border-slate-200 rounded-lg p-6">
<h3 className="font-semibold text-slate-900 mb-4">Zustimmungsrate nach Dokument</h3>
<div className="text-center py-8 text-slate-400 text-sm">
Diagramm wird in einer zukuenftigen Version verfuegbar sein
</div>
</div>
</div>
)}
</div>
</div>
{/* Email Template Edit Modal */}
{editingTemplate && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
<h3 className="font-semibold text-slate-900">E-Mail Vorlage bearbeiten</h3>
<button onClick={() => setEditingTemplate(null)} className="text-slate-400 hover:text-slate-600">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Betreff</label>
<input
type="text"
value={editingTemplate.subject}
onChange={(e) => setEditingTemplate({ ...editingTemplate, subject: 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">Inhalt</label>
<textarea
value={editingTemplate.body}
onChange={(e) => setEditingTemplate({ ...editingTemplate, body: e.target.value })}
rows={12}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono"
/>
</div>
<div className="bg-slate-50 rounded-lg p-3">
<div className="text-xs font-medium text-slate-500 mb-1">Verfuegbare Platzhalter:</div>
<div className="flex flex-wrap gap-2">
{['{{name}}', '{{email}}', '{{referenceNumber}}', '{{date}}', '{{deadline}}', '{{company}}'].map(v => (
<span key={v} className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs font-mono">{v}</span>
))}
</div>
</div>
</div>
<div className="px-6 py-4 border-t border-slate-200 flex justify-end gap-3">
<button onClick={() => setEditingTemplate(null)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg">
Abbrechen
</button>
<button
onClick={() => saveEmailTemplate(editingTemplate)}
className="px-4 py-2 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg font-medium"
>
Speichern
</button>
</div>
</div>
</div>
)}
{/* Consent Template Create Modal */}
{showCreateTemplateModal && (
<ConsentTemplateCreateModal
onClose={() => setShowCreateTemplateModal(false)}
onSuccess={() => { setShowCreateTemplateModal(false); loadApiEmailTemplates() }}
/>
)}
{/* Email Template Preview Modal */}
{previewTemplate && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
<h3 className="font-semibold text-slate-900">Vorschau</h3>
<button onClick={() => setPreviewTemplate(null)} className="text-slate-400 hover:text-slate-600">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="p-6 space-y-4">
<div className="bg-slate-50 rounded-lg p-4">
<div className="text-xs text-slate-500 mb-1">Betreff:</div>
<div className="font-medium text-slate-900">
{previewTemplate.subject
.replace(/\{\{name\}\}/g, 'Max Mustermann')
.replace(/\{\{email\}\}/g, 'max@example.de')
.replace(/\{\{referenceNumber\}\}/g, 'DSR-2025-000001')
.replace(/\{\{date\}\}/g, new Date().toLocaleDateString('de-DE'))
.replace(/\{\{deadline\}\}/g, '30 Tage')
.replace(/\{\{company\}\}/g, 'BreakPilot GmbH')
}
</div>
</div>
<div className="border border-slate-200 rounded-lg p-4 whitespace-pre-wrap text-sm text-slate-700">
{previewTemplate.body
.replace(/\{\{name\}\}/g, 'Max Mustermann')
.replace(/\{\{email\}\}/g, 'max@example.de')
.replace(/\{\{referenceNumber\}\}/g, 'DSR-2025-000001')
.replace(/\{\{date\}\}/g, new Date().toLocaleDateString('de-DE'))
.replace(/\{\{deadline\}\}/g, '30 Tage')
.replace(/\{\{company\}\}/g, 'BreakPilot GmbH')
}
</div>
</div>
<div className="px-6 py-4 border-t border-slate-200 flex justify-end">
<button onClick={() => setPreviewTemplate(null)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg">
Schliessen
</button>
</div>
</div>
</div>
)}
</div>
)
}