mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 14:53:41 +00:00
feat: add reactivate and delete buttons for admin disclosure view
After revoking or expiry, admins now see "Wieder aufleben" (reactivate with new 24h window) and "Verwerfen" (hard delete) buttons. Rejected disclosures also show "Verwerfen". Backend: PUT .../reactivate and DELETE endpoints for admin disclosures. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
547bfa3ea5
commit
78ae11fb99
5 changed files with 159 additions and 3 deletions
|
|
@ -19,7 +19,8 @@ from app.schemas.disclosure import (
|
|||
from app.schemas.user import UserCreate, UserResponse, UserUpdate
|
||||
from app.services.audit_service import log_action
|
||||
from app.services.disclosure_service import (
|
||||
get_pending_count, review_disclosure_request, revoke_disclosure,
|
||||
admin_delete_disclosure, get_pending_count, reactivate_disclosure,
|
||||
review_disclosure_request, revoke_disclosure,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
|
|
@ -411,3 +412,55 @@ def admin_revoke_disclosure(
|
|||
reviewed_by=dr.reviewed_by, reviewed_at=dr.reviewed_at,
|
||||
expires_at=dr.expires_at, created_at=dr.created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.put("/disclosure-requests/{request_id}/reactivate", response_model=DisclosureRequestResponse)
|
||||
def admin_reactivate_disclosure(
|
||||
request_id: int,
|
||||
request: Request,
|
||||
admin: User = Depends(require_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Admin: reactivate an expired/revoked disclosure (new 24h window)."""
|
||||
try:
|
||||
dr = reactivate_disclosure(db, request_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
log_action(
|
||||
db, user_id=admin.id, action="disclosure_reactivated",
|
||||
entity_type="disclosure_request", entity_id=request_id,
|
||||
new_values={"expires_at": dr.expires_at.isoformat() if dr.expires_at else None},
|
||||
ip_address=request.client.host if request.client else None,
|
||||
user_agent=request.headers.get("user-agent"),
|
||||
)
|
||||
|
||||
return DisclosureRequestResponse(
|
||||
id=dr.id, case_id=dr.case_id, requester_id=dr.requester_id,
|
||||
requester_username=dr.requester.username if dr.requester else None,
|
||||
fall_id=dr.case.fall_id if dr.case else None,
|
||||
reason=dr.reason, status=dr.status,
|
||||
reviewed_by=dr.reviewed_by, reviewed_at=dr.reviewed_at,
|
||||
expires_at=dr.expires_at, created_at=dr.created_at,
|
||||
)
|
||||
|
||||
|
||||
@router.delete("/disclosure-requests/{request_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def admin_delete_disclosure_request(
|
||||
request_id: int,
|
||||
request: Request,
|
||||
admin: User = Depends(require_admin),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""Admin: permanently delete a non-active disclosure request."""
|
||||
try:
|
||||
admin_delete_disclosure(db, request_id)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
|
||||
|
||||
log_action(
|
||||
db, user_id=admin.id, action="disclosure_admin_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"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -140,6 +140,34 @@ def revoke_disclosure(db: Session, request_id: int, user_id: int, *, admin: bool
|
|||
return dr
|
||||
|
||||
|
||||
def reactivate_disclosure(db: Session, request_id: int) -> DisclosureRequest:
|
||||
"""Reactivate an expired/revoked disclosure by granting a new 24h window."""
|
||||
dr = db.query(DisclosureRequest).filter(DisclosureRequest.id == request_id).first()
|
||||
if not dr:
|
||||
raise ValueError("Disclosure request not found")
|
||||
if dr.status != "approved":
|
||||
raise ValueError("Only approved disclosures can be reactivated")
|
||||
if dr.expires_at and dr.expires_at > _utcnow_naive():
|
||||
raise ValueError("Disclosure is still active")
|
||||
dr.expires_at = _utcnow_naive() + timedelta(hours=24)
|
||||
db.commit()
|
||||
db.refresh(dr)
|
||||
return dr
|
||||
|
||||
|
||||
def admin_delete_disclosure(db: Session, request_id: int) -> None:
|
||||
"""Admin: permanently delete any non-active disclosure request."""
|
||||
dr = db.query(DisclosureRequest).filter(DisclosureRequest.id == request_id).first()
|
||||
if not dr:
|
||||
raise ValueError("Disclosure request not found")
|
||||
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 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()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
getDisclosureRequests, getMyDisclosureRequests,
|
||||
reviewDisclosure, revokeDisclosure, adminRevokeDisclosure,
|
||||
deleteDisclosure, requestDisclosure,
|
||||
adminDeleteDisclosure, adminReactivateDisclosure,
|
||||
} from '@/services/disclosureService'
|
||||
|
||||
export function useDisclosures(status?: string) {
|
||||
|
|
@ -74,3 +75,24 @@ export function useRequestDisclosure() {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useAdminDeleteDisclosure() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => adminDeleteDisclosure(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['disclosures'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useAdminReactivateDisclosure() {
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation({
|
||||
mutationFn: (id: number) => adminReactivateDisclosure(id),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['disclosures'] })
|
||||
queryClient.invalidateQueries({ queryKey: ['notifications'] })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from 'react'
|
||||
import { Check, X, ShieldOff } from 'lucide-react'
|
||||
import { Check, X, ShieldOff, RotateCcw, Trash2 } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
|
|
@ -7,7 +7,10 @@ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|||
import {
|
||||
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { useDisclosures, useReviewDisclosure, useAdminRevokeDisclosure } from '@/hooks/useDisclosures'
|
||||
import {
|
||||
useDisclosures, useReviewDisclosure, useAdminRevokeDisclosure,
|
||||
useAdminDeleteDisclosure, useAdminReactivateDisclosure,
|
||||
} from '@/hooks/useDisclosures'
|
||||
import type { DisclosureRequest } from '@/types'
|
||||
|
||||
type StatusFilter = 'all' | 'pending' | 'approved' | 'rejected'
|
||||
|
|
@ -32,12 +35,20 @@ function isActiveApproved(dr: DisclosureRequest): boolean {
|
|||
return new Date(dr.expires_at) > new Date()
|
||||
}
|
||||
|
||||
function isExpiredApproved(dr: DisclosureRequest): boolean {
|
||||
if (dr.status !== 'approved') return false
|
||||
if (!dr.expires_at) return false
|
||||
return new Date(dr.expires_at) <= new Date()
|
||||
}
|
||||
|
||||
export function DisclosuresPage() {
|
||||
const [statusFilter, setStatusFilter] = useState<StatusFilter>('all')
|
||||
const queryStatus = statusFilter === 'all' ? undefined : statusFilter
|
||||
const { data: requests = [], isLoading } = useDisclosures(queryStatus)
|
||||
const reviewMutation = useReviewDisclosure()
|
||||
const revokeMutation = useAdminRevokeDisclosure()
|
||||
const deleteMutation = useAdminDeleteDisclosure()
|
||||
const reactivateMutation = useAdminReactivateDisclosure()
|
||||
|
||||
const handleReview = (id: number, status: 'approved' | 'rejected') => {
|
||||
reviewMutation.mutate({ id, status })
|
||||
|
|
@ -126,6 +137,39 @@ export function DisclosuresPage() {
|
|||
Entziehen
|
||||
</Button>
|
||||
)}
|
||||
{isExpiredApproved(dr) && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
disabled={reactivateMutation.isPending}
|
||||
onClick={() => reactivateMutation.mutate(dr.id)}
|
||||
>
|
||||
<RotateCcw className="size-4 mr-1" />
|
||||
Wieder aufleben
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={() => deleteMutation.mutate(dr.id)}
|
||||
>
|
||||
<Trash2 className="size-4 mr-1" />
|
||||
Verwerfen
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{dr.status === 'rejected' && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
disabled={deleteMutation.isPending}
|
||||
onClick={() => deleteMutation.mutate(dr.id)}
|
||||
>
|
||||
<Trash2 className="size-4 mr-1" />
|
||||
Verwerfen
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
|
|||
|
|
@ -40,3 +40,12 @@ export async function adminRevokeDisclosure(requestId: number): Promise<Disclosu
|
|||
export async function deleteDisclosure(requestId: number): Promise<void> {
|
||||
await api.delete(`/cases/disclosure-requests/${requestId}`)
|
||||
}
|
||||
|
||||
export async function adminDeleteDisclosure(requestId: number): Promise<void> {
|
||||
await api.delete(`/admin/disclosure-requests/${requestId}`)
|
||||
}
|
||||
|
||||
export async function adminReactivateDisclosure(requestId: number): Promise<DisclosureRequest> {
|
||||
const res = await api.put<DisclosureRequest>(`/admin/disclosure-requests/${requestId}/reactivate`)
|
||||
return res.data
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue