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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<button className="w-full px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-sm">
|
||||||
|
PDF exportieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{!request.identityVerification.verified && (
|
||||||
|
<button
|
||||||
|
onClick={onVerifyIdentity}
|
||||||
|
className="w-full px-4 py-2 bg-yellow-500 text-white hover:bg-yellow-600 rounded-lg transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
Identitaet verifizieren
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onAssign}
|
||||||
|
className="w-full px-4 py-2 text-purple-600 bg-purple-50 hover:bg-purple-100 rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
{request.assignment.assignedTo ? 'Neu zuweisen' : 'Zuweisen'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onExtendDeadline}
|
||||||
|
className="w-full px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Frist verlaengern
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-200 pt-2 mt-2">
|
||||||
|
<button
|
||||||
|
onClick={onComplete}
|
||||||
|
className="w-full px-4 py-2 bg-green-600 text-white hover:bg-green-700 rounded-lg transition-colors text-sm font-medium"
|
||||||
|
>
|
||||||
|
Abschliessen
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={onReject}
|
||||||
|
className="w-full mt-2 px-4 py-2 text-red-600 bg-red-50 hover:bg-red-100 rounded-lg transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Ablehnen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
export function AuditLog({ history }: { history: any[] }) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<h4 className="text-sm font-medium text-gray-700">Aktivitaeten</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{history.length === 0 && (
|
||||||
|
<div className="text-xs text-gray-400">Keine Eintraege</div>
|
||||||
|
)}
|
||||||
|
{history.map((entry, idx) => (
|
||||||
|
<div key={entry.id || idx} className="flex items-start gap-2 text-xs">
|
||||||
|
<div className="w-1.5 h-1.5 rounded-full bg-gray-300 mt-1.5 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-900">
|
||||||
|
{entry.previous_status
|
||||||
|
? `${entry.previous_status} → ${entry.new_status}`
|
||||||
|
: entry.new_status
|
||||||
|
}
|
||||||
|
{entry.comment && `: ${entry.comment}`}
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-500">
|
||||||
|
{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'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Request Info */}
|
||||||
|
<div className="grid grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-gray-500 mb-2">Antragsteller</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="font-medium text-gray-900">{request.requester.name}</div>
|
||||||
|
<div className="text-sm text-gray-600">{request.requester.email}</div>
|
||||||
|
{request.requester.phone && (
|
||||||
|
<div className="text-sm text-gray-600">{request.requester.phone}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-gray-500 mb-2">Eingereicht</h4>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="font-medium text-gray-900">
|
||||||
|
{new Date(request.receivedAt).toLocaleDateString('de-DE', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric'
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
Quelle: {request.source === 'web_form' ? 'Kontaktformular' :
|
||||||
|
request.source === 'email' ? 'E-Mail' :
|
||||||
|
request.source === 'letter' ? 'Brief' :
|
||||||
|
request.source === 'phone' ? 'Telefon' : request.source}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Identity Verification */}
|
||||||
|
<div className={`
|
||||||
|
p-4 rounded-xl border
|
||||||
|
${request.identityVerification.verified
|
||||||
|
? 'bg-green-50 border-green-200'
|
||||||
|
: 'bg-yellow-50 border-yellow-200'
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className={`
|
||||||
|
w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0
|
||||||
|
${request.identityVerification.verified ? 'bg-green-100' : 'bg-yellow-100'}
|
||||||
|
`}>
|
||||||
|
{request.identityVerification.verified ? (
|
||||||
|
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg className="w-4 h-4 text-yellow-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">
|
||||||
|
<div className={`font-medium ${request.identityVerification.verified ? 'text-green-800' : 'text-yellow-800'}`}>
|
||||||
|
{request.identityVerification.verified
|
||||||
|
? 'Identitaet verifiziert'
|
||||||
|
: 'Identitaetspruefung ausstehend'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{request.identityVerification.verified && (
|
||||||
|
<div className="text-sm text-green-700 mt-1">
|
||||||
|
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')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!request.identityVerification.verified && (
|
||||||
|
<button
|
||||||
|
onClick={onShowIdentityModal}
|
||||||
|
className="px-3 py-1.5 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors text-sm"
|
||||||
|
>
|
||||||
|
Jetzt pruefen
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Request Text */}
|
||||||
|
{request.requestText && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-gray-500 mb-2">Anfragetext</h4>
|
||||||
|
<div className="bg-gray-50 rounded-xl p-4 text-gray-700 whitespace-pre-wrap">
|
||||||
|
{request.requestText}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Notes */}
|
||||||
|
{request.notes && (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-gray-500 mb-2">Notizen</h4>
|
||||||
|
<div className="bg-gray-50 rounded-xl p-4 text-gray-700">
|
||||||
|
{request.notes}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Link
|
||||||
|
href="/sdk/dsr"
|
||||||
|
className="p-2 text-gray-500 hover:text-gray-700 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="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm 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.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900 mt-1">
|
||||||
|
{request.requester.name}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button 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-4 h-4" 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>
|
||||||
|
Exportieren
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Status Card */}
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium text-gray-900">Status</h3>
|
||||||
|
<StatusBadge status={request.status} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-gray-100 pt-4">
|
||||||
|
<DeadlineDisplay request={request} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Priority */}
|
||||||
|
<div className="border-t border-gray-100 pt-4">
|
||||||
|
<div className="text-sm text-gray-500 mb-1">Prioritaet</div>
|
||||||
|
<div className={`
|
||||||
|
inline-flex px-2 py-1 text-sm font-medium rounded-lg
|
||||||
|
${request.priority === 'critical' ? 'bg-red-100 text-red-700' :
|
||||||
|
request.priority === 'high' ? 'bg-orange-100 text-orange-700' :
|
||||||
|
request.priority === 'normal' ? 'bg-gray-100 text-gray-700' :
|
||||||
|
'bg-blue-100 text-blue-700'
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
{request.priority === 'critical' ? 'Kritisch' :
|
||||||
|
request.priority === 'high' ? 'Hoch' :
|
||||||
|
request.priority === 'normal' ? 'Normal' : 'Niedrig'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Assignment */}
|
||||||
|
<div className="border-t border-gray-100 pt-4">
|
||||||
|
<div className="text-sm text-gray-500 mb-1">Zugewiesen an</div>
|
||||||
|
<div className="font-medium text-gray-900">
|
||||||
|
{request.assignment.assignedTo || 'Nicht zugewiesen'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions Card */}
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||||
|
<h3 className="font-medium text-gray-900 mb-4">Aktionen</h3>
|
||||||
|
<ActionButtons
|
||||||
|
request={request}
|
||||||
|
onVerifyIdentity={onVerifyIdentity}
|
||||||
|
onExtendDeadline={onExtendDeadline}
|
||||||
|
onComplete={onComplete}
|
||||||
|
onReject={onReject}
|
||||||
|
onAssign={onAssign}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Audit Log Card */}
|
||||||
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||||
|
<AuditLog history={history} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div>
|
||||||
|
{/* Art. 17 - Erasure */}
|
||||||
|
{request.type === 'erasure' && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<DSRErasureChecklistComponent
|
||||||
|
checklist={request.erasureChecklist}
|
||||||
|
onChange={(checklist) => setRequest({ ...request, erasureChecklist: checklist })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Art. 17(3) Exception Checks from Backend */}
|
||||||
|
{exceptionChecks.length > 0 && (
|
||||||
|
<div className="mt-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-3">Art. 17(3) Ausnahmepruefung</h3>
|
||||||
|
<p className="text-sm text-gray-500 mb-4">
|
||||||
|
Pruefen Sie, ob eine der gesetzlichen Ausnahmen zur Loeschpflicht greift.
|
||||||
|
</p>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{exceptionChecks.map((check) => (
|
||||||
|
<div key={check.id} className="bg-gray-50 rounded-xl p-4 border border-gray-200">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={check.applies || false}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="font-medium text-gray-900 text-sm">
|
||||||
|
{check.article}: {check.label}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-0.5">{check.description}</div>
|
||||||
|
{check.checked_by && (
|
||||||
|
<div className="text-xs text-gray-400 mt-1">
|
||||||
|
Geprueft von {check.checked_by} am{' '}
|
||||||
|
{check.checked_at ? new Date(check.checked_at).toLocaleDateString('de-DE') : '-'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Art. 15/20 - Data Export */}
|
||||||
|
{(request.type === 'access' || request.type === 'portability') && (
|
||||||
|
<DSRDataExportComponent
|
||||||
|
dsrId={request.id}
|
||||||
|
dsrType={request.type}
|
||||||
|
existingExport={request.dataExport}
|
||||||
|
onGenerate={async (format) => {
|
||||||
|
// 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 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Zu korrigierende Daten</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{request.rectificationDetails.fieldsToCorrect.map((field, idx) => (
|
||||||
|
<div key={idx} className="bg-gray-50 rounded-xl p-4">
|
||||||
|
<div className="font-medium text-gray-900 mb-2">{field.field}</div>
|
||||||
|
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-500 mb-1">Aktueller Wert</div>
|
||||||
|
<div className="text-red-600 line-through">{field.currentValue}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-gray-500 mb-1">Angeforderter Wert</div>
|
||||||
|
<div className="text-green-600">{field.requestedValue}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{field.corrected && (
|
||||||
|
<div className="mt-2 text-xs text-green-600">
|
||||||
|
Korrigiert am {new Date(field.correctedAt!).toLocaleDateString('de-DE')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Art. 21 - Objection */}
|
||||||
|
{request.type === 'objection' && request.objectionDetails && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900">Widerspruchsdetails</h3>
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="bg-gray-50 rounded-xl p-4">
|
||||||
|
<div className="text-sm text-gray-500 mb-1">Verarbeitungszweck</div>
|
||||||
|
<div className="font-medium">{request.objectionDetails.processingPurpose}</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 rounded-xl p-4">
|
||||||
|
<div className="text-sm text-gray-500 mb-1">Rechtsgrundlage</div>
|
||||||
|
<div className="font-medium">{request.objectionDetails.legalBasis}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-gray-50 rounded-xl p-4">
|
||||||
|
<div className="text-sm text-gray-500 mb-1">Widerspruchsgruende</div>
|
||||||
|
<div>{request.objectionDetails.objectionGrounds}</div>
|
||||||
|
</div>
|
||||||
|
{request.objectionDetails.decision !== 'pending' && (
|
||||||
|
<div className={`
|
||||||
|
rounded-xl p-4 border
|
||||||
|
${request.objectionDetails.decision === 'accepted'
|
||||||
|
? 'bg-green-50 border-green-200'
|
||||||
|
: 'bg-red-50 border-red-200'
|
||||||
|
}
|
||||||
|
`}>
|
||||||
|
<div className={`font-medium ${
|
||||||
|
request.objectionDetails.decision === 'accepted' ? 'text-green-800' : 'text-red-800'
|
||||||
|
}`}>
|
||||||
|
Widerspruch {request.objectionDetails.decision === 'accepted' ? 'angenommen' : 'abgelehnt'}
|
||||||
|
</div>
|
||||||
|
{request.objectionDetails.decisionReason && (
|
||||||
|
<div className={`text-sm mt-1 ${
|
||||||
|
request.objectionDetails.decision === 'accepted' ? 'text-green-700' : 'text-red-700'
|
||||||
|
}`}>
|
||||||
|
{request.objectionDetails.decisionReason}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Default for restriction */}
|
||||||
|
{request.type === 'restriction' && (
|
||||||
|
<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" 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>
|
||||||
|
<div className="font-medium text-blue-800">Einschraenkung der Verarbeitung</div>
|
||||||
|
<p className="text-sm text-blue-700 mt-1">
|
||||||
|
Markieren Sie die betroffenen Daten im System als eingeschraenkt.
|
||||||
|
Die Daten duerfen nur noch gespeichert, aber nicht mehr verarbeitet werden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="text-gray-500">
|
||||||
|
<div className="text-sm">Abgeschlossen am</div>
|
||||||
|
<div className="text-lg font-semibold">
|
||||||
|
{request.completedAt
|
||||||
|
? new Date(request.completedAt).toLocaleDateString('de-DE')
|
||||||
|
: '-'
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${overdue ? 'text-red-600' : urgent ? 'text-orange-600' : 'text-gray-900'}`}>
|
||||||
|
<div className="text-sm">Frist</div>
|
||||||
|
<div className="text-2xl font-bold">
|
||||||
|
{overdue
|
||||||
|
? `${Math.abs(daysRemaining)} Tage ueberfaellig`
|
||||||
|
: `${daysRemaining} Tage`
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 mt-1">
|
||||||
|
bis {new Date(request.deadline.currentDeadline).toLocaleDateString('de-DE')}
|
||||||
|
</div>
|
||||||
|
{request.deadline.extended && (
|
||||||
|
<div className="text-xs text-purple-600 mt-1">
|
||||||
|
(Verlaengert)
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<span className={`px-3 py-1.5 text-sm font-medium rounded-lg ${info.bgColor} ${info.color} border ${info.borderColor}`}>
|
||||||
|
{info.label}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
204
admin-compliance/app/sdk/dsr/[requestId]/_hooks/useDSRDetail.ts
Normal file
204
admin-compliance/app/sdk/dsr/[requestId]/_hooks/useDSRDetail.ts
Normal file
@@ -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<DSRRequest | null>(null)
|
||||||
|
const [communications, setCommunications] = useState<DSRCommunication[]>([])
|
||||||
|
const [history, setHistory] = useState<any[]>([])
|
||||||
|
const [exceptionChecks, setExceptionChecks] = useState<any[]>([])
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,390 +1,42 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState } from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useParams, useRouter } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import {
|
import { DSR_TYPE_INFO, isOverdue, isUrgent } from '@/lib/sdk/dsr/types'
|
||||||
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 {
|
import {
|
||||||
DSRWorkflowStepper,
|
DSRWorkflowStepper,
|
||||||
DSRIdentityModal,
|
DSRIdentityModal,
|
||||||
DSRCommunicationLog,
|
DSRCommunicationLog,
|
||||||
DSRErasureChecklistComponent,
|
|
||||||
DSRDataExportComponent
|
|
||||||
} from '@/components/sdk/dsr'
|
} from '@/components/sdk/dsr'
|
||||||
|
import { DSRHeader } from './_components/DSRHeader'
|
||||||
// =============================================================================
|
import { DSRDetailsTab } from './_components/DSRDetailsTab'
|
||||||
// COMPONENTS
|
import { DSRTypeSpecificTab } from './_components/DSRTypeSpecificTab'
|
||||||
// =============================================================================
|
import { DSRSidebar } from './_components/DSRSidebar'
|
||||||
|
import { useDSRDetail } from './_hooks/useDSRDetail'
|
||||||
function StatusBadge({ status }: { status: string }) {
|
|
||||||
const info = DSR_STATUS_INFO[status as keyof typeof DSR_STATUS_INFO]
|
|
||||||
if (!info) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={`px-3 py-1.5 text-sm font-medium rounded-lg ${info.bgColor} ${info.color} border ${info.borderColor}`}>
|
|
||||||
{info.label}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="text-gray-500">
|
|
||||||
<div className="text-sm">Abgeschlossen am</div>
|
|
||||||
<div className="text-lg font-semibold">
|
|
||||||
{request.completedAt
|
|
||||||
? new Date(request.completedAt).toLocaleDateString('de-DE')
|
|
||||||
: '-'
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`${overdue ? 'text-red-600' : urgent ? 'text-orange-600' : 'text-gray-900'}`}>
|
|
||||||
<div className="text-sm">Frist</div>
|
|
||||||
<div className="text-2xl font-bold">
|
|
||||||
{overdue
|
|
||||||
? `${Math.abs(daysRemaining)} Tage ueberfaellig`
|
|
||||||
: `${daysRemaining} Tage`
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500 mt-1">
|
|
||||||
bis {new Date(request.deadline.currentDeadline).toLocaleDateString('de-DE')}
|
|
||||||
</div>
|
|
||||||
{request.deadline.extended && (
|
|
||||||
<div className="text-xs text-purple-600 mt-1">
|
|
||||||
(Verlaengert)
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<button className="w-full px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-sm">
|
|
||||||
PDF exportieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{!request.identityVerification.verified && (
|
|
||||||
<button
|
|
||||||
onClick={onVerifyIdentity}
|
|
||||||
className="w-full px-4 py-2 bg-yellow-500 text-white hover:bg-yellow-600 rounded-lg transition-colors text-sm font-medium"
|
|
||||||
>
|
|
||||||
Identitaet verifizieren
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={onAssign}
|
|
||||||
className="w-full px-4 py-2 text-purple-600 bg-purple-50 hover:bg-purple-100 rounded-lg transition-colors text-sm"
|
|
||||||
>
|
|
||||||
{request.assignment.assignedTo ? 'Neu zuweisen' : 'Zuweisen'}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={onExtendDeadline}
|
|
||||||
className="w-full px-4 py-2 text-gray-600 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors text-sm"
|
|
||||||
>
|
|
||||||
Frist verlaengern
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="border-t border-gray-200 pt-2 mt-2">
|
|
||||||
<button
|
|
||||||
onClick={onComplete}
|
|
||||||
className="w-full px-4 py-2 bg-green-600 text-white hover:bg-green-700 rounded-lg transition-colors text-sm font-medium"
|
|
||||||
>
|
|
||||||
Abschliessen
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={onReject}
|
|
||||||
className="w-full mt-2 px-4 py-2 text-red-600 bg-red-50 hover:bg-red-100 rounded-lg transition-colors text-sm"
|
|
||||||
>
|
|
||||||
Ablehnen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function AuditLog({ history }: { history: any[] }) {
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
<h4 className="text-sm font-medium text-gray-700">Aktivitaeten</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
{history.length === 0 && (
|
|
||||||
<div className="text-xs text-gray-400">Keine Eintraege</div>
|
|
||||||
)}
|
|
||||||
{history.map((entry, idx) => (
|
|
||||||
<div key={entry.id || idx} className="flex items-start gap-2 text-xs">
|
|
||||||
<div className="w-1.5 h-1.5 rounded-full bg-gray-300 mt-1.5 flex-shrink-0" />
|
|
||||||
<div>
|
|
||||||
<div className="text-gray-900">
|
|
||||||
{entry.previous_status
|
|
||||||
? `${entry.previous_status} → ${entry.new_status}`
|
|
||||||
: entry.new_status
|
|
||||||
}
|
|
||||||
{entry.comment && `: ${entry.comment}`}
|
|
||||||
</div>
|
|
||||||
<div className="text-gray-500">
|
|
||||||
{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'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// MAIN PAGE
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
export default function DSRDetailPage() {
|
export default function DSRDetailPage() {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const router = useRouter()
|
|
||||||
const requestId = params.requestId as string
|
const requestId = params.requestId as string
|
||||||
|
|
||||||
const [request, setRequest] = useState<DSRRequest | null>(null)
|
|
||||||
const [communications, setCommunications] = useState<DSRCommunication[]>([])
|
|
||||||
const [history, setHistory] = useState<any[]>([])
|
|
||||||
const [exceptionChecks, setExceptionChecks] = useState<any[]>([])
|
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
|
||||||
const [showIdentityModal, setShowIdentityModal] = useState(false)
|
const [showIdentityModal, setShowIdentityModal] = useState(false)
|
||||||
const [activeContentTab, setActiveContentTab] = useState<'details' | 'communication' | 'type-specific'>('details')
|
const [activeContentTab, setActiveContentTab] = useState<'details' | 'communication' | 'type-specific'>('details')
|
||||||
|
|
||||||
const reloadRequest = async () => {
|
const {
|
||||||
const found = await fetchSDKDSR(requestId)
|
request,
|
||||||
if (found) setRequest(found)
|
setRequest,
|
||||||
}
|
communications,
|
||||||
|
history,
|
||||||
// Load data from SDK backend
|
exceptionChecks,
|
||||||
useEffect(() => {
|
isLoading,
|
||||||
const loadData = async () => {
|
handleVerifyIdentity,
|
||||||
setIsLoading(true)
|
handleAssign,
|
||||||
try {
|
handleExtendDeadline,
|
||||||
const found = await fetchSDKDSR(requestId)
|
handleComplete,
|
||||||
if (found) {
|
handleReject,
|
||||||
setRequest(found)
|
handleSendCommunication,
|
||||||
// Load communications, history, and exception checks in parallel
|
handleExceptionCheckChange,
|
||||||
const [comms, hist] = await Promise.all([
|
} = useDSRDetail(requestId)
|
||||||
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -428,36 +80,7 @@ export default function DSRDetailPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
<DSRHeader request={request} />
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Link
|
|
||||||
href="/sdk/dsr"
|
|
||||||
className="p-2 text-gray-500 hover:text-gray-700 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="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
|
||||||
</svg>
|
|
||||||
</Link>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-sm 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.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 mt-1">
|
|
||||||
{request.requester.name}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button 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-4 h-4" 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>
|
|
||||||
Exportieren
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Workflow Stepper */}
|
{/* Workflow Stepper */}
|
||||||
<div className={`
|
<div className={`
|
||||||
@@ -498,116 +121,13 @@ export default function DSRDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{/* Details Tab */}
|
|
||||||
{activeContentTab === 'details' && (
|
{activeContentTab === 'details' && (
|
||||||
<div className="space-y-6">
|
<DSRDetailsTab
|
||||||
{/* Request Info */}
|
request={request}
|
||||||
<div className="grid grid-cols-2 gap-6">
|
onShowIdentityModal={() => setShowIdentityModal(true)}
|
||||||
<div>
|
/>
|
||||||
<h4 className="text-sm font-medium text-gray-500 mb-2">Antragsteller</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="font-medium text-gray-900">{request.requester.name}</div>
|
|
||||||
<div className="text-sm text-gray-600">{request.requester.email}</div>
|
|
||||||
{request.requester.phone && (
|
|
||||||
<div className="text-sm text-gray-600">{request.requester.phone}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-gray-500 mb-2">Eingereicht</h4>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="font-medium text-gray-900">
|
|
||||||
{new Date(request.receivedAt).toLocaleDateString('de-DE', {
|
|
||||||
day: '2-digit',
|
|
||||||
month: 'long',
|
|
||||||
year: 'numeric'
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-gray-600">
|
|
||||||
Quelle: {request.source === 'web_form' ? 'Kontaktformular' :
|
|
||||||
request.source === 'email' ? 'E-Mail' :
|
|
||||||
request.source === 'letter' ? 'Brief' :
|
|
||||||
request.source === 'phone' ? 'Telefon' : request.source}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Identity Verification */}
|
|
||||||
<div className={`
|
|
||||||
p-4 rounded-xl border
|
|
||||||
${request.identityVerification.verified
|
|
||||||
? 'bg-green-50 border-green-200'
|
|
||||||
: 'bg-yellow-50 border-yellow-200'
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className={`
|
|
||||||
w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0
|
|
||||||
${request.identityVerification.verified ? 'bg-green-100' : 'bg-yellow-100'}
|
|
||||||
`}>
|
|
||||||
{request.identityVerification.verified ? (
|
|
||||||
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
|
||||||
</svg>
|
|
||||||
) : (
|
|
||||||
<svg className="w-4 h-4 text-yellow-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">
|
|
||||||
<div className={`font-medium ${request.identityVerification.verified ? 'text-green-800' : 'text-yellow-800'}`}>
|
|
||||||
{request.identityVerification.verified
|
|
||||||
? 'Identitaet verifiziert'
|
|
||||||
: 'Identitaetspruefung ausstehend'
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
{request.identityVerification.verified && (
|
|
||||||
<div className="text-sm text-green-700 mt-1">
|
|
||||||
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')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!request.identityVerification.verified && (
|
|
||||||
<button
|
|
||||||
onClick={() => setShowIdentityModal(true)}
|
|
||||||
className="px-3 py-1.5 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700 transition-colors text-sm"
|
|
||||||
>
|
|
||||||
Jetzt pruefen
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Request Text */}
|
|
||||||
{request.requestText && (
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-gray-500 mb-2">Anfragetext</h4>
|
|
||||||
<div className="bg-gray-50 rounded-xl p-4 text-gray-700 whitespace-pre-wrap">
|
|
||||||
{request.requestText}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Notes */}
|
|
||||||
{request.notes && (
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-gray-500 mb-2">Notizen</h4>
|
|
||||||
<div className="bg-gray-50 rounded-xl p-4 text-gray-700">
|
|
||||||
{request.notes}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Communication Tab */}
|
|
||||||
{activeContentTab === 'communication' && (
|
{activeContentTab === 'communication' && (
|
||||||
<DSRCommunicationLog
|
<DSRCommunicationLog
|
||||||
communications={communications}
|
communications={communications}
|
||||||
@@ -615,217 +135,22 @@ export default function DSRDetailPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Type-Specific Tab */}
|
|
||||||
{activeContentTab === 'type-specific' && (
|
{activeContentTab === 'type-specific' && (
|
||||||
<div>
|
<DSRTypeSpecificTab
|
||||||
{/* Art. 17 - Erasure */}
|
request={request}
|
||||||
{request.type === 'erasure' && (
|
setRequest={setRequest}
|
||||||
<div className="space-y-4">
|
exceptionChecks={exceptionChecks}
|
||||||
<DSRErasureChecklistComponent
|
onExceptionCheckChange={handleExceptionCheckChange}
|
||||||
checklist={request.erasureChecklist}
|
|
||||||
onChange={(checklist) => setRequest({ ...request, erasureChecklist: checklist })}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Art. 17(3) Exception Checks from Backend */}
|
|
||||||
{exceptionChecks.length > 0 && (
|
|
||||||
<div className="mt-6">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">Art. 17(3) Ausnahmepruefung</h3>
|
|
||||||
<p className="text-sm text-gray-500 mb-4">
|
|
||||||
Pruefen Sie, ob eine der gesetzlichen Ausnahmen zur Loeschpflicht greift.
|
|
||||||
</p>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{exceptionChecks.map((check) => (
|
|
||||||
<div key={check.id} className="bg-gray-50 rounded-xl p-4 border border-gray-200">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={check.applies || false}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="font-medium text-gray-900 text-sm">
|
|
||||||
{check.article}: {check.label}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-gray-500 mt-0.5">{check.description}</div>
|
|
||||||
{check.checked_by && (
|
|
||||||
<div className="text-xs text-gray-400 mt-1">
|
|
||||||
Geprueft von {check.checked_by} am{' '}
|
|
||||||
{check.checked_at ? new Date(check.checked_at).toLocaleDateString('de-DE') : '-'}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Art. 15/20 - Data Export */}
|
|
||||||
{(request.type === 'access' || request.type === 'portability') && (
|
|
||||||
<DSRDataExportComponent
|
|
||||||
dsrId={request.id}
|
|
||||||
dsrType={request.type}
|
|
||||||
existingExport={request.dataExport}
|
|
||||||
onGenerate={async (format) => {
|
|
||||||
// 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 && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Zu korrigierende Daten</h3>
|
|
||||||
<div className="space-y-3">
|
|
||||||
{request.rectificationDetails.fieldsToCorrect.map((field, idx) => (
|
|
||||||
<div key={idx} className="bg-gray-50 rounded-xl p-4">
|
|
||||||
<div className="font-medium text-gray-900 mb-2">{field.field}</div>
|
|
||||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
|
||||||
<div>
|
|
||||||
<div className="text-gray-500 mb-1">Aktueller Wert</div>
|
|
||||||
<div className="text-red-600 line-through">{field.currentValue}</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="text-gray-500 mb-1">Angeforderter Wert</div>
|
|
||||||
<div className="text-green-600">{field.requestedValue}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{field.corrected && (
|
|
||||||
<div className="mt-2 text-xs text-green-600">
|
|
||||||
Korrigiert am {new Date(field.correctedAt!).toLocaleDateString('de-DE')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Art. 21 - Objection */}
|
|
||||||
{request.type === 'objection' && request.objectionDetails && (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900">Widerspruchsdetails</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div className="bg-gray-50 rounded-xl p-4">
|
|
||||||
<div className="text-sm text-gray-500 mb-1">Verarbeitungszweck</div>
|
|
||||||
<div className="font-medium">{request.objectionDetails.processingPurpose}</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-50 rounded-xl p-4">
|
|
||||||
<div className="text-sm text-gray-500 mb-1">Rechtsgrundlage</div>
|
|
||||||
<div className="font-medium">{request.objectionDetails.legalBasis}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-gray-50 rounded-xl p-4">
|
|
||||||
<div className="text-sm text-gray-500 mb-1">Widerspruchsgruende</div>
|
|
||||||
<div>{request.objectionDetails.objectionGrounds}</div>
|
|
||||||
</div>
|
|
||||||
{request.objectionDetails.decision !== 'pending' && (
|
|
||||||
<div className={`
|
|
||||||
rounded-xl p-4 border
|
|
||||||
${request.objectionDetails.decision === 'accepted'
|
|
||||||
? 'bg-green-50 border-green-200'
|
|
||||||
: 'bg-red-50 border-red-200'
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
<div className={`font-medium ${
|
|
||||||
request.objectionDetails.decision === 'accepted' ? 'text-green-800' : 'text-red-800'
|
|
||||||
}`}>
|
|
||||||
Widerspruch {request.objectionDetails.decision === 'accepted' ? 'angenommen' : 'abgelehnt'}
|
|
||||||
</div>
|
|
||||||
{request.objectionDetails.decisionReason && (
|
|
||||||
<div className={`text-sm mt-1 ${
|
|
||||||
request.objectionDetails.decision === 'accepted' ? 'text-green-700' : 'text-red-700'
|
|
||||||
}`}>
|
|
||||||
{request.objectionDetails.decisionReason}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Default for restriction */}
|
|
||||||
{request.type === 'restriction' && (
|
|
||||||
<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" 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>
|
|
||||||
<div className="font-medium text-blue-800">Einschraenkung der Verarbeitung</div>
|
|
||||||
<p className="text-sm text-blue-700 mt-1">
|
|
||||||
Markieren Sie die betroffenen Daten im System als eingeschraenkt.
|
|
||||||
Die Daten duerfen nur noch gespeichert, aber nicht mehr verarbeitet werden.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - 1/3 Sidebar */}
|
{/* Right Column - 1/3 Sidebar */}
|
||||||
<div className="space-y-6">
|
<DSRSidebar
|
||||||
{/* Status Card */}
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="font-medium text-gray-900">Status</h3>
|
|
||||||
<StatusBadge status={request.status} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-t border-gray-100 pt-4">
|
|
||||||
<DeadlineDisplay request={request} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Priority */}
|
|
||||||
<div className="border-t border-gray-100 pt-4">
|
|
||||||
<div className="text-sm text-gray-500 mb-1">Prioritaet</div>
|
|
||||||
<div className={`
|
|
||||||
inline-flex px-2 py-1 text-sm font-medium rounded-lg
|
|
||||||
${request.priority === 'critical' ? 'bg-red-100 text-red-700' :
|
|
||||||
request.priority === 'high' ? 'bg-orange-100 text-orange-700' :
|
|
||||||
request.priority === 'normal' ? 'bg-gray-100 text-gray-700' :
|
|
||||||
'bg-blue-100 text-blue-700'
|
|
||||||
}
|
|
||||||
`}>
|
|
||||||
{request.priority === 'critical' ? 'Kritisch' :
|
|
||||||
request.priority === 'high' ? 'Hoch' :
|
|
||||||
request.priority === 'normal' ? 'Normal' : 'Niedrig'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Assignment */}
|
|
||||||
<div className="border-t border-gray-100 pt-4">
|
|
||||||
<div className="text-sm text-gray-500 mb-1">Zugewiesen an</div>
|
|
||||||
<div className="font-medium text-gray-900">
|
|
||||||
{request.assignment.assignedTo || 'Nicht zugewiesen'}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Actions Card */}
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
||||||
<h3 className="font-medium text-gray-900 mb-4">Aktionen</h3>
|
|
||||||
<ActionButtons
|
|
||||||
request={request}
|
request={request}
|
||||||
|
history={history}
|
||||||
onVerifyIdentity={() => setShowIdentityModal(true)}
|
onVerifyIdentity={() => setShowIdentityModal(true)}
|
||||||
onExtendDeadline={handleExtendDeadline}
|
onExtendDeadline={handleExtendDeadline}
|
||||||
onComplete={handleComplete}
|
onComplete={handleComplete}
|
||||||
@@ -834,13 +159,6 @@ export default function DSRDetailPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Audit Log Card */}
|
|
||||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
||||||
<AuditLog history={history} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Identity Modal */}
|
{/* Identity Modal */}
|
||||||
<DSRIdentityModal
|
<DSRIdentityModal
|
||||||
isOpen={showIdentityModal}
|
isOpen={showIdentityModal}
|
||||||
|
|||||||
Reference in New Issue
Block a user