mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 16:03:41 +00:00
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:
parent
1bedbcf243
commit
547bfa3ea5
5 changed files with 168 additions and 14 deletions
|
|
@ -26,7 +26,8 @@ from app.config import get_settings
|
||||||
from app.services.audit_service import log_action
|
from app.services.audit_service import log_action
|
||||||
from app.services.import_service import has_random_suffix
|
from app.services.import_service import has_random_suffix
|
||||||
from app.services.disclosure_service import (
|
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,
|
has_pending_request, revoke_disclosure,
|
||||||
)
|
)
|
||||||
from app.services.icd_service import generate_coding_template, get_pending_icd_cases, save_icd_for_case
|
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
|
# Paginated case list
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -140,6 +140,22 @@ def revoke_disclosure(db: Session, request_id: int, user_id: int, *, admin: bool
|
||||||
return dr
|
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]:
|
def get_my_disclosure_requests(db: Session, user_id: int) -> list[DisclosureRequest]:
|
||||||
"""Return all disclosure requests made by a specific user."""
|
"""Return all disclosure requests made by a specific user."""
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import {
|
import {
|
||||||
getDisclosureRequests, getMyDisclosureRequests,
|
getDisclosureRequests, getMyDisclosureRequests,
|
||||||
reviewDisclosure, revokeDisclosure, adminRevokeDisclosure,
|
reviewDisclosure, revokeDisclosure, adminRevokeDisclosure,
|
||||||
|
deleteDisclosure, requestDisclosure,
|
||||||
} from '@/services/disclosureService'
|
} from '@/services/disclosureService'
|
||||||
|
|
||||||
export function useDisclosures(status?: string) {
|
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'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { Button } from '@/components/ui/button'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import {
|
import {
|
||||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||||
} from '@/components/ui/table'
|
} 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'
|
import type { DisclosureRequest } from '@/types'
|
||||||
|
|
||||||
function statusBadge(dr: DisclosureRequest) {
|
function statusBadge(dr: DisclosureRequest) {
|
||||||
|
|
@ -29,9 +37,33 @@ function isActive(dr: DisclosureRequest): boolean {
|
||||||
return new Date(dr.expires_at) > new Date()
|
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() {
|
export function MyDisclosuresPage() {
|
||||||
const { data: requests = [], isLoading } = useMyDisclosures()
|
const { data: requests = [], isLoading } = useMyDisclosures()
|
||||||
const revokeMutation = useRevokeDisclosure()
|
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 (
|
return (
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
|
|
@ -71,17 +103,41 @@ export function MyDisclosuresPage() {
|
||||||
: '—'}
|
: '—'}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
{isActive(dr) && (
|
<div className="flex justify-end gap-1">
|
||||||
<Button
|
{isActive(dr) && (
|
||||||
size="sm"
|
<Button
|
||||||
variant="outline"
|
size="sm"
|
||||||
disabled={revokeMutation.isPending}
|
variant="outline"
|
||||||
onClick={() => revokeMutation.mutate(dr.id)}
|
disabled={revokeMutation.isPending}
|
||||||
>
|
onClick={() => revokeMutation.mutate(dr.id)}
|
||||||
<ShieldOff className="size-4 mr-1" />
|
>
|
||||||
Als erledigt markieren
|
<ShieldOff className="size-4 mr-1" />
|
||||||
</Button>
|
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>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
@ -89,6 +145,39 @@ export function MyDisclosuresPage() {
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,3 +36,7 @@ export async function adminRevokeDisclosure(requestId: number): Promise<Disclosu
|
||||||
const res = await api.put<DisclosureRequest>(`/admin/disclosure-requests/${requestId}/revoke`)
|
const res = await api.put<DisclosureRequest>(`/admin/disclosure-requests/${requestId}/revoke`)
|
||||||
return res.data
|
return res.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteDisclosure(requestId: number): Promise<void> {
|
||||||
|
await api.delete(`/cases/disclosure-requests/${requestId}`)
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue