mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 14:53:41 +00:00
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:
parent
3afe03b0a9
commit
143e65e3d9
3 changed files with 116 additions and 3 deletions
|
|
@ -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
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
||||
{selectedCase && (
|
||||
<CaseDetail caseData={selectedCase} />
|
||||
<CaseDetail
|
||||
caseData={selectedCase}
|
||||
onDeleted={() => { setSheetOpen(false); setSelectedCase(null) }}
|
||||
/>
|
||||
)}
|
||||
</SheetContent>
|
||||
</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 {
|
||||
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 (
|
||||
<>
|
||||
<SheetHeader>
|
||||
|
|
@ -654,6 +671,34 @@ function CaseDetail({ caseData }: { caseData: Case }) {
|
|||
)}
|
||||
</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 && (
|
||||
<Alert variant="destructive">
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
|
|
|
|||
Loading…
Reference in a new issue