'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 (
{tabs.map(tab => (
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'
}
`}
>
{tab.label}
{tab.count !== undefined && tab.count > 0 && (
{tab.count}
)}
))}
)
}
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 (
{label}
{value}
{trend && (
= 0 ? 'text-green-600' : 'text-red-600'}`}>
{trend.value >= 0 ? '+' : ''}{trend.value} {trend.label}
)}
{icon && (
{icon}
)}
)
}
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 (
{/* Header Badges */}
{request.referenceNumber}
{typeInfo.article} {typeInfo.labelShort}
{!request.identityVerification.verified && request.status !== 'completed' && request.status !== 'rejected' && (
ID fehlt
)}
{/* Requester Info */}
{request.requester.name}
{request.requester.email}
{/* Workflow Status */}
{/* Right Side - Deadline */}
{request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled'
? statusInfo.label
: overdue
? `${Math.abs(daysRemaining)} Tage ueberfaellig`
: `${daysRemaining} Tage`
}
{new Date(request.receivedAt).toLocaleDateString('de-DE')}
{/* Notes Preview */}
{request.notes && (
{request.notes}
)}
{/* Footer */}
{request.assignment.assignedTo
? `Zugewiesen: ${request.assignment.assignedTo}`
: 'Nicht zugewiesen'
}
{request.status !== 'completed' && request.status !== 'rejected' && request.status !== 'cancelled' && (
<>
{!request.identityVerification.verified && (
ID pruefen
)}
Bearbeiten
>
)}
{request.status === 'completed' && (
Details
)}
)
}
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 (
Filter:
{/* Type Filter */}
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"
>
Alle Typen
{Object.entries(DSR_TYPE_INFO).map(([type, info]) => (
{info.article} - {info.labelShort}
))}
{/* Status Filter */}
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"
>
Alle Status
{Object.entries(DSR_STATUS_INFO).map(([status, info]) => (
{info.label}
))}
{/* Priority Filter */}
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"
>
Alle Prioritaeten
Kritisch
Hoch
Normal
Niedrig
{/* Clear Filters */}
{hasFilters && (
Filter zuruecksetzen
)}
)
}
// =============================================================================
// DSR CREATE MODAL
// =============================================================================
function DSRCreateModal({
onClose,
onSuccess
}: {
onClose: () => void
onSuccess: () => void
}) {
const [type, setType] = useState('access')
const [subjectName, setSubjectName] = useState('')
const [subjectEmail, setSubjectEmail] = useState('')
const [description, setDescription] = useState('')
const [source, setSource] = useState('web_form')
const [isSaving, setIsSaving] = useState(false)
const [error, setError] = useState(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 (
{/* Backdrop */}
{/* Modal */}
)
}
// =============================================================================
// DSR DETAIL PANEL
// =============================================================================
function DSRDetailPanel({
request,
onClose,
onUpdated
}: {
request: DSRRequest
onClose: () => void
onUpdated: () => void
}) {
const [isUpdatingStatus, setIsUpdatingStatus] = useState(false)
const [actionError, setActionError] = useState(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> = {
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 */}
{/* Drawer */}
{/* Header */}
{request.referenceNumber}
{typeInfo.article} {typeInfo.labelShort}
{actionError && (
{actionError}
)}
{/* Workflow Stepper */}
Bearbeitungsstand
{/* Requester Info */}
Antragsteller
{request.requester.name.charAt(0).toUpperCase()}
{request.requester.name}
{request.requester.email}
{/* Deadline */}
Gesetzliche Frist
{new Date(request.deadline.currentDeadline).toLocaleDateString('de-DE')}
{overdue ? `${Math.abs(daysRemaining)}` : daysRemaining}
{overdue ? 'Tage ueberfaellig' : 'Tage verbleibend'}
{/* Details */}
Status
{statusInfo.label}
Zugewiesen an
{request.assignment?.assignedTo || '—'}
Eingegangen am
{new Date(request.receivedAt).toLocaleDateString('de-DE')}
Identitaet geprueft
{request.identityVerification.verified ? 'Ja' : 'Ausstehend'}
{/* Notes */}
{request.notes && (
)}
{/* Status Transitions */}
{transitions.length > 0 && (
Naechste Schritte
{transitions.map((t) => (
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}
))}
)}
>
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function DSRPage() {
const { state } = useSDK()
const [activeTab, setActiveTab] = useState('overview')
const [requests, setRequests] = useState([])
const [statistics, setStatistics] = useState(null)
const [isLoading, setIsLoading] = useState(true)
const [showCreateModal, setShowCreateModal] = useState(false)
const [selectedRequest, setSelectedRequest] = useState(null)
// Filters
const [selectedType, setSelectedType] = useState('all')
const [selectedStatus, setSelectedStatus] = useState('all')
const [selectedPriority, setSelectedPriority] = useState('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 (
{/* Step Header */}
{
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"
>
CSV Export
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"
>
Anfrage erfassen
{/* Tab Navigation */}
{/* Loading State */}
{isLoading ? (
) : activeTab === 'settings' ? (
/* Settings Tab */
Einstellungen
DSR-Portal-Einstellungen, E-Mail-Vorlagen und Workflow-Konfiguration
werden in einer spaeteren Version verfuegbar sein.
) : (
<>
{/* Statistics (Overview Tab) */}
{activeTab === 'overview' && statistics && (
0 ? 'red' : 'green'}
/>
)}
{/* Overdue Alert */}
{tabCounts.overdue > 0 && (
Achtung: {tabCounts.overdue} ueberfaellige Anfrage(n)
Die gesetzliche Frist ist abgelaufen. Handeln Sie umgehend, um Bussgelder zu vermeiden.
{
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
)}
{/* Info Box (Overview Tab) */}
{activeTab === 'overview' && (
Fristen beachten
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.
)}
{/* Filters */}
{/* Requests List */}
{filteredRequests.map(request => (
setSelectedRequest(request)}
/>
))}
{/* Empty State */}
{filteredRequests.length === 0 && (
Keine Anfragen gefunden
{selectedType !== 'all' || selectedStatus !== 'all' || selectedPriority !== 'all'
? 'Passen Sie die Filter an oder'
: 'Es sind noch keine Anfragen vorhanden.'
}
{(selectedType !== 'all' || selectedStatus !== 'all' || selectedPriority !== 'all') ? (
Filter zuruecksetzen
) : (
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"
>
Erste Anfrage erfassen
)}
)}
>
)}
{/* Modals */}
{showCreateModal && (
setShowCreateModal(false)}
onSuccess={() => { setShowCreateModal(false); loadData() }}
/>
)}
{selectedRequest && (
setSelectedRequest(null)}
onUpdated={() => { setSelectedRequest(null); loadData() }}
/>
)}
)
}