diff --git a/admin-compliance/app/sdk/einwilligungen/_components/ConsentDetailModal.tsx b/admin-compliance/app/sdk/einwilligungen/_components/ConsentDetailModal.tsx
new file mode 100644
index 0000000..0db8ddf
--- /dev/null
+++ b/admin-compliance/app/sdk/einwilligungen/_components/ConsentDetailModal.tsx
@@ -0,0 +1,247 @@
+'use client'
+
+import { useState } from 'react'
+import {
+ X,
+ History,
+ Shield,
+ AlertTriangle,
+ Monitor,
+ Globe,
+ Calendar,
+ User,
+} from 'lucide-react'
+import {
+ ConsentRecord,
+ typeColors,
+ typeLabels,
+ statusColors,
+ statusLabels,
+ actionLabels,
+ actionIcons,
+ formatDateTime,
+} from '../_types'
+
+interface ConsentDetailModalProps {
+ record: ConsentRecord
+ onClose: () => void
+ onRevoke: (recordId: string) => void
+}
+
+export 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) => (
+
+ {/* 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 && (
+
+ )}
+
+ {showRevokeConfirm && (
+
+ Wirklich widerrufen?
+
+
+
+ )}
+
+
+
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/einwilligungen/_components/ConsentRecordRow.tsx b/admin-compliance/app/sdk/einwilligungen/_components/ConsentRecordRow.tsx
new file mode 100644
index 0000000..985ce58
--- /dev/null
+++ b/admin-compliance/app/sdk/einwilligungen/_components/ConsentRecordRow.tsx
@@ -0,0 +1,60 @@
+'use client'
+
+import { History } from 'lucide-react'
+import {
+ ConsentRecord,
+ typeColors,
+ typeLabels,
+ statusColors,
+ statusLabels,
+ formatDate,
+} from '../_types'
+
+interface ConsentRecordRowProps {
+ record: ConsentRecord
+ onShowDetails: (record: ConsentRecord) => void
+}
+
+export 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}
+
+ |
+
+
+ |
+
+ )
+}
diff --git a/admin-compliance/app/sdk/einwilligungen/_components/EinwilligungenNavTabs.tsx b/admin-compliance/app/sdk/einwilligungen/_components/EinwilligungenNavTabs.tsx
new file mode 100644
index 0000000..02ffc47
--- /dev/null
+++ b/admin-compliance/app/sdk/einwilligungen/_components/EinwilligungenNavTabs.tsx
@@ -0,0 +1,84 @@
+'use client'
+
+import Link from 'next/link'
+import { usePathname } from 'next/navigation'
+import {
+ Database,
+ FileText,
+ Cookie,
+ Clock,
+ LayoutGrid,
+} from 'lucide-react'
+
+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',
+ },
+]
+
+export function EinwilligungenNavTabs() {
+ const pathname = usePathname()
+
+ return (
+
+
+ {EINWILLIGUNGEN_TABS.map((tab) => {
+ const Icon = tab.icon
+ const isActive = pathname === tab.href
+
+ return (
+
+
+
+
+ {tab.label}
+
+
{tab.description}
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/einwilligungen/_components/Pagination.tsx b/admin-compliance/app/sdk/einwilligungen/_components/Pagination.tsx
new file mode 100644
index 0000000..4b3c869
--- /dev/null
+++ b/admin-compliance/app/sdk/einwilligungen/_components/Pagination.tsx
@@ -0,0 +1,55 @@
+'use client'
+
+import { PAGE_SIZE } from '../_types'
+
+interface PaginationProps {
+ currentPage: number
+ totalRecords: number
+ onPageChange: (page: number) => void
+}
+
+export function Pagination({ currentPage, totalRecords, onPageChange }: PaginationProps) {
+ 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
+
+
+
+ {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 (
+
+ )
+ })}
+
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/einwilligungen/_components/RecordsTable.tsx b/admin-compliance/app/sdk/einwilligungen/_components/RecordsTable.tsx
new file mode 100644
index 0000000..a2dee2e
--- /dev/null
+++ b/admin-compliance/app/sdk/einwilligungen/_components/RecordsTable.tsx
@@ -0,0 +1,48 @@
+'use client'
+
+import { ConsentRecord } from '../_types'
+import { ConsentRecordRow } from './ConsentRecordRow'
+
+interface RecordsTableProps {
+ records: ConsentRecord[]
+ onShowDetails: (record: ConsentRecord) => void
+}
+
+export function RecordsTable({ records, onShowDetails }: RecordsTableProps) {
+ return (
+
+
+
+
+
+ | Nutzer |
+ Typ |
+ Status |
+ Erteilt am |
+ Widerrufen am |
+ Version |
+ Historie |
+ Aktion |
+
+
+
+ {records.map(record => (
+
+ ))}
+
+
+
+
+ {records.length === 0 && (
+
+
Keine Einträge gefunden
+
Passen Sie die Suche oder den Filter an.
+
+ )}
+
+ )
+}
diff --git a/admin-compliance/app/sdk/einwilligungen/_components/SearchAndFilter.tsx b/admin-compliance/app/sdk/einwilligungen/_components/SearchAndFilter.tsx
new file mode 100644
index 0000000..09f611a
--- /dev/null
+++ b/admin-compliance/app/sdk/einwilligungen/_components/SearchAndFilter.tsx
@@ -0,0 +1,54 @@
+'use client'
+
+interface SearchAndFilterProps {
+ searchQuery: string
+ onSearchChange: (v: string) => void
+ filter: string
+ onFilterChange: (v: string) => void
+}
+
+const FILTER_OPTIONS = ['all', 'granted', 'withdrawn', 'terms', 'privacy', 'cookies', 'marketing', 'analytics']
+
+function filterLabel(f: string): string {
+ return f === 'all' ? 'Alle' :
+ f === 'granted' ? 'Erteilt' :
+ f === 'withdrawn' ? 'Widerrufen' :
+ f === 'terms' ? 'AGB' :
+ f === 'privacy' ? 'DSI' :
+ f === 'cookies' ? 'Cookies' :
+ f === 'marketing' ? 'Marketing' : 'Analyse'
+}
+
+export function SearchAndFilter({ searchQuery, onSearchChange, filter, onFilterChange }: SearchAndFilterProps) {
+ return (
+
+
+
+
onSearchChange(e.target.value)}
+ placeholder="E-Mail oder User-ID suchen..."
+ className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
+ />
+
+
+ {FILTER_OPTIONS.map(f => (
+
+ ))}
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/einwilligungen/_components/StatsGrid.tsx b/admin-compliance/app/sdk/einwilligungen/_components/StatsGrid.tsx
new file mode 100644
index 0000000..68a7d8e
--- /dev/null
+++ b/admin-compliance/app/sdk/einwilligungen/_components/StatsGrid.tsx
@@ -0,0 +1,31 @@
+'use client'
+
+interface StatsGridProps {
+ total: number
+ active: number
+ revoked: number
+ versionUpdates: number
+}
+
+export function StatsGrid({ total, active, revoked, versionUpdates }: StatsGridProps) {
+ return (
+
+
+
+
Aktive Einwilligungen
+
{active}
+
+
+
Widerrufen
+
{revoked}
+
+
+
Versions-Updates
+
{versionUpdates}
+
+
+ )
+}
diff --git a/admin-compliance/app/sdk/einwilligungen/_hooks/useConsents.ts b/admin-compliance/app/sdk/einwilligungen/_hooks/useConsents.ts
new file mode 100644
index 0000000..da8cf81
--- /dev/null
+++ b/admin-compliance/app/sdk/einwilligungen/_hooks/useConsents.ts
@@ -0,0 +1,171 @@
+'use client'
+
+import { useCallback, useEffect, useState } from 'react'
+import {
+ ConsentRecord,
+ ConsentStatus,
+ ConsentType,
+ HistoryAction,
+ PAGE_SIZE,
+} from '../_types'
+
+interface GlobalStats {
+ total: number
+ active: number
+ revoked: number
+}
+
+export function useConsents() {
+ const [records, setRecords] = useState([])
+ const [isLoading, setIsLoading] = useState(true)
+ const [currentPage, setCurrentPage] = useState(1)
+ const [totalRecords, setTotalRecords] = useState(0)
+ const [globalStats, setGlobalStats] = useState({ total: 0, active: 0, revoked: 0 })
+
+ const loadConsents = 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 = 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
+ }
+ }, [])
+
+ useEffect(() => {
+ loadStats()
+ }, [loadStats])
+
+ useEffect(() => { loadConsents(currentPage) }, [currentPage, loadConsents])
+
+ const handleRevoke = useCallback(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
+ }))
+ }
+ }, [loadStats])
+
+ return {
+ records,
+ isLoading,
+ currentPage,
+ setCurrentPage,
+ totalRecords,
+ globalStats,
+ handleRevoke,
+ }
+}
diff --git a/admin-compliance/app/sdk/einwilligungen/_types.ts b/admin-compliance/app/sdk/einwilligungen/_types.ts
new file mode 100644
index 0000000..9245a9f
--- /dev/null
+++ b/admin-compliance/app/sdk/einwilligungen/_types.ts
@@ -0,0 +1,102 @@
+import React from 'react'
+import {
+ CheckCircle,
+ XCircle,
+ FileCheck,
+ Shield,
+} from 'lucide-react'
+
+export type ConsentType = 'marketing' | 'analytics' | 'newsletter' | 'terms' | 'privacy' | 'cookies'
+export type ConsentStatus = 'granted' | 'withdrawn' | 'pending'
+export type HistoryAction = 'granted' | 'withdrawn' | 'version_update' | 'renewed'
+
+export interface ConsentHistoryEntry {
+ id: string
+ action: HistoryAction
+ timestamp: Date
+ version: string
+ documentTitle?: string
+ ipAddress: string
+ userAgent: string
+ source: string
+ notes?: string
+}
+
+export 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[]
+}
+
+export const typeLabels: Record = {
+ marketing: 'Marketing',
+ analytics: 'Analyse',
+ newsletter: 'Newsletter',
+ terms: 'AGB',
+ privacy: 'Datenschutz',
+ cookies: 'Cookies',
+}
+
+export 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',
+}
+
+export const statusColors: Record = {
+ granted: 'bg-green-100 text-green-700',
+ withdrawn: 'bg-red-100 text-red-700',
+ pending: 'bg-yellow-100 text-yellow-700',
+}
+
+export const statusLabels: Record = {
+ granted: 'Erteilt',
+ withdrawn: 'Widerrufen',
+ pending: 'Ausstehend',
+}
+
+export const actionLabels: Record = {
+ granted: 'Einwilligung erteilt',
+ withdrawn: 'Einwilligung widerrufen',
+ version_update: 'Neue Version akzeptiert',
+ renewed: 'Einwilligung erneuert',
+}
+
+export const actionIcons: Record = {
+ granted: React.createElement(CheckCircle, { className: 'w-5 h-5 text-green-500' }),
+ withdrawn: React.createElement(XCircle, { className: 'w-5 h-5 text-red-500' }),
+ version_update: React.createElement(FileCheck, { className: 'w-5 h-5 text-blue-500' }),
+ renewed: React.createElement(Shield, { className: 'w-5 h-5 text-purple-500' }),
+}
+
+export 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',
+ })
+}
+
+export function formatDate(date: Date | null): string {
+ if (!date) return '-'
+ return date.toLocaleDateString('de-DE')
+}
+
+export const PAGE_SIZE = 50
diff --git a/admin-compliance/app/sdk/einwilligungen/page.tsx b/admin-compliance/app/sdk/einwilligungen/page.tsx
index d30b550..8f07199 100644
--- a/admin-compliance/app/sdk/einwilligungen/page.tsx
+++ b/admin-compliance/app/sdk/einwilligungen/page.tsx
@@ -1,594 +1,31 @@
'use client'
-import React, { useState } from 'react'
-import Link from 'next/link'
-import { usePathname } from 'next/navigation'
-import { useSDK } from '@/lib/sdk'
+import { useState } from 'react'
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'
+import { History } 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 && (
-
- )}
-
- {showRevokeConfirm && (
-
- Wirklich widerrufen?
-
-
-
- )}
-
-
-
-
-
-
- )
-}
-
-// =============================================================================
-// 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}
-
- |
-
-
- |
-
- )
-}
-
-// =============================================================================
-// MAIN PAGE
-// =============================================================================
+import { ConsentRecord } from './_types'
+import { useConsents } from './_hooks/useConsents'
+import { EinwilligungenNavTabs } from './_components/EinwilligungenNavTabs'
+import { StatsGrid } from './_components/StatsGrid'
+import { SearchAndFilter } from './_components/SearchAndFilter'
+import { RecordsTable } from './_components/RecordsTable'
+import { Pagination } from './_components/Pagination'
+import { ConsentDetailModal } from './_components/ConsentDetailModal'
export default function EinwilligungenPage() {
- const { state } = useSDK()
- const [records, setRecords] = useState([])
+ const {
+ records,
+ currentPage,
+ setCurrentPage,
+ totalRecords,
+ globalStats,
+ handleRevoke,
+ } = useConsents()
+
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
@@ -598,53 +35,10 @@ export default function EinwilligungenPage() {
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 versionUpdates = records.reduce(
+ (acc, r) => acc + r.history.filter(h => h.action === 'version_update').length,
+ 0,
+ )
const stepInfo = STEP_EXPLANATIONS['einwilligungen']
@@ -670,24 +64,12 @@ export default function EinwilligungenPage() {
{/* Stats */}
-
-
-
Gesamt
-
{globalStats.total}
-
-
-
Aktive Einwilligungen
-
{globalStats.active}
-
-
-
Widerrufen
-
{globalStats.revoked}
-
-
-
Versions-Updates
-
{versionUpdates}
-
-
+
{/* Info Banner */}
@@ -702,123 +84,22 @@ export default function EinwilligungenPage() {
{/* Search and Filter */}
-
-
-
-
setSearchQuery(e.target.value)}
- placeholder="E-Mail oder User-ID suchen..."
- className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
- />
-
-
- {['all', 'granted', 'withdrawn', 'terms', 'privacy', 'cookies', 'marketing', 'analytics'].map(f => (
-
- ))}
-
-
+
{/* 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
-
-
-
- {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 (
-
- )
- })}
-
-
-
- )
- })()}
+
{/* Detail Modal */}
{selectedRecord && (