From 547bfa3ea524bc997d7d9ceeb9397720c8e134f1 Mon Sep 17 00:00:00 2001 From: CCS Admin Date: Fri, 27 Feb 2026 08:11:08 +0000 Subject: [PATCH] feat: add re-request and delete buttons for inactive disclosures DAK employees now see "Erneute Anfrage" (opens dialog with pre-filled reason) and "Verwerfen" (hard delete) buttons for expired, revoked, or rejected disclosure requests on their My Disclosures page. Backend: new DELETE /cases/disclosure-requests/{id} endpoint. Frontend: new hooks useDeleteDisclosure, useRequestDisclosure. Co-Authored-By: Claude Opus 4.6 --- backend/app/api/cases.py | 24 ++++- backend/app/services/disclosure_service.py | 16 +++ frontend/src/hooks/useDisclosures.ts | 23 +++++ frontend/src/pages/MyDisclosuresPage.tsx | 115 ++++++++++++++++++--- frontend/src/services/disclosureService.ts | 4 + 5 files changed, 168 insertions(+), 14 deletions(-) diff --git a/backend/app/api/cases.py b/backend/app/api/cases.py index 6c96bac..7e9e2d4 100644 --- a/backend/app/api/cases.py +++ b/backend/app/api/cases.py @@ -26,7 +26,8 @@ from app.config import get_settings from app.services.audit_service import log_action from app.services.import_service import has_random_suffix from app.services.disclosure_service import ( - create_disclosure_request, get_my_disclosure_requests, has_active_disclosure, + create_disclosure_request, delete_disclosure_request, + get_my_disclosure_requests, has_active_disclosure, has_pending_request, revoke_disclosure, ) from app.services.icd_service import generate_coding_template, get_pending_icd_cases, save_icd_for_case @@ -262,6 +263,27 @@ def revoke_my_disclosure( ) +@router.delete("/disclosure-requests/{request_id}", status_code=status.HTTP_204_NO_CONTENT) +def delete_my_disclosure( + request_id: int, + request: Request, + db: Session = Depends(get_db), + user: User = Depends(get_current_user), +): + """Delete own inactive disclosure request (rejected, expired, revoked).""" + try: + delete_disclosure_request(db, request_id, user.id) + except ValueError as e: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e)) + + log_action( + db, user_id=user.id, action="disclosure_deleted", + entity_type="disclosure_request", entity_id=request_id, + ip_address=request.client.host if request.client else None, + user_agent=request.headers.get("user-agent"), + ) + + # --------------------------------------------------------------------------- # Paginated case list # --------------------------------------------------------------------------- diff --git a/backend/app/services/disclosure_service.py b/backend/app/services/disclosure_service.py index b2866f2..58a593d 100644 --- a/backend/app/services/disclosure_service.py +++ b/backend/app/services/disclosure_service.py @@ -140,6 +140,22 @@ def revoke_disclosure(db: Session, request_id: int, user_id: int, *, admin: bool return dr +def delete_disclosure_request(db: Session, request_id: int, user_id: int) -> None: + """Permanently delete a non-active disclosure request owned by the user.""" + dr = db.query(DisclosureRequest).filter(DisclosureRequest.id == request_id).first() + if not dr: + raise ValueError("Disclosure request not found") + if dr.requester_id != user_id: + raise ValueError("Not authorized to delete this disclosure") + # Only allow deletion of inactive requests (rejected, expired, revoked) + if dr.status == "pending": + raise ValueError("Pending requests cannot be deleted") + if dr.status == "approved" and dr.expires_at and dr.expires_at > _utcnow_naive(): + raise ValueError("Active disclosures cannot be deleted") + db.delete(dr) + db.commit() + + def get_my_disclosure_requests(db: Session, user_id: int) -> list[DisclosureRequest]: """Return all disclosure requests made by a specific user.""" return ( diff --git a/frontend/src/hooks/useDisclosures.ts b/frontend/src/hooks/useDisclosures.ts index dc70e56..8733a8f 100644 --- a/frontend/src/hooks/useDisclosures.ts +++ b/frontend/src/hooks/useDisclosures.ts @@ -2,6 +2,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { getDisclosureRequests, getMyDisclosureRequests, reviewDisclosure, revokeDisclosure, adminRevokeDisclosure, + deleteDisclosure, requestDisclosure, } from '@/services/disclosureService' export function useDisclosures(status?: string) { @@ -51,3 +52,25 @@ export function useAdminRevokeDisclosure() { }, }) } + +export function useDeleteDisclosure() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: (id: number) => deleteDisclosure(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['my-disclosures'] }) + }, + }) +} + +export function useRequestDisclosure() { + const queryClient = useQueryClient() + return useMutation({ + mutationFn: ({ caseId, reason }: { caseId: number; reason: string }) => + requestDisclosure(caseId, reason), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['my-disclosures'] }) + queryClient.invalidateQueries({ queryKey: ['notifications'] }) + }, + }) +} diff --git a/frontend/src/pages/MyDisclosuresPage.tsx b/frontend/src/pages/MyDisclosuresPage.tsx index a776055..3569ed4 100644 --- a/frontend/src/pages/MyDisclosuresPage.tsx +++ b/frontend/src/pages/MyDisclosuresPage.tsx @@ -1,11 +1,19 @@ -import { ShieldOff } from 'lucide-react' +import { useState } from 'react' +import { ShieldOff, RefreshCw, Trash2 } from 'lucide-react' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { Skeleton } from '@/components/ui/skeleton' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table' -import { useMyDisclosures, useRevokeDisclosure } from '@/hooks/useDisclosures' +import { + Dialog, DialogContent, DialogDescription, DialogFooter, + DialogHeader, DialogTitle, +} from '@/components/ui/dialog' +import { + useMyDisclosures, useRevokeDisclosure, + useDeleteDisclosure, useRequestDisclosure, +} from '@/hooks/useDisclosures' import type { DisclosureRequest } from '@/types' function statusBadge(dr: DisclosureRequest) { @@ -29,9 +37,33 @@ function isActive(dr: DisclosureRequest): boolean { return new Date(dr.expires_at) > new Date() } +function isInactive(dr: DisclosureRequest): boolean { + if (dr.status === 'rejected') return true + if (dr.status === 'approved' && dr.expires_at && new Date(dr.expires_at) <= new Date()) return true + return false +} + export function MyDisclosuresPage() { const { data: requests = [], isLoading } = useMyDisclosures() const revokeMutation = useRevokeDisclosure() + const deleteMutation = useDeleteDisclosure() + const requestMutation = useRequestDisclosure() + + const [reRequestTarget, setReRequestTarget] = useState(null) + const [reason, setReason] = useState('') + + const openReRequestDialog = (dr: DisclosureRequest) => { + setReason(dr.reason) + setReRequestTarget(dr) + } + + const submitReRequest = () => { + if (!reRequestTarget || !reason.trim()) return + requestMutation.mutate( + { caseId: reRequestTarget.case_id, reason: reason.trim() }, + { onSuccess: () => setReRequestTarget(null) }, + ) + } return (
@@ -71,17 +103,41 @@ export function MyDisclosuresPage() { : '—'} - {isActive(dr) && ( - - )} +
+ {isActive(dr) && ( + + )} + {isInactive(dr) && ( + <> + + + + )} +
))} @@ -89,6 +145,39 @@ export function MyDisclosuresPage() {
)} + + { if (!open) setReRequestTarget(null) }}> + + + Erneute Freigabe-Anfrage + + Fall: {reRequestTarget?.fall_id || `#${reRequestTarget?.case_id}`} + + +
+ +