'use client' /** * DSR (Data Subject Requests) Admin Page * * GDPR Article 15-21 Request Management * * Migriert auf SDK API: /sdk/v1/dsgvo/dsr */ import { useEffect, useState, useCallback } from 'react' import { PagePurpose } from '@/components/common/PagePurpose' interface DSRRequest { id: string tenant_id: string namespace_id?: string request_type: string // access, rectification, erasure, restriction, portability, objection status: string // received, verified, in_progress, completed, rejected, extended subject_name: string subject_email: string subject_identifier?: string request_description: string request_channel: string // email, form, phone, letter received_at: string verified_at?: string verification_method?: string deadline_at: string extended_deadline_at?: string extension_reason?: string completed_at?: string response_sent: boolean response_sent_at?: string response_method?: string rejection_reason?: string notes?: string affected_systems?: string[] assigned_to?: string created_at: string updated_at: string } interface DSRStats { total: number received: number in_progress: number completed: number overdue: number } export default function DSRPage() { const [requests, setRequests] = useState([]) const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) const [selectedRequest, setSelectedRequest] = useState(null) const [filter, setFilter] = useState('all') const [error, setError] = useState(null) const [showCreateModal, setShowCreateModal] = useState(false) const [newRequest, setNewRequest] = useState({ request_type: 'access', subject_name: '', subject_email: '', subject_identifier: '', request_description: '', request_channel: 'email', notes: '' }) useEffect(() => { loadRequests() }, []) async function loadRequests() { setLoading(true) setError(null) try { const res = await fetch('/sdk/v1/dsgvo/dsr', { headers: { 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', } }) if (!res.ok) { throw new Error(`HTTP ${res.status}`) } const data = await res.json() const allRequests = data.dsrs || [] setRequests(allRequests) // Calculate stats const now = new Date() setStats({ total: allRequests.length, received: allRequests.filter((r: DSRRequest) => r.status === 'received' || r.status === 'verified').length, in_progress: allRequests.filter((r: DSRRequest) => r.status === 'in_progress').length, completed: allRequests.filter((r: DSRRequest) => r.status === 'completed').length, overdue: allRequests.filter((r: DSRRequest) => { const deadline = r.extended_deadline_at ? new Date(r.extended_deadline_at) : new Date(r.deadline_at) return deadline < now && r.status !== 'completed' && r.status !== 'rejected' }).length, }) } catch (err) { console.error('Failed to load DSRs:', err) setError('Fehler beim Laden der Anfragen') } finally { setLoading(false) } } async function createRequest() { try { const res = await fetch('/sdk/v1/dsgvo/dsr', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', }, body: JSON.stringify(newRequest) }) if (!res.ok) { throw new Error(`HTTP ${res.status}`) } setShowCreateModal(false) setNewRequest({ request_type: 'access', subject_name: '', subject_email: '', subject_identifier: '', request_description: '', request_channel: 'email', notes: '' }) loadRequests() } catch (err) { console.error('Failed to create DSR:', err) alert('Fehler beim Erstellen der Anfrage') } } async function updateStatus(id: string, status: string) { try { const res = await fetch(`/sdk/v1/dsgvo/dsr/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', }, body: JSON.stringify({ status }) }) if (!res.ok) { throw new Error(`HTTP ${res.status}`) } setSelectedRequest(null) loadRequests() } catch (err) { console.error('Failed to update DSR:', err) alert('Fehler beim Aktualisieren') } } async function exportDSRs(format: 'csv' | 'json') { try { const res = await fetch(`/sdk/v1/dsgvo/export/dsr?format=${format}`, { headers: { 'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '', 'X-User-ID': localStorage.getItem('bp_user_id') || '', } }) if (!res.ok) { throw new Error(`HTTP ${res.status}`) } const blob = await res.blob() const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = `dsr-export.${format}` a.click() window.URL.revokeObjectURL(url) } catch (err) { console.error('Export failed:', err) alert('Export fehlgeschlagen') } } // Get status badge color const getStatusColor = (status: string) => { switch (status) { case 'received': return 'bg-slate-100 text-slate-800' case 'verified': return 'bg-yellow-100 text-yellow-800' case 'in_progress': return 'bg-blue-100 text-blue-800' case 'completed': return 'bg-green-100 text-green-800' case 'rejected': return 'bg-red-100 text-red-800' case 'extended': return 'bg-orange-100 text-orange-800' default: return 'bg-slate-100 text-slate-800' } } const getStatusLabel = (status: string) => { const labels: Record = { 'received': 'Eingegangen', 'verified': 'Verifiziert', 'in_progress': 'In Bearbeitung', 'completed': 'Abgeschlossen', 'rejected': 'Abgelehnt', 'extended': 'Verlängert' } return labels[status] || status } // Get request type label const getTypeLabel = (type: string) => { const labels: Record = { 'access': 'Auskunft (Art. 15)', 'rectification': 'Berichtigung (Art. 16)', 'erasure': 'Löschung (Art. 17)', 'restriction': 'Einschränkung (Art. 18)', 'portability': 'Datenübertragbarkeit (Art. 20)', 'objection': 'Widerspruch (Art. 21)', } return labels[type] || type } const getChannelLabel = (channel: string) => { const labels: Record = { 'email': 'E-Mail', 'form': 'Formular', 'phone': 'Telefon', 'letter': 'Brief', } return labels[channel] || channel } // Filter requests const filteredRequests = requests.filter(r => { if (filter === 'all') return true if (filter === 'overdue') { const deadline = r.extended_deadline_at ? new Date(r.extended_deadline_at) : new Date(r.deadline_at) return deadline < new Date() && r.status !== 'completed' && r.status !== 'rejected' } if (filter === 'open') { return r.status === 'received' || r.status === 'verified' } return r.status === filter }) // Format date const formatDate = (dateStr: string) => { return new Date(dateStr).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }) } // Check if overdue const isOverdue = (request: DSRRequest) => { const deadline = request.extended_deadline_at ? new Date(request.extended_deadline_at) : new Date(request.deadline_at) return deadline < new Date() && request.status !== 'completed' && request.status !== 'rejected' } // Calculate days until deadline const daysUntilDeadline = (request: DSRRequest) => { const deadline = request.extended_deadline_at ? new Date(request.extended_deadline_at) : new Date(request.deadline_at) const now = new Date() const diff = Math.ceil((deadline.getTime() - now.getTime()) / (1000 * 60 * 60 * 24)) return diff } if (loading) { return (
Lade Anfragen...
) } return (
{/* Page Purpose */} {/* Error Message */} {error && (
{error}
)} {/* Header Actions */}
{/* Stats */} {stats && (
{stats.total}
Gesamt
{stats.received}
Offen
{stats.in_progress}
In Bearbeitung
{stats.completed}
Abgeschlossen
0 ? 'text-red-600' : 'text-slate-400'}`}> {stats.overdue}
Überfällig
)} {/* Filter Tabs */}
{[ { value: 'all', label: 'Alle' }, { value: 'open', label: 'Offen' }, { value: 'in_progress', label: 'In Bearbeitung' }, { value: 'completed', label: 'Abgeschlossen' }, { value: 'overdue', label: 'Überfällig' }, ].map((tab) => ( ))}
{/* Requests Table */}
{filteredRequests.length === 0 ? ( ) : ( filteredRequests.map((request) => ( )) )}
Typ Betroffener Status Kanal Frist Aktionen
Keine Anfragen gefunden
{getTypeLabel(request.request_type)}
{request.subject_name}
{request.subject_email}
{getStatusLabel(request.status)} {getChannelLabel(request.request_channel)}
{formatDate(request.extended_deadline_at || request.deadline_at)}
{request.status !== 'completed' && request.status !== 'rejected' && (
{daysUntilDeadline(request) < 0 ? `${Math.abs(daysUntilDeadline(request))} Tage überfällig` : `${daysUntilDeadline(request)} Tage verbleibend`}
)}
{/* Detail Modal */} {selectedRequest && (

{getTypeLabel(selectedRequest.request_type)}

Betroffener
{selectedRequest.subject_name}
{selectedRequest.subject_email}
Status
{getStatusLabel(selectedRequest.status)}
Eingegangen am
{formatDate(selectedRequest.received_at)}
Frist
{formatDate(selectedRequest.extended_deadline_at || selectedRequest.deadline_at)} {selectedRequest.extended_deadline_at && ( (verlängert) )}
Kanal
{getChannelLabel(selectedRequest.request_channel)}
{selectedRequest.subject_identifier && (
Kunden-ID
{selectedRequest.subject_identifier}
)}
{selectedRequest.request_description && (
Beschreibung
{selectedRequest.request_description}
)} {selectedRequest.notes && (
Notizen
{selectedRequest.notes}
)} {selectedRequest.affected_systems && selectedRequest.affected_systems.length > 0 && (
Betroffene Systeme
{selectedRequest.affected_systems.map((sys, idx) => ( {sys} ))}
)}
{selectedRequest.status === 'received' && ( )} {(selectedRequest.status === 'received' || selectedRequest.status === 'verified') && ( )} {selectedRequest.status === 'in_progress' && ( <> )}
)} {/* Create Modal */} {showCreateModal && (

Neue Anfrage erfassen

setNewRequest({ ...newRequest, subject_name: e.target.value })} className="w-full border border-slate-300 rounded-lg px-3 py-2" placeholder="Max Mustermann" />
setNewRequest({ ...newRequest, subject_email: e.target.value })} className="w-full border border-slate-300 rounded-lg px-3 py-2" placeholder="max@example.com" />
setNewRequest({ ...newRequest, subject_identifier: e.target.value })} className="w-full border border-slate-300 rounded-lg px-3 py-2" placeholder="z.B. CUST-12345" />