From 143e65e3d91bed32f3f3d2b8a6795283c96225aa Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Mon, 16 Mar 2026 16:22:31 +0000 Subject: [PATCH] feat: add case deletion for admins with inline confirmation - Add DELETE /cases/{case_id} endpoint (admin-only) with cascading cleanup of DisclosureRequests and audit trail preservation - Add useCaseDelete() mutation hook with cache invalidation - Add delete button in CaseDetail sheet with inline confirmation UI Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/api/cases.py | 56 ++++++++++++++++++++++++++++++++ frontend/src/hooks/useCases.ts | 12 +++++++ frontend/src/pages/CasesPage.tsx | 51 +++++++++++++++++++++++++++-- 3 files changed, 116 insertions(+), 3 deletions(-) diff --git a/backend/app/api/cases.py b/backend/app/api/cases.py index d95c4d8..16cee36 100644 --- a/backend/app/api/cases.py +++ b/backend/app/api/cases.py @@ -14,6 +14,7 @@ from sqlalchemy.orm import Session from app.core.dependencies import get_current_user, require_admin from app.database import get_db +from app.models.audit import DisclosureRequest from app.models.case import Case from app.models.user import User from app.schemas.case import ( @@ -554,6 +555,61 @@ def update_case( return case +# --------------------------------------------------------------------------- +# Delete case (admin only) +# --------------------------------------------------------------------------- + + +@router.delete("/{case_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_case( + case_id: int, + request: Request, + db: Session = Depends(get_db), + user: User = Depends(require_admin), +): + """Permanently delete a case and its related records (admin only). + + Cascading behaviour: + - CaseICDCode records are deleted automatically (DB CASCADE). + - DisclosureRequest records are deleted manually (no DB CASCADE). + - AuditLog entries are kept (no FK constraint) as a deletion trace. + """ + case = db.query(Case).filter(Case.id == case_id).first() + if not case: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Case not found", + ) + + fall_id = case.fall_id + case_info = { + "fall_id": fall_id, + "nachname": case.nachname, + "vorname": case.vorname, + "kvnr": case.kvnr, + "fallgruppe": case.fallgruppe, + "jahr": case.jahr, + "kw": case.kw, + } + + # Remove DisclosureRequests first (no DB cascade) + db.query(DisclosureRequest).filter(DisclosureRequest.case_id == case_id).delete() + + db.delete(case) + db.commit() + + log_action( + db, + user_id=user.id, + action="case_deleted", + entity_type="case", + entity_id=case_id, + old_values=case_info, + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get("user-agent"), + ) + + # --------------------------------------------------------------------------- # ICD entry # --------------------------------------------------------------------------- diff --git a/frontend/src/hooks/useCases.ts b/frontend/src/hooks/useCases.ts index 4ad034d..b13e2a7 100644 --- a/frontend/src/hooks/useCases.ts +++ b/frontend/src/hooks/useCases.ts @@ -68,3 +68,15 @@ export function useIcdUpdate() { }, }) } + +export function useCaseDelete() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (id: number) => + api.delete(`/cases/${id}`), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['cases'] }) + queryClient.invalidateQueries({ queryKey: ['dashboard'] }) + }, + }) +} diff --git a/frontend/src/pages/CasesPage.tsx b/frontend/src/pages/CasesPage.tsx index 98dd6d5..047666e 100644 --- a/frontend/src/pages/CasesPage.tsx +++ b/frontend/src/pages/CasesPage.tsx @@ -5,7 +5,7 @@ import { } from 'lucide-react' import type { Case } from '@/types' import { useAuth } from '@/context/AuthContext' -import { useCases, usePendingIcdCases, useIcdUpdate, type CaseFilters } from '@/hooks/useCases' +import { useCases, usePendingIcdCases, useIcdUpdate, useCaseDelete, type CaseFilters } from '@/hooks/useCases' import api from '@/services/api' import { useFilterPresets, useCreateFilterPreset, useDeleteFilterPreset } from '@/hooks/useFilterPresets' import { Input } from '@/components/ui/input' @@ -472,7 +472,10 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) { {selectedCase && ( - + { setSheetOpen(false); setSelectedCase(null) }} + /> )} @@ -590,7 +593,7 @@ function StatusBadges({ c }: { c: Case }) { ) } -function CaseDetail({ caseData }: { caseData: Case }) { +function CaseDetail({ caseData, onDeleted }: { caseData: Case; onDeleted?: () => void }) { const { isAdmin } = useAuth() const { editing, formValues, saving, error, success, @@ -603,10 +606,15 @@ function CaseDetail({ caseData }: { caseData: Case }) { const [icdError, setIcdError] = useState('') const [icdSuccess, setIcdSuccess] = useState(false) + // Delete state + const deleteMutation = useCaseDelete() + const [confirmDelete, setConfirmDelete] = useState(false) + useEffect(() => { setIcdValue(caseData.icd || '') setIcdError('') setIcdSuccess(false) + setConfirmDelete(false) }, [caseData.id, caseData.icd]) const saveIcd = async () => { @@ -621,6 +629,15 @@ function CaseDetail({ caseData }: { caseData: Case }) { } } + const handleDelete = async () => { + try { + await deleteMutation.mutateAsync(caseData.id) + onDeleted?.() + } catch { + // error is visible via deleteMutation.isError + } + } + return ( <> @@ -654,6 +671,34 @@ function CaseDetail({ caseData }: { caseData: Case }) { )} + {/* Delete button (admin only) */} + {isAdmin && !editing && ( +
+ {!confirmDelete ? ( + + ) : ( +
+ Fall endgültig löschen? + + +
+ )} +
+ )} + + {deleteMutation.isError && ( + + Fehler beim Löschen des Falls. + + )} + {error && ( {error}