'use client'
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { useParams, useRouter } from 'next/navigation'
import {
DSRRequest,
DSR_TYPE_INFO,
DSR_STATUS_INFO,
getDaysRemaining,
isOverdue,
isUrgent,
DSRCommunication,
DSRVerifyIdentityRequest
} from '@/lib/sdk/dsr/types'
import {
fetchSDKDSR,
updateSDKDSRStatus,
verifyDSRIdentity,
assignDSR,
extendDSRDeadline,
completeDSR,
rejectDSR,
fetchDSRCommunications,
sendDSRCommunication,
fetchDSRExceptionChecks,
initDSRExceptionChecks,
updateDSRExceptionCheck,
fetchDSRHistory,
} from '@/lib/sdk/dsr/api'
import {
DSRWorkflowStepper,
DSRIdentityModal,
DSRCommunicationLog,
DSRErasureChecklistComponent,
DSRDataExportComponent
} from '@/components/sdk/dsr'
// =============================================================================
// COMPONENTS
// =============================================================================
function StatusBadge({ status }: { status: string }) {
const info = DSR_STATUS_INFO[status as keyof typeof DSR_STATUS_INFO]
if (!info) return null
return (
{info.label}
)
}
function DeadlineDisplay({ request }: { request: DSRRequest }) {
const daysRemaining = getDaysRemaining(request.deadline.currentDeadline)
const overdue = isOverdue(request)
const urgent = isUrgent(request)
const isTerminal = request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled'
if (isTerminal) {
return (
Abgeschlossen am
{request.completedAt
? new Date(request.completedAt).toLocaleDateString('de-DE')
: '-'
}
)
}
return (
Frist
{overdue
? `${Math.abs(daysRemaining)} Tage ueberfaellig`
: `${daysRemaining} Tage`
}
bis {new Date(request.deadline.currentDeadline).toLocaleDateString('de-DE')}
{request.deadline.extended && (
(Verlaengert)
)}
)
}
function ActionButtons({
request,
onVerifyIdentity,
onExtendDeadline,
onComplete,
onReject,
onAssign
}: {
request: DSRRequest
onVerifyIdentity: () => void
onExtendDeadline: () => void
onComplete: () => void
onReject: () => void
onAssign: () => void
}) {
const isTerminal = request.status === 'completed' || request.status === 'rejected' || request.status === 'cancelled'
if (isTerminal) {
return (
)
}
return (
{!request.identityVerification.verified && (
)}
)
}
function AuditLog({ history }: { history: any[] }) {
return (
Aktivitaeten
{history.length === 0 && (
Keine Eintraege
)}
{history.map((entry, idx) => (
{entry.previous_status
? `${entry.previous_status} → ${entry.new_status}`
: entry.new_status
}
{entry.comment && `: ${entry.comment}`}
{entry.created_at
? new Date(entry.created_at).toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
})
: ''
}
{' - '}
{entry.changed_by || 'System'}
))}
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
export default function DSRDetailPage() {
const params = useParams()
const router = useRouter()
const requestId = params.requestId as string
const [request, setRequest] = useState(null)
const [communications, setCommunications] = useState([])
const [history, setHistory] = useState([])
const [exceptionChecks, setExceptionChecks] = useState([])
const [isLoading, setIsLoading] = useState(true)
const [showIdentityModal, setShowIdentityModal] = useState(false)
const [activeContentTab, setActiveContentTab] = useState<'details' | 'communication' | 'type-specific'>('details')
const reloadRequest = async () => {
const found = await fetchSDKDSR(requestId)
if (found) setRequest(found)
}
// Load data from SDK backend
useEffect(() => {
const loadData = async () => {
setIsLoading(true)
try {
const found = await fetchSDKDSR(requestId)
if (found) {
setRequest(found)
// Load communications, history, and exception checks in parallel
const [comms, hist] = await Promise.all([
fetchDSRCommunications(requestId).catch(() => []),
fetchDSRHistory(requestId).catch(() => []),
])
setCommunications(comms.map((c: any) => ({
id: c.id,
dsrId: c.dsr_id,
type: c.communication_type,
channel: c.channel,
subject: c.subject,
content: c.content,
createdAt: c.created_at,
createdBy: c.created_by,
sentAt: c.sent_at,
sentBy: c.sent_by,
})))
setHistory(hist)
// Load exception checks for erasure requests
if (found.type === 'erasure') {
try {
const checks = await fetchDSRExceptionChecks(requestId)
if (checks.length === 0) {
// Auto-initialize if none exist
const initialized = await initDSRExceptionChecks(requestId)
setExceptionChecks(initialized)
} else {
setExceptionChecks(checks)
}
} catch {
setExceptionChecks([])
}
}
}
} catch (error) {
console.error('Failed to load DSR:', error)
} finally {
setIsLoading(false)
}
}
loadData()
}, [requestId])
const handleVerifyIdentity = async (verification: DSRVerifyIdentityRequest) => {
if (!request) return
try {
const updated = await verifyDSRIdentity(request.id, {
method: verification.method,
notes: verification.notes,
document_ref: verification.documentRef,
})
setRequest(updated)
// Reload history
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
} catch (err) {
console.error('Failed to verify identity:', err)
}
}
const handleAssign = async () => {
if (!request) return
const assignee = prompt('Zuweisen an (Name/ID):')
if (!assignee) return
try {
const updated = await assignDSR(request.id, assignee)
setRequest(updated)
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
} catch (err) {
console.error('Failed to assign:', err)
}
}
const handleExtendDeadline = async () => {
if (!request) return
const reason = prompt('Grund fuer die Fristverlaengerung:')
if (!reason) return
const daysStr = prompt('Um wie viele Tage verlaengern? (Standard: 60)', '60')
const days = parseInt(daysStr || '60', 10) || 60
try {
const updated = await extendDSRDeadline(request.id, reason, days)
setRequest(updated)
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
} catch (err) {
console.error('Failed to extend deadline:', err)
}
}
const handleComplete = async () => {
if (!request) return
const summary = prompt('Zusammenfassung der Bearbeitung:')
if (summary === null) return
try {
const updated = await completeDSR(request.id, summary || undefined)
setRequest(updated)
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
} catch (err) {
console.error('Failed to complete:', err)
}
}
const handleReject = async () => {
if (!request) return
const reason = prompt('Ablehnungsgrund:')
if (!reason) return
const legalBasis = prompt('Rechtsgrundlage (optional):')
try {
const updated = await rejectDSR(request.id, reason, legalBasis || undefined)
setRequest(updated)
fetchDSRHistory(requestId).then(setHistory).catch(() => {})
} catch (err) {
console.error('Failed to reject:', err)
}
}
const handleSendCommunication = async (message: any) => {
if (!request) return
try {
const result = await sendDSRCommunication(requestId, {
communication_type: message.type || 'outgoing',
channel: message.channel || 'email',
subject: message.subject,
content: message.content,
})
// Map backend response to frontend format
const newComm: DSRCommunication = {
id: result.id,
dsrId: result.dsr_id,
type: result.communication_type,
channel: result.channel,
subject: result.subject,
content: result.content,
createdAt: result.created_at,
createdBy: result.created_by || 'Current User',
sentAt: result.sent_at,
sentBy: result.sent_by,
}
setCommunications(prev => [newComm, ...prev])
} catch (err) {
console.error('Failed to send communication:', err)
}
}
const handleExceptionCheckChange = async (checkId: string, applies: boolean, notes?: string) => {
try {
const updated = await updateDSRExceptionCheck(requestId, checkId, { applies, notes })
setExceptionChecks(prev => prev.map(c => c.id === checkId ? updated : c))
} catch (err) {
console.error('Failed to update exception check:', err)
}
}
if (isLoading) {
return (
)
}
if (!request) {
return (
Anfrage nicht gefunden
Die angeforderte DSR-Anfrage existiert nicht oder wurde geloescht.
Zurueck zur Uebersicht
)
}
const typeInfo = DSR_TYPE_INFO[request.type]
const overdue = isOverdue(request)
const urgent = isUrgent(request)
return (
{/* Header */}
{request.referenceNumber}
{typeInfo.article} {typeInfo.label}
{request.requester.name}
{/* Workflow Stepper */}
{/* Main Content: 2/3 + 1/3 Layout */}
{/* Left Column - 2/3 */}
{/* Content Tabs */}
{/* Details Tab */}
{activeContentTab === 'details' && (
{/* Request Info */}
Antragsteller
{request.requester.name}
{request.requester.email}
{request.requester.phone && (
{request.requester.phone}
)}
Eingereicht
{new Date(request.receivedAt).toLocaleDateString('de-DE', {
day: '2-digit',
month: 'long',
year: 'numeric'
})}
Quelle: {request.source === 'web_form' ? 'Kontaktformular' :
request.source === 'email' ? 'E-Mail' :
request.source === 'letter' ? 'Brief' :
request.source === 'phone' ? 'Telefon' : request.source}
{/* Identity Verification */}
{request.identityVerification.verified ? (
) : (
)}
{request.identityVerification.verified
? 'Identitaet verifiziert'
: 'Identitaetspruefung ausstehend'
}
{request.identityVerification.verified && (
Methode: {request.identityVerification.method === 'id_document' ? 'Ausweisdokument' :
request.identityVerification.method === 'email' ? 'E-Mail' :
request.identityVerification.method === 'existing_account' ? 'Bestehendes Konto' :
request.identityVerification.method}
{' | '}
{new Date(request.identityVerification.verifiedAt!).toLocaleDateString('de-DE')}
)}
{!request.identityVerification.verified && (
)}
{/* Request Text */}
{request.requestText && (
Anfragetext
{request.requestText}
)}
{/* Notes */}
{request.notes && (
)}
)}
{/* Communication Tab */}
{activeContentTab === 'communication' && (
)}
{/* Type-Specific Tab */}
{activeContentTab === 'type-specific' && (
{/* Art. 17 - Erasure */}
{request.type === 'erasure' && (
setRequest({ ...request, erasureChecklist: checklist })}
/>
{/* Art. 17(3) Exception Checks from Backend */}
{exceptionChecks.length > 0 && (
Art. 17(3) Ausnahmepruefung
Pruefen Sie, ob eine der gesetzlichen Ausnahmen zur Loeschpflicht greift.
{exceptionChecks.map((check) => (
handleExceptionCheckChange(check.id, e.target.checked, check.notes)}
className="mt-1 h-4 w-4 rounded border-gray-300 text-purple-600 focus:ring-purple-500"
/>
{check.article}: {check.label}
{check.description}
{check.checked_by && (
Geprueft von {check.checked_by} am{' '}
{check.checked_at ? new Date(check.checked_at).toLocaleDateString('de-DE') : '-'}
)}
))}
)}
)}
{/* Art. 15/20 - Data Export */}
{(request.type === 'access' || request.type === 'portability') && (
{
// Mock generation
setRequest({
...request,
dataExport: {
format,
generatedAt: new Date().toISOString(),
generatedBy: 'Current User',
fileName: `datenexport_${request.referenceNumber}.${format}`,
fileSize: 125000,
includesThirdPartyData: true
}
})
}}
/>
)}
{/* Art. 16 - Rectification */}
{request.type === 'rectification' && request.rectificationDetails && (
Zu korrigierende Daten
{request.rectificationDetails.fieldsToCorrect.map((field, idx) => (
{field.field}
Aktueller Wert
{field.currentValue}
Angeforderter Wert
{field.requestedValue}
{field.corrected && (
Korrigiert am {new Date(field.correctedAt!).toLocaleDateString('de-DE')}
)}
))}
)}
{/* Art. 21 - Objection */}
{request.type === 'objection' && request.objectionDetails && (
Widerspruchsdetails
Verarbeitungszweck
{request.objectionDetails.processingPurpose}
Rechtsgrundlage
{request.objectionDetails.legalBasis}
Widerspruchsgruende
{request.objectionDetails.objectionGrounds}
{request.objectionDetails.decision !== 'pending' && (
Widerspruch {request.objectionDetails.decision === 'accepted' ? 'angenommen' : 'abgelehnt'}
{request.objectionDetails.decisionReason && (
{request.objectionDetails.decisionReason}
)}
)}
)}
{/* Default for restriction */}
{request.type === 'restriction' && (
Einschraenkung der Verarbeitung
Markieren Sie die betroffenen Daten im System als eingeschraenkt.
Die Daten duerfen nur noch gespeichert, aber nicht mehr verarbeitet werden.
)}
)}
{/* Right Column - 1/3 Sidebar */}
{/* Status Card */}
Status
{/* Priority */}
Prioritaet
{request.priority === 'critical' ? 'Kritisch' :
request.priority === 'high' ? 'Hoch' :
request.priority === 'normal' ? 'Normal' : 'Niedrig'}
{/* Assignment */}
Zugewiesen an
{request.assignment.assignedTo || 'Nicht zugewiesen'}
{/* Actions Card */}
Aktionen
setShowIdentityModal(true)}
onExtendDeadline={handleExtendDeadline}
onComplete={handleComplete}
onReject={handleReject}
onAssign={handleAssign}
/>
{/* Audit Log Card */}
{/* Identity Modal */}
setShowIdentityModal(false)}
onVerify={handleVerifyIdentity}
requesterName={request.requester.name}
requesterEmail={request.requester.email}
/>
)
}