'use client'
import React, { useState } from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import {
Database,
FileText,
Cookie,
Clock,
LayoutGrid,
X,
History,
Shield,
AlertTriangle,
CheckCircle,
XCircle,
Monitor,
Globe,
Calendar,
User,
FileCheck,
} from 'lucide-react'
// =============================================================================
// NAVIGATION TABS
// =============================================================================
const EINWILLIGUNGEN_TABS = [
{
id: 'overview',
label: 'Übersicht',
href: '/sdk/einwilligungen',
icon: LayoutGrid,
description: 'Consent-Tracking Dashboard',
},
{
id: 'catalog',
label: 'Datenpunktkatalog',
href: '/sdk/einwilligungen/catalog',
icon: Database,
description: '18 Kategorien, 128 Datenpunkte',
},
{
id: 'privacy-policy',
label: 'DSI Generator',
href: '/sdk/einwilligungen/privacy-policy',
icon: FileText,
description: 'Datenschutzinformation erstellen',
},
{
id: 'cookie-banner',
label: 'Cookie-Banner',
href: '/sdk/einwilligungen/cookie-banner',
icon: Cookie,
description: 'Cookie-Consent konfigurieren',
},
{
id: 'retention',
label: 'Löschmatrix',
href: '/sdk/einwilligungen/retention',
icon: Clock,
description: 'Aufbewahrungsfristen verwalten',
},
]
function EinwilligungenNavTabs() {
const pathname = usePathname()
return (
{EINWILLIGUNGEN_TABS.map((tab) => {
const Icon = tab.icon
const isActive = pathname === tab.href
return (
{tab.label}
{tab.description}
)
})}
)
}
// =============================================================================
// TYPES
// =============================================================================
type ConsentType = 'marketing' | 'analytics' | 'newsletter' | 'terms' | 'privacy' | 'cookies'
type ConsentStatus = 'granted' | 'withdrawn' | 'pending'
type HistoryAction = 'granted' | 'withdrawn' | 'version_update' | 'renewed'
interface ConsentHistoryEntry {
id: string
action: HistoryAction
timestamp: Date
version: string
documentTitle?: string
ipAddress: string
userAgent: string
source: string
notes?: string
}
interface ConsentRecord {
id: string
identifier: string
email: string
firstName?: string
lastName?: string
consentType: ConsentType
status: ConsentStatus
currentVersion: string
grantedAt: Date | null
withdrawnAt: Date | null
source: string | null
ipAddress: string
userAgent: string
history: ConsentHistoryEntry[]
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
const typeLabels: Record = {
marketing: 'Marketing',
analytics: 'Analyse',
newsletter: 'Newsletter',
terms: 'AGB',
privacy: 'Datenschutz',
cookies: 'Cookies',
}
const typeColors: Record = {
marketing: 'bg-purple-100 text-purple-700',
analytics: 'bg-blue-100 text-blue-700',
newsletter: 'bg-green-100 text-green-700',
terms: 'bg-yellow-100 text-yellow-700',
privacy: 'bg-orange-100 text-orange-700',
cookies: 'bg-pink-100 text-pink-700',
}
const statusColors: Record = {
granted: 'bg-green-100 text-green-700',
withdrawn: 'bg-red-100 text-red-700',
pending: 'bg-yellow-100 text-yellow-700',
}
const statusLabels: Record = {
granted: 'Erteilt',
withdrawn: 'Widerrufen',
pending: 'Ausstehend',
}
const actionLabels: Record = {
granted: 'Einwilligung erteilt',
withdrawn: 'Einwilligung widerrufen',
version_update: 'Neue Version akzeptiert',
renewed: 'Einwilligung erneuert',
}
const actionIcons: Record = {
granted: ,
withdrawn: ,
version_update: ,
renewed: ,
}
function formatDateTime(date: Date): string {
return date.toLocaleString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
}
function formatDate(date: Date | null): string {
if (!date) return '-'
return date.toLocaleDateString('de-DE')
}
// =============================================================================
// DETAIL MODAL COMPONENT
// =============================================================================
interface ConsentDetailModalProps {
record: ConsentRecord
onClose: () => void
onRevoke: (recordId: string) => void
}
function ConsentDetailModal({ record, onClose, onRevoke }: ConsentDetailModalProps) {
const [showRevokeConfirm, setShowRevokeConfirm] = useState(false)
return (
{/* Header */}
Consent-Details
{record.email}
{/* Content */}
{/* User Info */}
Benutzerinformationen
Name:
{record.firstName} {record.lastName}
E-Mail:
{record.email}
User-ID:
{record.identifier}
Consent-Status
Typ:
{typeLabels[record.consentType]}
Status:
{statusLabels[record.status]}
Version:
v{record.currentVersion}
{/* Technical Details */}
Technische Details (letzter Consent)
IP-Adresse
{record.ipAddress}
Quelle
{record.source ?? '—'}
User-Agent
{record.userAgent}
{/* History Timeline */}
Consent-Historie
{record.history.length} Einträge
{/* Timeline line */}
{record.history.map((entry, index) => (
{/* Icon */}
{actionIcons[entry.action]}
{/* Content */}
{actionLabels[entry.action]}
{entry.documentTitle && (
{entry.documentTitle}
)}
{formatDateTime(entry.timestamp)}
{entry.ipAddress}
Quelle: {entry.source}
{entry.notes && (
{entry.notes}
)}
{/* Expandable User-Agent */}
User-Agent anzeigen
{entry.userAgent}
))}
{/* Footer Actions */}
Consent-ID: {record.id}
{record.status === 'granted' && !showRevokeConfirm && (
setShowRevokeConfirm(true)}
className="flex items-center gap-2 px-4 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
>
Widerrufen
)}
{showRevokeConfirm && (
Wirklich widerrufen?
{
onRevoke(record.id)
onClose()
}}
className="px-3 py-1.5 bg-red-600 text-white text-sm rounded-lg hover:bg-red-700"
>
Ja, widerrufen
setShowRevokeConfirm(false)}
className="px-3 py-1.5 bg-gray-200 text-gray-700 text-sm rounded-lg hover:bg-gray-300"
>
Abbrechen
)}
Schließen
)
}
// =============================================================================
// TABLE ROW COMPONENT
// =============================================================================
interface ConsentRecordRowProps {
record: ConsentRecord
onShowDetails: (record: ConsentRecord) => void
}
function ConsentRecordRow({ record, onShowDetails }: ConsentRecordRowProps) {
return (
{record.email}
{record.identifier}
{typeLabels[record.consentType]}
{statusLabels[record.status]}
{formatDate(record.grantedAt)}
{formatDate(record.withdrawnAt)}
v{record.currentVersion}
{record.history.length}
onShowDetails(record)}
className="text-sm text-purple-600 hover:text-purple-700 font-medium"
>
Details
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function EinwilligungenPage() {
const { state } = useSDK()
const [records, setRecords] = useState([])
const [filter, setFilter] = useState('all')
const [searchQuery, setSearchQuery] = useState('')
const [selectedRecord, setSelectedRecord] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const PAGE_SIZE = 50
const [currentPage, setCurrentPage] = useState(1)
const [totalRecords, setTotalRecords] = useState(0)
const [globalStats, setGlobalStats] = useState({ total: 0, active: 0, revoked: 0 })
const loadConsents = React.useCallback(async (page: number) => {
setIsLoading(true)
try {
const offset = (page - 1) * PAGE_SIZE
const listResponse = await fetch(
`/api/sdk/v1/einwilligungen/consent?limit=${PAGE_SIZE}&offset=${offset}`
)
if (listResponse.ok) {
const listData = await listResponse.json()
setTotalRecords(listData.total ?? 0)
if (listData.consents?.length > 0) {
const mapped: ConsentRecord[] = listData.consents.map((c: {
id: string
user_id: string
data_point_id: string
granted: boolean
granted_at: string
revoked_at?: string
consent_version?: string
source?: string
ip_address?: string
user_agent?: string
history?: Array<{
id: string
action: string
created_at: string
consent_version?: string
ip_address?: string
user_agent?: string
source?: string
}>
}) => ({
id: c.id,
identifier: c.user_id,
email: c.user_id,
consentType: (c.data_point_id as ConsentType) || 'privacy',
status: (c.revoked_at ? 'withdrawn' : 'granted') as ConsentStatus,
currentVersion: c.consent_version || '1.0',
grantedAt: c.granted_at ? new Date(c.granted_at) : null,
withdrawnAt: c.revoked_at ? new Date(c.revoked_at) : null,
source: c.source ?? null,
ipAddress: c.ip_address ?? '',
userAgent: c.user_agent ?? '',
history: (c.history ?? []).map(h => ({
id: h.id,
action: h.action as HistoryAction,
timestamp: new Date(h.created_at),
version: h.consent_version || '1.0',
ipAddress: h.ip_address ?? '',
userAgent: h.user_agent ?? '',
source: h.source ?? '',
})),
}))
setRecords(mapped)
} else {
setRecords([])
}
}
} catch {
// Backend nicht erreichbar, leere Liste anzeigen
} finally {
setIsLoading(false)
}
}, [])
const loadStats = React.useCallback(async () => {
try {
const res = await fetch('/api/sdk/v1/einwilligungen/consent?stats=true')
if (res.ok) {
const data = await res.json()
const s = data.statistics
if (s) {
setGlobalStats({
total: s.total_consents ?? 0,
active: s.active_consents ?? 0,
revoked: s.revoked_consents ?? 0,
})
setTotalRecords(s.total_consents ?? 0)
}
}
} catch {
// Statistiken nicht erreichbar — lokale Werte behalten
}
}, [])
React.useEffect(() => {
loadStats()
}, [loadStats])
React.useEffect(() => { loadConsents(currentPage) }, [currentPage, loadConsents])
const filteredRecords = records.filter(record => {
const matchesFilter = filter === 'all' || record.consentType === filter || record.status === filter
const matchesSearch = searchQuery === '' ||
record.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
record.identifier.toLowerCase().includes(searchQuery.toLowerCase())
return matchesFilter && matchesSearch
})
const versionUpdates = records.reduce((acc, r) => acc + r.history.filter(h => h.action === 'version_update').length, 0)
const handleRevoke = async (recordId: string) => {
try {
const response = await fetch('/api/sdk/v1/einwilligungen/consent', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ consentId: recordId, action: 'revoke' }),
})
if (response.ok) {
const now = new Date()
setRecords(prev => prev.map(r => {
if (r.id === recordId) {
return {
...r,
status: 'withdrawn' as ConsentStatus,
withdrawnAt: now,
history: [
...r.history,
{
id: `h-${recordId}-${r.history.length + 1}`,
action: 'withdrawn' as HistoryAction,
timestamp: now,
version: r.currentVersion,
ipAddress: 'Admin-Portal',
userAgent: 'Admin Action',
source: 'Manueller Widerruf durch Admin',
notes: 'Widerruf über Admin-Portal durchgeführt',
},
],
}
}
return r
}))
loadStats()
}
} catch {
// Fallback: update local state even if API call fails
const now = new Date()
setRecords(prev => prev.map(r => {
if (r.id === recordId) {
return { ...r, status: 'withdrawn' as ConsentStatus, withdrawnAt: now }
}
return r
}))
}
}
const stepInfo = STEP_EXPLANATIONS['einwilligungen']
return (
{/* Step Header */}
Export
{/* Navigation Tabs */}
{/* Stats */}
Gesamt
{globalStats.total}
Aktive Einwilligungen
{globalStats.active}
Widerrufen
{globalStats.revoked}
Versions-Updates
{versionUpdates}
{/* Info Banner */}
Consent-Historie aktiviert
Alle Änderungen an Einwilligungen werden protokolliert, inkl. Zustimmungen zu neuen Versionen von AGB, DSI und anderen Dokumenten.
Klicken Sie auf "Details" um die vollständige Historie eines Nutzers einzusehen.
{/* Search and Filter */}
{['all', 'granted', 'withdrawn', 'terms', 'privacy', 'cookies', 'marketing', 'analytics'].map(f => (
setFilter(f)}
className={`px-3 py-1 text-sm rounded-full transition-colors ${
filter === f
? 'bg-purple-600 text-white'
: 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{f === 'all' ? 'Alle' :
f === 'granted' ? 'Erteilt' :
f === 'withdrawn' ? 'Widerrufen' :
f === 'terms' ? 'AGB' :
f === 'privacy' ? 'DSI' :
f === 'cookies' ? 'Cookies' :
f === 'marketing' ? 'Marketing' : 'Analyse'}
))}
{/* Records Table */}
Nutzer
Typ
Status
Erteilt am
Widerrufen am
Version
Historie
Aktion
{filteredRecords.map(record => (
))}
{filteredRecords.length === 0 && (
Keine Einträge gefunden
Passen Sie die Suche oder den Filter an.
)}
{/* Pagination */}
{(() => {
const totalPages = Math.ceil(totalRecords / PAGE_SIZE)
return (
Zeige {totalRecords === 0 ? 0 : (currentPage - 1) * PAGE_SIZE + 1}–
{Math.min(currentPage * PAGE_SIZE, totalRecords)} von {totalRecords} Einträgen
setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
className="px-3 py-1 text-sm text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50"
>
Zurück
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
const page = Math.max(1, Math.min(currentPage - 2, totalPages - 4)) + i
if (page < 1 || page > totalPages) return null
return (
setCurrentPage(page)}
className={`px-3 py-1 text-sm rounded-lg ${
page === currentPage
? 'text-white bg-purple-600'
: 'text-gray-600 bg-gray-100 hover:bg-gray-200'
}`}
>
{page}
)
})}
setCurrentPage(p => Math.min(totalPages || 1, p + 1))}
disabled={currentPage >= (totalPages || 1)}
className="px-3 py-1 text-sm text-gray-600 bg-gray-100 rounded-lg hover:bg-gray-200 disabled:opacity-50"
>
Weiter
)
})()}
{/* Detail Modal */}
{selectedRecord && (
setSelectedRecord(null)}
onRevoke={handleRevoke}
/>
)}
)
}