'use client' /** * DSR (Data Subject Requests) Admin Page * * GDPR Article 15-21 Request Management */ import AdminLayout from '@/components/admin/AdminLayout' import { useEffect, useState, useCallback } from 'react' interface DSRRequest { id: string request_number: string requester_email: string requester_name: string request_type: string status: string priority: string created_at: string deadline: string assigned_to?: string notes?: string } interface DSRStats { total: number pending: number in_progress: number completed: number overdue: number } export default function DSRManagementPage() { const [adminToken, setAdminToken] = useState('') const [requests, setRequests] = useState([]) const [stats, setStats] = useState(null) const [loading, setLoading] = useState(false) const [selectedRequest, setSelectedRequest] = useState(null) const [filter, setFilter] = useState('all') const [error, setError] = useState(null) const API_BASE = 'http://localhost:8081/api/v1' // Load saved token useEffect(() => { const savedToken = localStorage.getItem('adminToken') if (savedToken) { setAdminToken(savedToken) } }, []) // Save token const saveToken = (token: string) => { setAdminToken(token) localStorage.setItem('adminToken', token) } // Fetch DSR requests const fetchRequests = useCallback(async () => { if (!adminToken) return setLoading(true) setError(null) try { const response = await fetch(`${API_BASE}/dsr/requests`, { headers: { 'Authorization': `Bearer ${adminToken}`, }, }) if (!response.ok) { if (response.status === 401) { throw new Error('Nicht autorisiert - Token ungültig') } throw new Error(`HTTP ${response.status}`) } const data = await response.json() setRequests(data.requests || []) // Calculate stats const allRequests = data.requests || [] const now = new Date() setStats({ total: allRequests.length, pending: allRequests.filter((r: DSRRequest) => r.status === 'pending').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) => new Date(r.deadline) < now && r.status !== 'completed').length, }) } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Laden') } finally { setLoading(false) } }, [adminToken]) useEffect(() => { if (adminToken) { fetchRequests() } }, [adminToken, fetchRequests]) // Get status badge color const getStatusColor = (status: string) => { switch (status) { case 'pending': 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' default: return 'bg-slate-100 text-slate-800' } } // Get priority badge color const getPriorityColor = (priority: string) => { switch (priority) { case 'urgent': return 'bg-red-100 text-red-800' case 'high': return 'bg-orange-100 text-orange-800' case 'normal': return 'bg-slate-100 text-slate-800' case 'low': return 'bg-slate-50 text-slate-600' default: return 'bg-slate-100 text-slate-800' } } // 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 } // Filter requests const filteredRequests = requests.filter(r => { if (filter === 'all') return true if (filter === 'overdue') { return new Date(r.deadline) < new Date() && r.status !== 'completed' } 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 = (deadline: string, status: string) => { return new Date(deadline) < new Date() && status !== 'completed' } return ( {/* Token Input */}
saveToken(e.target.value)} placeholder="JWT Token eingeben..." className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500" />
{/* Error Message */} {error && (
{error}
)} {/* Stats */} {stats && (
{stats.total}
Gesamt
{stats.pending}
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: 'pending', 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) => ( )) )}
Nr. Typ Anfragesteller Status Priorität Frist Aktionen
Keine Anfragen gefunden
{request.request_number} {getTypeLabel(request.request_type)}
{request.requester_name}
{request.requester_email}
{request.status} {request.priority} {formatDate(request.deadline)}
{/* Detail Modal */} {selectedRequest && (

Anfrage {selectedRequest.request_number}

Typ
{getTypeLabel(selectedRequest.request_type)}
Status
{selectedRequest.status}
Anfragesteller
{selectedRequest.requester_name}
{selectedRequest.requester_email}
Frist
{formatDate(selectedRequest.deadline)}
Eingegangen
{formatDate(selectedRequest.created_at)}
Zugewiesen an
{selectedRequest.assigned_to || '-'}
{selectedRequest.notes && (
Notizen
{selectedRequest.notes}
)}
)} {/* Info Box */}

DSGVO-Fristen

  • Art. 15 (Auskunft): 1 Monat, verlängerbar auf 3 Monate
  • Art. 16 (Berichtigung): Unverzüglich
  • Art. 17 (Löschung): Unverzüglich
  • Art. 18 (Einschränkung): Unverzüglich
  • Art. 20 (Datenübertragbarkeit): 1 Monat
  • Art. 21 (Widerspruch): Unverzüglich
) }