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) <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-03-16 16:22:31 +00:00
parent 3afe03b0a9
commit 143e65e3d9
3 changed files with 116 additions and 3 deletions

View file

@ -14,6 +14,7 @@ from sqlalchemy.orm import Session
from app.core.dependencies import get_current_user, require_admin from app.core.dependencies import get_current_user, require_admin
from app.database import get_db from app.database import get_db
from app.models.audit import DisclosureRequest
from app.models.case import Case from app.models.case import Case
from app.models.user import User from app.models.user import User
from app.schemas.case import ( from app.schemas.case import (
@ -554,6 +555,61 @@ def update_case(
return 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 # ICD entry
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -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'] })
},
})
}

View file

@ -5,7 +5,7 @@ import {
} from 'lucide-react' } from 'lucide-react'
import type { Case } from '@/types' import type { Case } from '@/types'
import { useAuth } from '@/context/AuthContext' 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 api from '@/services/api'
import { useFilterPresets, useCreateFilterPreset, useDeleteFilterPreset } from '@/hooks/useFilterPresets' import { useFilterPresets, useCreateFilterPreset, useDeleteFilterPreset } from '@/hooks/useFilterPresets'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@ -472,7 +472,10 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}> <Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetContent className="w-full sm:max-w-lg overflow-y-auto"> <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
{selectedCase && ( {selectedCase && (
<CaseDetail caseData={selectedCase} /> <CaseDetail
caseData={selectedCase}
onDeleted={() => { setSheetOpen(false); setSelectedCase(null) }}
/>
)} )}
</SheetContent> </SheetContent>
</Sheet> </Sheet>
@ -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 { isAdmin } = useAuth()
const { const {
editing, formValues, saving, error, success, editing, formValues, saving, error, success,
@ -603,10 +606,15 @@ function CaseDetail({ caseData }: { caseData: Case }) {
const [icdError, setIcdError] = useState('') const [icdError, setIcdError] = useState('')
const [icdSuccess, setIcdSuccess] = useState(false) const [icdSuccess, setIcdSuccess] = useState(false)
// Delete state
const deleteMutation = useCaseDelete()
const [confirmDelete, setConfirmDelete] = useState(false)
useEffect(() => { useEffect(() => {
setIcdValue(caseData.icd || '') setIcdValue(caseData.icd || '')
setIcdError('') setIcdError('')
setIcdSuccess(false) setIcdSuccess(false)
setConfirmDelete(false)
}, [caseData.id, caseData.icd]) }, [caseData.id, caseData.icd])
const saveIcd = async () => { 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 ( return (
<> <>
<SheetHeader> <SheetHeader>
@ -654,6 +671,34 @@ function CaseDetail({ caseData }: { caseData: Case }) {
)} )}
</div> </div>
{/* Delete button (admin only) */}
{isAdmin && !editing && (
<div className="flex items-center gap-2">
{!confirmDelete ? (
<Button size="sm" variant="outline" className="text-destructive hover:text-destructive" onClick={() => setConfirmDelete(true)}>
<Trash2 className="size-4 mr-1" />
Löschen
</Button>
) : (
<div className="flex items-center gap-2 p-2 border border-destructive/30 rounded-lg bg-destructive/5">
<span className="text-sm text-destructive">Fall endgültig löschen?</span>
<Button size="sm" variant="destructive" onClick={handleDelete} disabled={deleteMutation.isPending}>
{deleteMutation.isPending ? 'Löschen...' : 'Ja, löschen'}
</Button>
<Button size="sm" variant="outline" onClick={() => setConfirmDelete(false)} disabled={deleteMutation.isPending}>
Abbrechen
</Button>
</div>
)}
</div>
)}
{deleteMutation.isError && (
<Alert variant="destructive">
<AlertDescription>Fehler beim Löschen des Falls.</AlertDescription>
</Alert>
)}
{error && ( {error && (
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription>{error}</AlertDescription> <AlertDescription>{error}</AlertDescription>