diff --git a/admin-compliance/app/sdk/whistleblower/_components/CaseDetailPanel.tsx b/admin-compliance/app/sdk/whistleblower/_components/CaseDetailPanel.tsx new file mode 100644 index 0000000..da2738a --- /dev/null +++ b/admin-compliance/app/sdk/whistleblower/_components/CaseDetailPanel.tsx @@ -0,0 +1,296 @@ +'use client' + +import React, { useState } from 'react' +import { + WhistleblowerReport, + ReportStatus, + REPORT_CATEGORY_INFO, + REPORT_STATUS_INFO +} from '@/lib/sdk/whistleblower/types' + +export function CaseDetailPanel({ + report, + onClose, + onUpdated, + onDeleted, +}: { + report: WhistleblowerReport + onClose: () => void + onUpdated: () => void + onDeleted?: () => void +}) { + const [officerName, setOfficerName] = useState(report.assignedTo || '') + const [commentText, setCommentText] = useState('') + const [isSavingOfficer, setIsSavingOfficer] = useState(false) + const [isSavingStatus, setIsSavingStatus] = useState(false) + const [isSendingComment, setIsSendingComment] = useState(false) + const [isDeleting, setIsDeleting] = useState(false) + const [actionError, setActionError] = useState(null) + + const handleDeleteReport = async () => { + if (!window.confirm(`Meldung "${report.title}" wirklich löschen?`)) return + setIsDeleting(true) + try { + const res = await fetch(`/api/sdk/v1/whistleblower/reports/${report.id}`, { method: 'DELETE' }) + if (!res.ok) throw new Error(`Fehler: ${res.status}`) + onDeleted ? onDeleted() : onClose() + } catch (err: unknown) { + setActionError(err instanceof Error ? err.message : 'Löschen fehlgeschlagen.') + } finally { + setIsDeleting(false) + } + } + + const categoryInfo = REPORT_CATEGORY_INFO[report.category] + const statusInfo = REPORT_STATUS_INFO[report.status] + + const statusTransitions: Partial> = { + new: [{ label: 'Bestaetigen', next: 'acknowledged' }], + acknowledged: [{ label: 'Pruefung starten', next: 'under_review' }], + under_review: [{ label: 'Untersuchung starten', next: 'investigation' }], + investigation: [{ label: 'Massnahmen eingeleitet', next: 'measures_taken' }], + measures_taken: [{ label: 'Abschliessen', next: 'closed' }] + } + + const transitions = statusTransitions[report.status] || [] + + const handleStatusChange = async (newStatus: string) => { + setIsSavingStatus(true) + setActionError(null) + try { + const res = await fetch(`/api/sdk/v1/whistleblower/reports/${report.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ status: newStatus }) + }) + if (!res.ok) { + const data = await res.json().catch(() => ({})) + throw new Error(data?.detail || data?.message || `Fehler ${res.status}`) + } + onUpdated() + } catch (err: unknown) { + setActionError(err instanceof Error ? err.message : 'Unbekannter Fehler') + } finally { + setIsSavingStatus(false) + } + } + + const handleSaveOfficer = async () => { + setIsSavingOfficer(true) + setActionError(null) + try { + const res = await fetch(`/api/sdk/v1/whistleblower/reports/${report.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ assignedTo: officerName }) + }) + if (!res.ok) { + const data = await res.json().catch(() => ({})) + throw new Error(data?.detail || data?.message || `Fehler ${res.status}`) + } + onUpdated() + } catch (err: unknown) { + setActionError(err instanceof Error ? err.message : 'Unbekannter Fehler') + } finally { + setIsSavingOfficer(false) + } + } + + const handleSendComment = async () => { + if (!commentText.trim()) return + setIsSendingComment(true) + setActionError(null) + try { + const res = await fetch(`/api/sdk/v1/whistleblower/reports/${report.id}/messages`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ senderRole: 'ombudsperson', message: commentText.trim() }) + }) + if (!res.ok) { + const data = await res.json().catch(() => ({})) + throw new Error(data?.detail || data?.message || `Fehler ${res.status}`) + } + setCommentText('') + onUpdated() + } catch (err: unknown) { + setActionError(err instanceof Error ? err.message : 'Unbekannter Fehler') + } finally { + setIsSendingComment(false) + } + } + + return ( + <> + {/* Backdrop */} +
+ + {/* Drawer */} +
+ {/* Header */} +
+
+

{report.referenceNumber}

+

{report.title}

+
+ +
+ +
+ {actionError && ( +
+ {actionError} +
+ )} + + {/* Badges */} +
+ + {categoryInfo.label} + + + {statusInfo.label} + + {report.isAnonymous && ( + + Anonym + + )} + + {report.priority === 'critical' ? 'Kritisch' : + report.priority === 'high' ? 'Hoch' : + report.priority === 'normal' ? 'Normal' : 'Niedrig'} + +
+ + {/* Description */} +
+

Beschreibung

+

{report.description}

+
+ + {/* Details */} +
+
+

Eingegangen am

+

+ {new Date(report.receivedAt).toLocaleDateString('de-DE')} +

+
+
+

Zugewiesen an

+

+ {report.assignedTo || '—'} +

+
+
+ + {/* Status Transitions */} + {transitions.length > 0 && ( +
+

Status aendern

+
+ {transitions.map((t) => ( + + ))} +
+
+ )} + + {/* Assign Officer */} +
+

Zuweisen an:

+
+ setOfficerName(e.target.value)} + className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-purple-500 text-sm" + placeholder="Name der zustaendigen Person" + /> + +
+
+ + {/* Comment Section */} +
+

Kommentar senden

+