refactor(admin): split einwilligungen page.tsx into colocated components
Extract nav tabs, detail modal, table row, stats grid, search/filter, records table, pagination, and data-loading hook into _components/ and _hooks/. page.tsx reduced from 833 to 114 LOC. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between bg-gradient-to-r from-purple-50 to-indigo-50">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900">Consent-Details</h2>
|
||||
<p className="text-sm text-gray-500">{record.email}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-white/50 rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto p-6 space-y-6">
|
||||
{/* User Info */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="bg-gray-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<User className="w-5 h-5 text-gray-400" />
|
||||
<span className="font-medium text-gray-700">Benutzerinformationen</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Name:</span>
|
||||
<span className="font-medium">{record.firstName} {record.lastName}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">E-Mail:</span>
|
||||
<span className="font-medium">{record.email}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">User-ID:</span>
|
||||
<span className="font-mono text-xs bg-gray-200 px-2 py-0.5 rounded">{record.identifier}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Shield className="w-5 h-5 text-gray-400" />
|
||||
<span className="font-medium text-gray-700">Consent-Status</span>
|
||||
</div>
|
||||
<div className="space-y-2 text-sm">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">Typ:</span>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${typeColors[record.consentType]}`}>
|
||||
{typeLabels[record.consentType]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-500">Status:</span>
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${statusColors[record.status]}`}>
|
||||
{statusLabels[record.status]}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-gray-500">Version:</span>
|
||||
<span className="font-mono font-medium">v{record.currentVersion}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Technical Details */}
|
||||
<div className="bg-gray-50 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Monitor className="w-5 h-5 text-gray-400" />
|
||||
<span className="font-medium text-gray-700">Technische Details (letzter Consent)</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<div className="text-gray-500 mb-1">IP-Adresse</div>
|
||||
<div className="font-mono text-xs bg-white px-3 py-2 rounded border">{record.ipAddress}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-gray-500 mb-1">Quelle</div>
|
||||
<div className="bg-white px-3 py-2 rounded border">{record.source ?? '—'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<div className="text-gray-500 mb-1">User-Agent</div>
|
||||
<div className="font-mono text-xs bg-white px-3 py-2 rounded border break-all">{record.userAgent}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* History Timeline */}
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<History className="w-5 h-5 text-purple-600" />
|
||||
<span className="font-semibold text-gray-900">Consent-Historie</span>
|
||||
<span className="text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded-full">
|
||||
{record.history.length} Einträge
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
{/* Timeline line */}
|
||||
<div className="absolute left-[22px] top-0 bottom-0 w-0.5 bg-gray-200" />
|
||||
|
||||
<div className="space-y-4">
|
||||
{record.history.map((entry) => (
|
||||
<div key={entry.id} className="relative flex gap-4">
|
||||
{/* Icon */}
|
||||
<div className="relative z-10 bg-white p-1 rounded-full">
|
||||
{actionIcons[entry.action]}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 bg-white rounded-xl border border-gray-200 p-4 shadow-sm">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div>
|
||||
<div className="font-medium text-gray-900">{actionLabels[entry.action]}</div>
|
||||
{entry.documentTitle && (
|
||||
<div className="text-sm text-purple-600 font-medium">{entry.documentTitle}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-xs font-mono bg-gray-100 px-2 py-1 rounded">v{entry.version}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4 text-xs text-gray-500 mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="w-3 h-3" />
|
||||
{formatDateTime(entry.timestamp)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Globe className="w-3 h-3" />
|
||||
{entry.ipAddress}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
<span className="font-medium">Quelle:</span> {entry.source}
|
||||
</div>
|
||||
|
||||
{entry.notes && (
|
||||
<div className="mt-2 text-sm text-gray-600 bg-gray-50 rounded-lg px-3 py-2 border-l-2 border-purple-300">
|
||||
{entry.notes}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expandable User-Agent */}
|
||||
<details className="mt-2">
|
||||
<summary className="text-xs text-gray-400 cursor-pointer hover:text-gray-600">
|
||||
User-Agent anzeigen
|
||||
</summary>
|
||||
<div className="mt-1 font-mono text-xs text-gray-500 bg-gray-50 p-2 rounded break-all">
|
||||
{entry.userAgent}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="px-6 py-4 border-t border-gray-200 bg-gray-50 flex items-center justify-between">
|
||||
<div className="text-xs text-gray-500">
|
||||
Consent-ID: <span className="font-mono">{record.id}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{record.status === 'granted' && !showRevokeConfirm && (
|
||||
<button
|
||||
onClick={() => setShowRevokeConfirm(true)}
|
||||
className="flex items-center gap-2 px-4 py-2 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
>
|
||||
<AlertTriangle className="w-4 h-4" />
|
||||
Widerrufen
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showRevokeConfirm && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-red-600">Wirklich widerrufen?</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowRevokeConfirm(false)}
|
||||
className="px-3 py-1.5 bg-gray-200 text-gray-700 text-sm rounded-lg hover:bg-gray-300"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user