mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 17:13:42 +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.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
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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'
|
} 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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue