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 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-27 08:11:08 +00:00
parent 1bedbcf243
commit 547bfa3ea5
5 changed files with 168 additions and 14 deletions

View file

@ -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
# ---------------------------------------------------------------------------

View file

@ -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 (

View file

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

View file

@ -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<DisclosureRequest | null>(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 (
<div className="p-6 space-y-4">
@ -71,17 +103,41 @@ export function MyDisclosuresPage() {
: '—'}
</TableCell>
<TableCell className="text-right">
{isActive(dr) && (
<Button
size="sm"
variant="outline"
disabled={revokeMutation.isPending}
onClick={() => revokeMutation.mutate(dr.id)}
>
<ShieldOff className="size-4 mr-1" />
Als erledigt markieren
</Button>
)}
<div className="flex justify-end gap-1">
{isActive(dr) && (
<Button
size="sm"
variant="outline"
disabled={revokeMutation.isPending}
onClick={() => revokeMutation.mutate(dr.id)}
>
<ShieldOff className="size-4 mr-1" />
Als erledigt markieren
</Button>
)}
{isInactive(dr) && (
<>
<Button
size="sm"
variant="outline"
disabled={requestMutation.isPending}
onClick={() => openReRequestDialog(dr)}
>
<RefreshCw className="size-4 mr-1" />
Erneute Anfrage
</Button>
<Button
size="sm"
variant="ghost"
disabled={deleteMutation.isPending}
onClick={() => deleteMutation.mutate(dr.id)}
>
<Trash2 className="size-4 mr-1" />
Verwerfen
</Button>
</>
)}
</div>
</TableCell>
</TableRow>
))}
@ -89,6 +145,39 @@ export function MyDisclosuresPage() {
</Table>
</div>
)}
<Dialog open={!!reRequestTarget} onOpenChange={(open) => { if (!open) setReRequestTarget(null) }}>
<DialogContent>
<DialogHeader>
<DialogTitle>Erneute Freigabe-Anfrage</DialogTitle>
<DialogDescription>
Fall: {reRequestTarget?.fall_id || `#${reRequestTarget?.case_id}`}
</DialogDescription>
</DialogHeader>
<div className="space-y-2">
<label htmlFor="reason" className="text-sm font-medium">Begründung</label>
<textarea
id="reason"
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={3}
className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
placeholder="Begründung für die erneute Anfrage..."
/>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setReRequestTarget(null)}>
Abbrechen
</Button>
<Button
disabled={!reason.trim() || requestMutation.isPending}
onClick={submitReRequest}
>
Anfrage senden
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}

View file

@ -36,3 +36,7 @@ export async function adminRevokeDisclosure(requestId: number): Promise<Disclosu
const res = await api.put<DisclosureRequest>(`/admin/disclosure-requests/${requestId}/revoke`)
return res.data
}
export async function deleteDisclosure(requestId: number): Promise<void> {
await api.delete(`/cases/disclosure-requests/${requestId}`)
}