From cc3a9a37dc8785223d7e1a54858684fb6541fd70 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Wed, 15 Apr 2026 08:20:24 +0200 Subject: [PATCH] refactor(admin): split dsr/[requestId] page.tsx into colocated components Split the 854-line DSR detail page into colocated components under _components/ and a data-loading hook under _hooks/. No behavior changes. page.tsx is now 172 LOC, all extracted files under 300 LOC. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../[requestId]/_components/ActionButtons.tsx | 75 ++ .../dsr/[requestId]/_components/AuditLog.tsx | 44 + .../[requestId]/_components/DSRDetailsTab.tsx | 120 +++ .../dsr/[requestId]/_components/DSRHeader.tsx | 41 + .../[requestId]/_components/DSRSidebar.tsx | 85 ++ .../_components/DSRTypeSpecificTab.tsx | 183 +++++ .../_components/DeadlineDisplay.tsx | 45 + .../[requestId]/_components/StatusBadge.tsx | 15 + .../dsr/[requestId]/_hooks/useDSRDetail.ts | 204 +++++ .../app/sdk/dsr/[requestId]/page.tsx | 768 +----------------- 10 files changed, 855 insertions(+), 725 deletions(-) create mode 100644 admin-compliance/app/sdk/dsr/[requestId]/_components/ActionButtons.tsx create mode 100644 admin-compliance/app/sdk/dsr/[requestId]/_components/AuditLog.tsx create mode 100644 admin-compliance/app/sdk/dsr/[requestId]/_components/DSRDetailsTab.tsx create mode 100644 admin-compliance/app/sdk/dsr/[requestId]/_components/DSRHeader.tsx create mode 100644 admin-compliance/app/sdk/dsr/[requestId]/_components/DSRSidebar.tsx create mode 100644 admin-compliance/app/sdk/dsr/[requestId]/_components/DSRTypeSpecificTab.tsx create mode 100644 admin-compliance/app/sdk/dsr/[requestId]/_components/DeadlineDisplay.tsx create mode 100644 admin-compliance/app/sdk/dsr/[requestId]/_components/StatusBadge.tsx create mode 100644 admin-compliance/app/sdk/dsr/[requestId]/_hooks/useDSRDetail.ts diff --git a/admin-compliance/app/sdk/dsr/[requestId]/_components/ActionButtons.tsx b/admin-compliance/app/sdk/dsr/[requestId]/_components/ActionButtons.tsx new file mode 100644 index 0000000..9521b37 --- /dev/null +++ b/admin-compliance/app/sdk/dsr/[requestId]/_components/ActionButtons.tsx @@ -0,0 +1,75 @@ +'use client' + +import React from 'react' +import { DSRRequest } from '@/lib/sdk/dsr/types' + +export 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 && ( + + )} + + + + + +
+ + + +
+
+ ) +} diff --git a/admin-compliance/app/sdk/dsr/[requestId]/_components/AuditLog.tsx b/admin-compliance/app/sdk/dsr/[requestId]/_components/AuditLog.tsx new file mode 100644 index 0000000..f4b0266 --- /dev/null +++ b/admin-compliance/app/sdk/dsr/[requestId]/_components/AuditLog.tsx @@ -0,0 +1,44 @@ +'use client' + +import React from 'react' + +export 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'} +
+
+
+ ))} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRDetailsTab.tsx b/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRDetailsTab.tsx new file mode 100644 index 0000000..f05035a --- /dev/null +++ b/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRDetailsTab.tsx @@ -0,0 +1,120 @@ +'use client' + +import React from 'react' +import { DSRRequest } from '@/lib/sdk/dsr/types' + +export function DSRDetailsTab({ + request, + onShowIdentityModal, +}: { + request: DSRRequest + onShowIdentityModal: () => void +}) { + return ( +
+ {/* 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 && ( +
+

Notizen

+
+ {request.notes} +
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRHeader.tsx b/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRHeader.tsx new file mode 100644 index 0000000..988ae94 --- /dev/null +++ b/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRHeader.tsx @@ -0,0 +1,41 @@ +'use client' + +import React from 'react' +import Link from 'next/link' +import { DSRRequest, DSR_TYPE_INFO } from '@/lib/sdk/dsr/types' + +export function DSRHeader({ request }: { request: DSRRequest }) { + const typeInfo = DSR_TYPE_INFO[request.type] + + return ( +
+
+ + + + + +
+
+ {request.referenceNumber} + + {typeInfo.article} {typeInfo.label} + +
+

+ {request.requester.name} +

+
+
+ +
+ ) +} diff --git a/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRSidebar.tsx b/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRSidebar.tsx new file mode 100644 index 0000000..fa89d04 --- /dev/null +++ b/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRSidebar.tsx @@ -0,0 +1,85 @@ +'use client' + +import React from 'react' +import { DSRRequest } from '@/lib/sdk/dsr/types' +import { StatusBadge } from './StatusBadge' +import { DeadlineDisplay } from './DeadlineDisplay' +import { ActionButtons } from './ActionButtons' +import { AuditLog } from './AuditLog' + +export function DSRSidebar({ + request, + history, + onVerifyIdentity, + onExtendDeadline, + onComplete, + onReject, + onAssign, +}: { + request: DSRRequest + history: any[] + onVerifyIdentity: () => void + onExtendDeadline: () => void + onComplete: () => void + onReject: () => void + onAssign: () => void +}) { + return ( +
+ {/* 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

+ +
+ + {/* Audit Log Card */} +
+ +
+
+ ) +} diff --git a/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRTypeSpecificTab.tsx b/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRTypeSpecificTab.tsx new file mode 100644 index 0000000..699c4b4 --- /dev/null +++ b/admin-compliance/app/sdk/dsr/[requestId]/_components/DSRTypeSpecificTab.tsx @@ -0,0 +1,183 @@ +'use client' + +import React from 'react' +import { DSRRequest } from '@/lib/sdk/dsr/types' +import { + DSRErasureChecklistComponent, + DSRDataExportComponent +} from '@/components/sdk/dsr' + +export function DSRTypeSpecificTab({ + request, + setRequest, + exceptionChecks, + onExceptionCheckChange, +}: { + request: DSRRequest + setRequest: (r: DSRRequest) => void + exceptionChecks: any[] + onExceptionCheckChange: (checkId: string, applies: boolean, notes?: string) => void +}) { + return ( +
+ {/* 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) => ( +
+
+ onExceptionCheckChange(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. +

+
+
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/dsr/[requestId]/_components/DeadlineDisplay.tsx b/admin-compliance/app/sdk/dsr/[requestId]/_components/DeadlineDisplay.tsx new file mode 100644 index 0000000..495452d --- /dev/null +++ b/admin-compliance/app/sdk/dsr/[requestId]/_components/DeadlineDisplay.tsx @@ -0,0 +1,45 @@ +'use client' + +import React from 'react' +import { DSRRequest, getDaysRemaining, isOverdue, isUrgent } from '@/lib/sdk/dsr/types' + +export 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) +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/dsr/[requestId]/_components/StatusBadge.tsx b/admin-compliance/app/sdk/dsr/[requestId]/_components/StatusBadge.tsx new file mode 100644 index 0000000..a51bb9e --- /dev/null +++ b/admin-compliance/app/sdk/dsr/[requestId]/_components/StatusBadge.tsx @@ -0,0 +1,15 @@ +'use client' + +import React from 'react' +import { DSR_STATUS_INFO } from '@/lib/sdk/dsr/types' + +export function StatusBadge({ status }: { status: string }) { + const info = DSR_STATUS_INFO[status as keyof typeof DSR_STATUS_INFO] + if (!info) return null + + return ( + + {info.label} + + ) +} diff --git a/admin-compliance/app/sdk/dsr/[requestId]/_hooks/useDSRDetail.ts b/admin-compliance/app/sdk/dsr/[requestId]/_hooks/useDSRDetail.ts new file mode 100644 index 0000000..6ee3132 --- /dev/null +++ b/admin-compliance/app/sdk/dsr/[requestId]/_hooks/useDSRDetail.ts @@ -0,0 +1,204 @@ +'use client' + +import { useState, useEffect } from 'react' +import { + DSRRequest, + DSRCommunication, + DSRVerifyIdentityRequest, +} from '@/lib/sdk/dsr/types' +import { + fetchSDKDSR, + verifyDSRIdentity, + assignDSR, + extendDSRDeadline, + completeDSR, + rejectDSR, + fetchDSRCommunications, + sendDSRCommunication, + fetchDSRExceptionChecks, + initDSRExceptionChecks, + updateDSRExceptionCheck, + fetchDSRHistory, +} from '@/lib/sdk/dsr/api' + +export function useDSRDetail(requestId: string) { + const [request, setRequest] = useState(null) + const [communications, setCommunications] = useState([]) + const [history, setHistory] = useState([]) + const [exceptionChecks, setExceptionChecks] = useState([]) + const [isLoading, setIsLoading] = useState(true) + + // 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) + 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, + }) + 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) + } + } + + return { + request, + setRequest, + communications, + history, + exceptionChecks, + isLoading, + handleVerifyIdentity, + handleAssign, + handleExtendDeadline, + handleComplete, + handleReject, + handleSendCommunication, + handleExceptionCheckChange, + } +} diff --git a/admin-compliance/app/sdk/dsr/[requestId]/page.tsx b/admin-compliance/app/sdk/dsr/[requestId]/page.tsx index 9d6721d..dd461a4 100644 --- a/admin-compliance/app/sdk/dsr/[requestId]/page.tsx +++ b/admin-compliance/app/sdk/dsr/[requestId]/page.tsx @@ -1,390 +1,42 @@ 'use client' -import React, { useState, useEffect } from 'react' +import React, { useState } 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 { useParams } from 'next/navigation' +import { DSR_TYPE_INFO, isOverdue, isUrgent } from '@/lib/sdk/dsr/types' 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 -// ============================================================================= +import { DSRHeader } from './_components/DSRHeader' +import { DSRDetailsTab } from './_components/DSRDetailsTab' +import { DSRTypeSpecificTab } from './_components/DSRTypeSpecificTab' +import { DSRSidebar } from './_components/DSRSidebar' +import { useDSRDetail } from './_hooks/useDSRDetail' 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) - } - } + const { + request, + setRequest, + communications, + history, + exceptionChecks, + isLoading, + handleVerifyIdentity, + handleAssign, + handleExtendDeadline, + handleComplete, + handleReject, + handleSendCommunication, + handleExceptionCheckChange, + } = useDSRDetail(requestId) if (isLoading) { return ( @@ -428,36 +80,7 @@ export default function DSRDetailPage() { return (
- {/* Header */} -
-
- - - - - -
-
- {request.referenceNumber} - - {typeInfo.article} {typeInfo.label} - -
-

- {request.requester.name} -

-
-
- -
+ {/* Workflow Stepper */}
- {/* 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 && ( -
-

Notizen

-
- {request.notes} -
-
- )} -
+ setShowIdentityModal(true)} + /> )} - {/* 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 */} -
- -
-
+ setShowIdentityModal(true)} + onExtendDeadline={handleExtendDeadline} + onComplete={handleComplete} + onReject={handleReject} + onAssign={handleAssign} + />
{/* Identity Modal */}