Files
breakpilot-compliance/admin-compliance/app/sdk/dsr/page.tsx
Benjamin Admin 095eff26d9
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 34s
CI / test-python-backend-compliance (push) Successful in 30s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 17s
feat(dsr): Go DSR deprecated, Python Export-Endpoint, Frontend an Backend-APIs anbinden
- Go: DEPRECATED-Kommentare an allen DSR-Handlern und Routes
- Python: GET /dsr/export?format=csv|json (Semikolon-CSV, 12 Spalten)
- API-Client: 12 neue Funktionen (verify, assign, extend, complete, reject, communications, exception-checks, history)
- Detail-Seite: Alle Actions verdrahtet (keine Coming-soon-Alerts mehr), Communications + Art.17(3)-Checks + Audit-Log live
- Haupt-Seite: CSV-Export-Button im Header
- Tests: 54/54 bestanden (4 neue Export-Tests)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 18:21:43 +01:00

1020 lines
39 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
import React, { useState, useEffect, useMemo, useCallback } from 'react'
import { useSDK } from '@/lib/sdk'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import {
DSRRequest,
DSRType,
DSRStatus,
DSRStatistics,
DSR_TYPE_INFO,
DSR_STATUS_INFO,
getDaysRemaining,
isOverdue,
isUrgent
} from '@/lib/sdk/dsr/types'
import { fetchSDKDSRList, createSDKDSR, updateSDKDSRStatus } from '@/lib/sdk/dsr/api'
import { DSRWorkflowStepperCompact } from '@/components/sdk/dsr'
// =============================================================================
// TYPES
// =============================================================================
type TabId = 'overview' | 'intake' | 'processing' | 'completed' | 'settings'
interface Tab {
id: TabId
label: string
count?: number
countColor?: string
}
// =============================================================================
// COMPONENTS
// =============================================================================
function TabNavigation({
tabs,
activeTab,
onTabChange
}: {
tabs: Tab[]
activeTab: TabId
onTabChange: (tab: TabId) => void
}) {
return (
<div className="border-b border-gray-200">
<nav className="flex gap-1 -mb-px" aria-label="Tabs">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => onTabChange(tab.id)}
className={`
px-4 py-3 text-sm font-medium border-b-2 transition-colors
${activeTab === tab.id
? 'border-purple-600 text-purple-600'
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
}
`}
>
<span className="flex items-center gap-2">
{tab.label}
{tab.count !== undefined && tab.count > 0 && (
<span className={`
px-2 py-0.5 text-xs rounded-full
${tab.countColor || 'bg-gray-100 text-gray-600'}
`}>
{tab.count}
</span>
)}
</span>
</button>
))}
</nav>
</div>
)
}
function StatCard({
label,
value,
color = 'gray',
icon,
trend
}: {
label: string
value: number | string
color?: 'gray' | 'blue' | 'yellow' | 'red' | 'green' | 'purple'
icon?: React.ReactNode
trend?: { value: number; label: string }
}) {
const colorClasses = {
gray: 'border-gray-200 text-gray-900',
blue: 'border-blue-200 text-blue-600',
yellow: 'border-yellow-200 text-yellow-600',
red: 'border-red-200 text-red-600',
green: 'border-green-200 text-green-600',
purple: 'border-purple-200 text-purple-600'
}
return (
<div className={`bg-white rounded-xl border ${colorClasses[color]} p-6`}>
<div className="flex items-start justify-between">
<div>
<div className={`text-sm ${color === 'gray' ? 'text-gray-500' : `text-${color}-600`}`}>
{label}
</div>
<div className={`text-3xl font-bold mt-1 ${colorClasses[color].split(' ')[1]}`}>
{value}
</div>
{trend && (
<div className={`text-xs mt-1 ${trend.value >= 0 ? 'text-green-600' : 'text-red-600'}`}>
{trend.value >= 0 ? '+' : ''}{trend.value} {trend.label}
</div>
)}
</div>
{icon && (
<div className={`w-10 h-10 rounded-lg flex items-center justify-center bg-${color}-50`}>
{icon}
</div>
)}
</div>
</div>
)
}
function RequestCard({ request, onClick }: { request: DSRRequest; onClick?: () => void }) {
const typeInfo = DSR_TYPE_INFO[request.type]
const statusInfo = DSR_STATUS_INFO[request.status]
const daysRemaining = getDaysRemaining(request.deadline.currentDeadline)
const overdue = isOverdue(request)
const urgent = isUrgent(request)
return (
<div
onClick={onClick}
className={`
bg-white rounded-xl border-2 p-6 hover:shadow-md transition-all cursor-pointer
${overdue ? 'border-red-300 hover:border-red-400' :
urgent ? 'border-orange-300 hover:border-orange-400' :
request.status === 'completed' ? 'border-green-200 hover:border-green-300' :
'border-gray-200 hover:border-purple-300'
}
`}
>
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
{/* Header Badges */}
<div className="flex items-center gap-2 mb-2 flex-wrap">
<span className="text-xs text-gray-500 font-mono">
{request.referenceNumber}
</span>
<span className={`px-2 py-1 text-xs rounded-full ${typeInfo.bgColor} ${typeInfo.color}`}>
{typeInfo.article} {typeInfo.labelShort}
</span>
{!request.identityVerification.verified && request.status !== 'completed' && request.status !== 'rejected' && (
<span className="px-2 py-1 text-xs bg-yellow-100 text-yellow-700 rounded-full flex items-center gap-1">
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
ID fehlt
</span>
)}
</div>
{/* Requester Info */}
<h3 className="text-lg font-semibold text-gray-900 truncate">
{request.requester.name}
</h3>
<p className="text-sm text-gray-500 truncate">{request.requester.email}</p>
{/* Workflow Status */}
<div className="mt-3">
<DSRWorkflowStepperCompact currentStatus={request.status} />
</div>
</div>
{/* Right Side - Deadline */}
<div className={`text-right ml-4 ${
overdue ? 'text-red-600' :
urgent ? 'text-orange-600' :
'text-gray-500'
}`}>
<div className="text-sm font-medium">
{request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled'
? statusInfo.label
: overdue
? `${Math.abs(daysRemaining)} Tage ueberfaellig`
: `${daysRemaining} Tage`
}
</div>
<div className="text-xs mt-0.5">
{new Date(request.receivedAt).toLocaleDateString('de-DE')}
</div>
</div>
</div>
{/* Notes Preview */}
{request.notes && (
<div className="mt-3 p-3 bg-gray-50 rounded-lg text-sm text-gray-600 line-clamp-2">
{request.notes}
</div>
)}
{/* Footer */}
<div className="mt-4 pt-4 border-t border-gray-100 flex items-center justify-between">
<div className="text-sm text-gray-500">
{request.assignment.assignedTo
? `Zugewiesen: ${request.assignment.assignedTo}`
: 'Nicht zugewiesen'
}
</div>
<div className="flex items-center gap-2">
{request.status !== 'completed' && request.status !== 'rejected' && request.status !== 'cancelled' && (
<>
{!request.identityVerification.verified && (
<span className="px-3 py-1 text-sm bg-yellow-50 text-yellow-700 rounded-lg">
ID pruefen
</span>
)}
<span className="px-3 py-1 text-sm text-purple-600 hover:bg-purple-50 rounded-lg transition-colors">
Bearbeiten
</span>
</>
)}
{request.status === 'completed' && (
<span className="px-3 py-1 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors">
Details
</span>
)}
</div>
</div>
</div>
)
}
function FilterBar({
selectedType,
selectedStatus,
selectedPriority,
onTypeChange,
onStatusChange,
onPriorityChange,
onClear
}: {
selectedType: DSRType | 'all'
selectedStatus: DSRStatus | 'all'
selectedPriority: string
onTypeChange: (type: DSRType | 'all') => void
onStatusChange: (status: DSRStatus | 'all') => void
onPriorityChange: (priority: string) => void
onClear: () => void
}) {
const hasFilters = selectedType !== 'all' || selectedStatus !== 'all' || selectedPriority !== 'all'
return (
<div className="flex items-center gap-4 flex-wrap">
<span className="text-sm text-gray-500">Filter:</span>
{/* Type Filter */}
<select
value={selectedType}
onChange={(e) => onTypeChange(e.target.value as DSRType | 'all')}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="all">Alle Typen</option>
{Object.entries(DSR_TYPE_INFO).map(([type, info]) => (
<option key={type} value={type}>{info.article} - {info.labelShort}</option>
))}
</select>
{/* Status Filter */}
<select
value={selectedStatus}
onChange={(e) => onStatusChange(e.target.value as DSRStatus | 'all')}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="all">Alle Status</option>
{Object.entries(DSR_STATUS_INFO).map(([status, info]) => (
<option key={status} value={status}>{info.label}</option>
))}
</select>
{/* Priority Filter */}
<select
value={selectedPriority}
onChange={(e) => onPriorityChange(e.target.value)}
className="px-3 py-1.5 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
>
<option value="all">Alle Prioritaeten</option>
<option value="critical">Kritisch</option>
<option value="high">Hoch</option>
<option value="normal">Normal</option>
<option value="low">Niedrig</option>
</select>
{/* Clear Filters */}
{hasFilters && (
<button
onClick={onClear}
className="px-3 py-1.5 text-sm text-gray-500 hover:text-gray-700 hover:bg-gray-100 rounded-lg transition-colors"
>
Filter zuruecksetzen
</button>
)}
</div>
)
}
// =============================================================================
// DSR CREATE MODAL
// =============================================================================
function DSRCreateModal({
onClose,
onSuccess
}: {
onClose: () => void
onSuccess: () => void
}) {
const [type, setType] = useState<string>('access')
const [subjectName, setSubjectName] = useState('')
const [subjectEmail, setSubjectEmail] = useState('')
const [description, setDescription] = useState('')
const [source, setSource] = useState<string>('web_form')
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState<string | null>(null)
const deadline = new Date()
deadline.setDate(deadline.getDate() + 30)
const deadlineStr = deadline.toLocaleDateString('de-DE')
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!subjectName.trim() || !subjectEmail.trim()) return
setIsSaving(true)
setError(null)
try {
await createSDKDSR({
type,
requester: { name: subjectName.trim(), email: subjectEmail.trim() },
requestText: description.trim(),
source
})
onSuccess()
} catch (err: unknown) {
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
} finally {
setIsSaving(false)
}
}
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50"
onClick={onClose}
/>
{/* Modal */}
<div className="relative bg-white rounded-2xl shadow-xl w-full max-w-lg mx-4 max-h-[90vh] overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-semibold text-gray-900">Neue Anfrage anlegen</h2>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
<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>
{error && (
<div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
{/* Type */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Art der Anfrage
</label>
<select
value={type}
onChange={(e) => setType(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
>
<option value="access">Art. 15 - Auskunft</option>
<option value="rectification">Art. 16 - Berichtigung</option>
<option value="erasure">Art. 17 - Loeschung</option>
<option value="restriction">Art. 18 - Einschraenkung</option>
<option value="portability">Art. 20 - Datenpортabilitaet</option>
<option value="objection">Art. 21 - Widerspruch</option>
</select>
</div>
{/* Subject Name */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Name der betroffenen Person <span className="text-red-500">*</span>
</label>
<input
type="text"
value={subjectName}
onChange={(e) => setSubjectName(e.target.value)}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
placeholder="Vor- und Nachname"
/>
</div>
{/* Subject Email */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
E-Mail der betroffenen Person <span className="text-red-500">*</span>
</label>
<input
type="email"
value={subjectEmail}
onChange={(e) => setSubjectEmail(e.target.value)}
required
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
placeholder="email@beispiel.de"
/>
</div>
{/* Description */}
<div>
<label className="block text-sm font-medium text-gray-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-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm resize-none"
placeholder="Details zur Anfrage..."
/>
</div>
{/* Source */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Eingangskanal
</label>
<select
value={source}
onChange={(e) => setSource(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm"
>
<option value="web_form">Webformular</option>
<option value="email">E-Mail</option>
<option value="phone">Telefon</option>
<option value="letter">Brief</option>
<option value="other">Sonstiges</option>
</select>
</div>
{/* Deadline Info */}
<div className="p-3 bg-blue-50 border border-blue-200 rounded-lg">
<p className="text-sm text-blue-700">
Gesetzliche Frist (Art. 12 DSGVO): Die Anfrage muss bis zum{' '}
<strong>{deadlineStr}</strong> beantwortet werden (30 Tage ab heute).
</p>
</div>
{/* Buttons */}
<div className="flex items-center justify-end gap-3 pt-2">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
Abbrechen
</button>
<button
type="submit"
disabled={isSaving || !subjectName.trim() || !subjectEmail.trim()}
className="px-4 py-2 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{isSaving ? 'Wird angelegt...' : 'Anfrage anlegen'}
</button>
</div>
</form>
</div>
</div>
</div>
)
}
// =============================================================================
// DSR DETAIL PANEL
// =============================================================================
function DSRDetailPanel({
request,
onClose,
onUpdated
}: {
request: DSRRequest
onClose: () => void
onUpdated: () => void
}) {
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false)
const [actionError, setActionError] = useState<string | null>(null)
const typeInfo = DSR_TYPE_INFO[request.type]
const statusInfo = DSR_STATUS_INFO[request.status]
const daysRemaining = getDaysRemaining(request.deadline.currentDeadline)
const overdue = isOverdue(request)
type StatusTransition = { label: string; next: DSRStatus; variant?: 'danger' }
const statusTransitions: Partial<Record<DSRStatus, StatusTransition[]>> = {
intake: [{ label: 'Identitaet pruefen', next: 'identity_verification' }],
identity_verification: [{ label: 'Bearbeitung starten', next: 'processing' }],
processing: [
{ label: 'Anfrage abschliessen', next: 'completed' },
{ label: 'Ablehnen', next: 'rejected', variant: 'danger' }
]
}
const transitions = statusTransitions[request.status] || []
const handleStatusChange = async (newStatus: DSRStatus) => {
setIsUpdatingStatus(true)
setActionError(null)
try {
await updateSDKDSRStatus(request.id, newStatus)
onUpdated()
} catch (err: unknown) {
setActionError(err instanceof Error ? err.message : 'Unbekannter Fehler')
} finally {
setIsUpdatingStatus(false)
}
}
const handleExportPDF = () => {
window.open(`/api/sdk/v1/compliance/dsr/${request.id}/export`, '_blank')
}
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40 bg-black/30"
onClick={onClose}
/>
{/* Drawer */}
<div className="fixed right-0 top-0 bottom-0 z-50 w-[600px] bg-white shadow-2xl overflow-y-auto">
{/* Header */}
<div className="sticky top-0 bg-white border-b border-gray-200 px-6 py-4 flex items-center justify-between">
<div>
<p className="text-xs text-gray-500 font-mono">{request.referenceNumber}</p>
<h2 className="text-lg font-semibold text-gray-900 mt-0.5">
{typeInfo.article} {typeInfo.labelShort}
</h2>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleExportPDF}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
PDF
</button>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-lg transition-colors"
>
<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>
<div className="p-6 space-y-6">
{actionError && (
<div className="p-3 bg-red-50 border border-red-200 rounded-lg text-sm text-red-700">
{actionError}
</div>
)}
{/* Workflow Stepper */}
<div>
<h3 className="text-sm font-medium text-gray-700 mb-3">Bearbeitungsstand</h3>
<DSRWorkflowStepperCompact currentStatus={request.status} />
</div>
{/* Requester Info */}
<div>
<h3 className="text-sm font-medium text-gray-700 mb-3">Antragsteller</h3>
<div className="space-y-2">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-purple-100 rounded-full flex items-center justify-center text-purple-600 font-medium text-sm">
{request.requester.name.charAt(0).toUpperCase()}
</div>
<div>
<p className="text-sm font-medium text-gray-900">{request.requester.name}</p>
<p className="text-xs text-gray-500">{request.requester.email}</p>
</div>
</div>
</div>
</div>
{/* Deadline */}
<div className={`p-4 rounded-lg border ${
overdue ? 'bg-red-50 border-red-200' : 'bg-gray-50 border-gray-200'
}`}>
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-gray-500">Gesetzliche Frist</p>
<p className="text-sm font-medium text-gray-900">
{new Date(request.deadline.currentDeadline).toLocaleDateString('de-DE')}
</p>
</div>
<div className={`text-right ${overdue ? 'text-red-600' : daysRemaining <= 7 ? 'text-orange-600' : 'text-gray-600'}`}>
<p className="text-lg font-bold">
{overdue ? `${Math.abs(daysRemaining)}` : daysRemaining}
</p>
<p className="text-xs">
{overdue ? 'Tage ueberfaellig' : 'Tage verbleibend'}
</p>
</div>
</div>
</div>
{/* Details */}
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-gray-500">Status</p>
<p className="text-sm font-medium text-gray-900">{statusInfo.label}</p>
</div>
<div>
<p className="text-xs text-gray-500">Zugewiesen an</p>
<p className="text-sm font-medium text-gray-900">
{request.assignment?.assignedTo || '—'}
</p>
</div>
<div>
<p className="text-xs text-gray-500">Eingegangen am</p>
<p className="text-sm font-medium text-gray-900">
{new Date(request.receivedAt).toLocaleDateString('de-DE')}
</p>
</div>
<div>
<p className="text-xs text-gray-500">Identitaet geprueft</p>
<p className={`text-sm font-medium ${request.identityVerification.verified ? 'text-green-600' : 'text-yellow-600'}`}>
{request.identityVerification.verified ? 'Ja' : 'Ausstehend'}
</p>
</div>
</div>
{/* Notes */}
{request.notes && (
<div>
<h3 className="text-sm font-medium text-gray-700 mb-2">Notizen</h3>
<p className="text-sm text-gray-600 whitespace-pre-wrap">{request.notes}</p>
</div>
)}
{/* Status Transitions */}
{transitions.length > 0 && (
<div>
<h3 className="text-sm font-medium text-gray-700 mb-3">Naechste Schritte</h3>
<div className="flex flex-wrap gap-2">
{transitions.map((t) => (
<button
key={t.next}
onClick={() => handleStatusChange(t.next)}
disabled={isUpdatingStatus}
className={`px-4 py-2 text-sm rounded-lg disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
t.variant === 'danger'
? 'bg-red-600 text-white hover:bg-red-700'
: 'bg-purple-600 text-white hover:bg-purple-700'
}`}
>
{isUpdatingStatus ? 'Wird gespeichert...' : t.label}
</button>
))}
</div>
</div>
)}
</div>
</div>
</>
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function DSRPage() {
const { state } = useSDK()
const [activeTab, setActiveTab] = useState<TabId>('overview')
const [requests, setRequests] = useState<DSRRequest[]>([])
const [statistics, setStatistics] = useState<DSRStatistics | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [showCreateModal, setShowCreateModal] = useState(false)
const [selectedRequest, setSelectedRequest] = useState<DSRRequest | null>(null)
// Filters
const [selectedType, setSelectedType] = useState<DSRType | 'all'>('all')
const [selectedStatus, setSelectedStatus] = useState<DSRStatus | 'all'>('all')
const [selectedPriority, setSelectedPriority] = useState<string>('all')
// Load data from SDK backend
const loadData = useCallback(async () => {
setIsLoading(true)
try {
const { requests: dsrRequests, statistics: dsrStats } = await fetchSDKDSRList()
setRequests(dsrRequests)
setStatistics(dsrStats)
} catch (error) {
console.error('Failed to load DSR data:', error)
} finally {
setIsLoading(false)
}
}, [])
useEffect(() => {
loadData()
}, [loadData])
// Calculate tab counts
const tabCounts = useMemo(() => {
return {
intake: requests.filter(r => r.status === 'intake' || r.status === 'identity_verification').length,
processing: requests.filter(r => r.status === 'processing').length,
completed: requests.filter(r => r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled').length,
overdue: requests.filter(r => isOverdue(r)).length
}
}, [requests])
// Filter requests based on active tab and filters
const filteredRequests = useMemo(() => {
let filtered = [...requests]
// Tab-based filtering
if (activeTab === 'intake') {
filtered = filtered.filter(r => r.status === 'intake' || r.status === 'identity_verification')
} else if (activeTab === 'processing') {
filtered = filtered.filter(r => r.status === 'processing')
} else if (activeTab === 'completed') {
filtered = filtered.filter(r => r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled')
}
// Type filter
if (selectedType !== 'all') {
filtered = filtered.filter(r => r.type === selectedType)
}
// Status filter
if (selectedStatus !== 'all') {
filtered = filtered.filter(r => r.status === selectedStatus)
}
// Priority filter
if (selectedPriority !== 'all') {
filtered = filtered.filter(r => r.priority === selectedPriority)
}
// Sort by urgency
return filtered.sort((a, b) => {
const getUrgency = (r: DSRRequest) => {
if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return 100
const days = getDaysRemaining(r.deadline.currentDeadline)
if (days < 0) return -100 + days // Overdue items first
return days
}
return getUrgency(a) - getUrgency(b)
})
}, [requests, activeTab, selectedType, selectedStatus, selectedPriority])
const tabs: Tab[] = [
{ id: 'overview', label: 'Uebersicht' },
{ id: 'intake', label: 'Eingang', count: tabCounts.intake, countColor: 'bg-blue-100 text-blue-600' },
{ id: 'processing', label: 'In Bearbeitung', count: tabCounts.processing, countColor: 'bg-yellow-100 text-yellow-600' },
{ id: 'completed', label: 'Abgeschlossen', count: tabCounts.completed, countColor: 'bg-green-100 text-green-600' },
{ id: 'settings', label: 'Einstellungen' }
]
const stepInfo = STEP_EXPLANATIONS['dsr']
const clearFilters = () => {
setSelectedType('all')
setSelectedStatus('all')
setSelectedPriority('all')
}
return (
<div className="space-y-6">
{/* Step Header */}
<StepHeader
stepId="dsr"
title={stepInfo.title}
description={stepInfo.description}
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
<div className="flex items-center gap-2">
<button
onClick={() => {
const link = document.createElement('a')
link.href = '/api/sdk/v1/compliance/dsr/export?format=csv'
link.download = 'dsr_export.csv'
link.click()
}}
className="flex items-center gap-2 px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
CSV Export
</button>
<button
onClick={() => setShowCreateModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Anfrage erfassen
</button>
</div>
</StepHeader>
{/* Tab Navigation */}
<TabNavigation
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
/>
{/* Loading State */}
{isLoading ? (
<div className="flex items-center justify-center py-12">
<svg className="animate-spin w-8 h-8 text-purple-600" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
</div>
) : activeTab === 'settings' ? (
/* Settings Tab */
<div className="bg-white rounded-xl border border-gray-200 p-8 text-center">
<div className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Einstellungen</h3>
<p className="mt-2 text-gray-500">
DSR-Portal-Einstellungen, E-Mail-Vorlagen und Workflow-Konfiguration
werden in einer spaeteren Version verfuegbar sein.
</p>
</div>
) : (
<>
{/* Statistics (Overview Tab) */}
{activeTab === 'overview' && statistics && (
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<StatCard
label="Gesamt"
value={statistics.total}
color="gray"
/>
<StatCard
label="Neue Anfragen"
value={statistics.byStatus.intake + statistics.byStatus.identity_verification}
color="blue"
/>
<StatCard
label="In Bearbeitung"
value={statistics.byStatus.processing}
color="yellow"
/>
<StatCard
label="Ueberfaellig"
value={tabCounts.overdue}
color={tabCounts.overdue > 0 ? 'red' : 'green'}
/>
</div>
)}
{/* Overdue Alert */}
{tabCounts.overdue > 0 && (
<div className="bg-red-50 border border-red-200 rounded-xl p-4 flex items-center gap-4">
<div className="w-10 h-10 bg-red-100 rounded-full flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div className="flex-1">
<h4 className="font-medium text-red-800">
Achtung: {tabCounts.overdue} ueberfaellige Anfrage(n)
</h4>
<p className="text-sm text-red-600">
Die gesetzliche Frist ist abgelaufen. Handeln Sie umgehend, um Bussgelder zu vermeiden.
</p>
</div>
<button
onClick={() => {
setActiveTab('overview')
setSelectedStatus('all')
}}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors text-sm font-medium"
>
Anzeigen
</button>
</div>
)}
{/* Info Box (Overview Tab) */}
{activeTab === 'overview' && (
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="font-medium text-blue-800">Fristen beachten</h4>
<p className="text-sm text-blue-600 mt-1">
Nach Art. 12 DSGVO muessen Anfragen innerhalb von einem Monat beantwortet werden.
Eine Verlaengerung um zwei weitere Monate ist bei komplexen Anfragen moeglich,
sofern der Betroffene innerhalb eines Monats darueber informiert wird.
</p>
</div>
</div>
</div>
)}
{/* Filters */}
<FilterBar
selectedType={selectedType}
selectedStatus={selectedStatus}
selectedPriority={selectedPriority}
onTypeChange={setSelectedType}
onStatusChange={setSelectedStatus}
onPriorityChange={setSelectedPriority}
onClear={clearFilters}
/>
{/* Requests List */}
<div className="space-y-4">
{filteredRequests.map(request => (
<RequestCard
key={request.id}
request={request}
onClick={() => setSelectedRequest(request)}
/>
))}
</div>
{/* Empty State */}
{filteredRequests.length === 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-12 text-center">
<div className="w-16 h-16 mx-auto bg-gray-100 rounded-full flex items-center justify-center mb-4">
<svg className="w-8 h-8 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900">Keine Anfragen gefunden</h3>
<p className="mt-2 text-gray-500">
{selectedType !== 'all' || selectedStatus !== 'all' || selectedPriority !== 'all'
? 'Passen Sie die Filter an oder'
: 'Es sind noch keine Anfragen vorhanden.'
}
</p>
{(selectedType !== 'all' || selectedStatus !== 'all' || selectedPriority !== 'all') ? (
<button
onClick={clearFilters}
className="mt-4 px-4 py-2 text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
>
Filter zuruecksetzen
</button>
) : (
<button
onClick={() => setShowCreateModal(true)}
className="mt-4 inline-flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Erste Anfrage erfassen
</button>
)}
</div>
)}
</>
)}
{/* Modals */}
{showCreateModal && (
<DSRCreateModal
onClose={() => setShowCreateModal(false)}
onSuccess={() => { setShowCreateModal(false); loadData() }}
/>
)}
{selectedRequest && (
<DSRDetailPanel
request={selectedRequest}
onClose={() => setSelectedRequest(null)}
onUpdated={() => { setSelectedRequest(null); loadData() }}
/>
)}
</div>
)
}