From 1f45d6cca8ca613f5980f1ff39084548ffef41f6 Mon Sep 17 00:00:00 2001 From: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:50:25 +0200 Subject: [PATCH] refactor(admin): split whistleblower page.tsx + restore scope helpers Whistleblower (1220 -> 349 LOC) split into 6 colocated components: TabNavigation, StatCard, FilterBar, ReportCard, WhistleblowerCreateModal, CaseDetailPanel. All under the 300 LOC soft target. Drive-by fix: the earlier fc6a330 split of compliance-scope-types.ts dropped several helper exports that downstream consumers still import (lib/sdk/index.ts, compliance-scope-engine.ts, obligations page, compliance-scope page, constraint-enforcer, drafting-engine validate). Restored them in the appropriate domain modules: - core-levels.ts: maxDepthLevel, getDepthLevelNumeric, depthLevelFromNumeric - state.ts: createEmptyScopeState - decisions.ts: createEmptyScopeDecision + ApplicableRegulation, RegulationObligation, RegulationAssessmentResult, SupervisoryAuthorityInfo Verification: next build clean (142 pages generated), /sdk/whistleblower still builds at ~11.5 kB. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../_components/CaseDetailPanel.tsx | 296 ++++++ .../whistleblower/_components/FilterBar.tsx | 83 ++ .../whistleblower/_components/ReportCard.tsx | 191 ++++ .../whistleblower/_components/StatCard.tsx | 51 + .../_components/TabNavigation.tsx | 54 ++ .../_components/WhistleblowerCreateModal.tsx | 223 +++++ .../app/sdk/whistleblower/page.tsx | 883 +----------------- .../sdk/compliance-scope-types/core-levels.ts | 39 + .../sdk/compliance-scope-types/decisions.ts | 76 ++ .../lib/sdk/compliance-scope-types/state.ts | 12 + 10 files changed, 1031 insertions(+), 877 deletions(-) create mode 100644 admin-compliance/app/sdk/whistleblower/_components/CaseDetailPanel.tsx create mode 100644 admin-compliance/app/sdk/whistleblower/_components/FilterBar.tsx create mode 100644 admin-compliance/app/sdk/whistleblower/_components/ReportCard.tsx create mode 100644 admin-compliance/app/sdk/whistleblower/_components/StatCard.tsx create mode 100644 admin-compliance/app/sdk/whistleblower/_components/TabNavigation.tsx create mode 100644 admin-compliance/app/sdk/whistleblower/_components/WhistleblowerCreateModal.tsx 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

+