feat: add KPI links, My Disclosures page, and extend Admin Disclosures

- Dashboard KPI cards are now clickable with role-based links
- New GutachtenStatistikPage placeholder at /gutachten-statistik
- New "Meine Freigaben" page for DAK-Mitarbeiter to view/revoke own disclosures
- Backend: GET /cases/my-disclosure-requests, PUT /cases/disclosure-requests/{id}/revoke
- Admin Disclosures page: full history with status tabs and revoke capability
- Backend: PUT /admin/disclosure-requests/{id}/revoke, default shows all statuses
- Sidebar: "Meine Freigaben" entry for dak_mitarbeiter role

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-26 22:19:46 +00:00
parent 4504d4300f
commit 32cee4d30d
13 changed files with 435 additions and 76 deletions

View file

@ -19,7 +19,7 @@ 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,
get_pending_count, review_disclosure_request, revoke_disclosure,
)
router = APIRouter()
@ -316,12 +316,10 @@ def list_disclosure_requests(
admin: User = Depends(require_admin),
db: Session = Depends(get_db),
):
"""List disclosure requests (default: pending only)."""
"""List disclosure requests. Without status filter, returns all."""
query = db.query(DisclosureRequest)
if status_filter:
query = query.filter(DisclosureRequest.status == status_filter)
else:
query = query.filter(DisclosureRequest.status == "pending")
requests = query.order_by(DisclosureRequest.created_at.desc()).all()
@ -382,3 +380,34 @@ def review_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}/revoke", response_model=DisclosureRequestResponse)
def admin_revoke_disclosure(
request_id: int,
request: Request,
admin: User = Depends(require_admin),
db: Session = Depends(get_db),
):
"""Admin: revoke an active disclosure (sets expires_at to now)."""
try:
dr = revoke_disclosure(db, request_id, admin.id, admin=True)
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_revoked",
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,
)

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, has_active_disclosure, has_pending_request,
create_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
@ -205,6 +206,62 @@ def request_disclosure(
)
# ---------------------------------------------------------------------------
# My disclosure requests (for dak_mitarbeiter)
# ---------------------------------------------------------------------------
@router.get("/my-disclosure-requests", response_model=list[DisclosureRequestResponse])
def list_my_disclosure_requests(
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
"""Return all disclosure requests made by the current user."""
requests = get_my_disclosure_requests(db, user.id)
result = []
for dr in requests:
result.append(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,
))
return result
@router.put("/disclosure-requests/{request_id}/revoke", response_model=DisclosureRequestResponse)
def revoke_my_disclosure(
request_id: int,
request: Request,
db: Session = Depends(get_db),
user: User = Depends(get_current_user),
):
"""Revoke own active disclosure (sets expires_at to now)."""
try:
dr = revoke_disclosure(db, request_id, user.id, admin=False)
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_self_revoked",
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,
)
# ---------------------------------------------------------------------------
# Paginated case list
# ---------------------------------------------------------------------------

View file

@ -113,6 +113,38 @@ def review_disclosure_request(
return dr
def revoke_disclosure(db: Session, request_id: int, user_id: int, *, admin: bool = False) -> DisclosureRequest:
"""Revoke an active disclosure by setting expires_at to now.
If admin=False, only the original requester can revoke their own disclosure.
If admin=True, any admin can revoke any disclosure.
"""
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 revoked")
if dr.expires_at and dr.expires_at <= datetime.now(timezone.utc):
raise ValueError("Disclosure already expired")
if not admin and dr.requester_id != user_id:
raise ValueError("Not authorized to revoke this disclosure")
dr.expires_at = datetime.now(timezone.utc)
db.commit()
db.refresh(dr)
return dr
def get_my_disclosure_requests(db: Session, user_id: int) -> list[DisclosureRequest]:
"""Return all disclosure requests made by a specific user."""
return (
db.query(DisclosureRequest)
.filter(DisclosureRequest.requester_id == user_id)
.order_by(DisclosureRequest.created_at.desc())
.all()
)
def get_pending_requests(db: Session) -> list[DisclosureRequest]:
"""Return all pending disclosure requests."""
return (

View file

@ -16,6 +16,8 @@ import { AdminInvitationsPage } from '@/pages/AdminInvitationsPage'
import { AdminAuditPage } from '@/pages/AdminAuditPage'
import { DisclosuresPage } from '@/pages/DisclosuresPage'
import { AccountPage } from '@/pages/AccountPage'
import { GutachtenStatistikPage } from '@/pages/GutachtenStatistikPage'
import { MyDisclosuresPage } from '@/pages/MyDisclosuresPage'
const queryClient = new QueryClient({
defaultOptions: {
@ -43,7 +45,9 @@ function App() {
<Route path="icd" element={<IcdPage />} />
<Route path="coding" element={<ProtectedRoute requireAdmin><CodingPage /></ProtectedRoute>} />
<Route path="reports" element={<ReportsPage />} />
<Route path="gutachten-statistik" element={<GutachtenStatistikPage />} />
<Route path="account" element={<AccountPage />} />
<Route path="disclosures" element={<MyDisclosuresPage />} />
<Route path="admin/users" element={<ProtectedRoute requireAdmin><AdminUsersPage /></ProtectedRoute>} />
<Route path="admin/invitations" element={<ProtectedRoute requireAdmin><AdminInvitationsPage /></ProtectedRoute>} />
<Route path="admin/audit" element={<ProtectedRoute requireAdmin><AdminAuditPage /></ProtectedRoute>} />

View file

@ -36,6 +36,7 @@ const mainNavItems: NavItem[] = [
const accountNavItems: NavItem[] = [
{ label: 'Kontoverwaltung', to: '/account', icon: UserCog },
{ label: 'Meine Freigaben', to: '/disclosures', icon: ShieldCheck, mitarbeiterOnly: true },
]
const adminNavItems: NavItem[] = [
@ -68,11 +69,15 @@ function NavItemLink({ item }: { item: NavItem }) {
export function Sidebar({ className }: { className?: string }) {
const { user, isAdmin } = useAuth()
const visibleMainItems = mainNavItems.filter((item) => {
if (item.adminOnly && !isAdmin) return false
if (item.mitarbeiterOnly && user?.role !== 'dak_mitarbeiter') return false
return true
})
const filterItems = (items: NavItem[]) =>
items.filter((item) => {
if (item.adminOnly && !isAdmin) return false
if (item.mitarbeiterOnly && user?.role !== 'dak_mitarbeiter') return false
return true
})
const visibleMainItems = filterItems(mainNavItems)
const visibleAccountItems = filterItems(accountNavItems)
return (
<aside className={cn('flex h-full flex-col gap-4 p-4', className)}>
@ -88,7 +93,7 @@ export function Sidebar({ className }: { className?: string }) {
<Separator />
<nav className="flex flex-col gap-1">
{accountNavItems.map((item) => (
{visibleAccountItems.map((item) => (
<NavItemLink key={item.to} item={item} />
))}
</nav>

View file

@ -50,7 +50,7 @@ describe('useDisclosures', () => {
expect(result.current.data).toHaveLength(0)
})
it('works without status parameter (uses default "pending")', async () => {
it('works without status parameter (returns all)', async () => {
let capturedStatus: string | null = null
server.use(
@ -65,7 +65,7 @@ describe('useDisclosures', () => {
await waitFor(() => expect(result.current.isSuccess).toBe(true))
expect(capturedStatus).toBe('pending')
expect(capturedStatus).toBeNull()
expect(result.current.data).toHaveLength(1)
})
})

View file

@ -1,13 +1,23 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getDisclosureRequests, reviewDisclosure } from '@/services/disclosureService'
import {
getDisclosureRequests, getMyDisclosureRequests,
reviewDisclosure, revokeDisclosure, adminRevokeDisclosure,
} from '@/services/disclosureService'
export function useDisclosures(status: string = 'pending') {
export function useDisclosures(status?: string) {
return useQuery({
queryKey: ['disclosures', status],
queryKey: ['disclosures', status ?? 'all'],
queryFn: () => getDisclosureRequests(status),
})
}
export function useMyDisclosures() {
return useQuery({
queryKey: ['my-disclosures'],
queryFn: getMyDisclosureRequests,
})
}
export function useReviewDisclosure() {
const queryClient = useQueryClient()
return useMutation({
@ -19,3 +29,25 @@ export function useReviewDisclosure() {
},
})
}
export function useRevokeDisclosure() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: number) => revokeDisclosure(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['my-disclosures'] })
queryClient.invalidateQueries({ queryKey: ['cases'] })
},
})
}
export function useAdminRevokeDisclosure() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (id: number) => adminRevokeDisclosure(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['disclosures'] })
queryClient.invalidateQueries({ queryKey: ['notifications'] })
},
})
}

View file

@ -1,9 +1,11 @@
import { useState } from 'react'
import { Link } from 'react-router-dom'
import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
PieChart, Pie, Cell, ResponsiveContainer,
} from 'recharts'
import { FileText, Clock, Code, Stethoscope } from 'lucide-react'
import { useAuth } from '@/context/AuthContext'
import { useDashboard } from '@/hooks/useDashboard'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import {
@ -31,6 +33,7 @@ export function DashboardPage() {
const currentYear = new Date().getFullYear()
const [jahr, setJahr] = useState(currentYear)
const { data, isLoading: loading } = useDashboard(jahr)
const { isAdmin } = useAuth()
const fallgruppenData = data
? Object.entries(data.kpis.fallgruppen)
@ -80,21 +83,25 @@ export function DashboardPage() {
title="Fälle gesamt"
value={data.kpis.total_cases}
icon={<FileText className="size-5 text-muted-foreground" />}
href="/cases"
/>
<KpiCard
title="Offene ICD"
value={data.kpis.pending_icd}
icon={<Clock className="size-5 text-muted-foreground" />}
href="/icd"
/>
<KpiCard
title="Offene Codierung"
value={data.kpis.pending_coding}
icon={<Code className="size-5 text-muted-foreground" />}
href={isAdmin ? '/coding' : undefined}
/>
<KpiCard
title="Gutachten gesamt"
value={data.kpis.total_gutachten}
icon={<Stethoscope className="size-5 text-muted-foreground" />}
href="/gutachten-statistik"
/>
</div>
) : (
@ -205,9 +212,9 @@ export function DashboardPage() {
)
}
function KpiCard({ title, value, icon }: { title: string; value: number; icon: React.ReactNode }) {
return (
<Card>
function KpiCard({ title, value, icon, href }: { title: string; value: number; icon: React.ReactNode; href?: string }) {
const card = (
<Card className={href ? 'transition-colors hover:border-primary/50 hover:shadow-md' : undefined}>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
{icon}
@ -217,4 +224,9 @@ function KpiCard({ title, value, icon }: { title: string; value: number; icon: R
</CardContent>
</Card>
)
if (href) {
return <Link to={href} className="block">{card}</Link>
}
return card
}

View file

@ -1,13 +1,43 @@
import { Check, X } from 'lucide-react'
import { useState } from 'react'
import { Check, X, ShieldOff } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { useDisclosures, useReviewDisclosure } from '@/hooks/useDisclosures'
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 type { DisclosureRequest } from '@/types'
type StatusFilter = 'all' | 'pending' | 'approved' | 'rejected'
function statusBadge(dr: DisclosureRequest) {
const now = new Date()
if (dr.status === 'pending') {
return <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-300">Ausstehend</Badge>
}
if (dr.status === 'rejected') {
return <Badge variant="outline" className="bg-red-50 text-red-700 border-red-300">Abgelehnt</Badge>
}
if (dr.expires_at && new Date(dr.expires_at) <= now) {
return <Badge variant="outline" className="bg-gray-50 text-gray-500 border-gray-300">Abgelaufen</Badge>
}
return <Badge variant="outline" className="bg-green-50 text-green-700 border-green-300">Genehmigt</Badge>
}
function isActiveApproved(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 { data: requests = [], isLoading } = useDisclosures('pending')
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 handleReview = (id: number, status: 'approved' | 'rejected') => {
reviewMutation.mutate({ id, status })
@ -17,60 +47,91 @@ export function DisclosuresPage() {
<div className="p-6 space-y-4">
<h1 className="text-2xl font-bold">Freigabe-Anfragen</h1>
<Tabs value={statusFilter} onValueChange={(v) => setStatusFilter(v as StatusFilter)}>
<TabsList>
<TabsTrigger value="all">Alle</TabsTrigger>
<TabsTrigger value="pending">Ausstehend</TabsTrigger>
<TabsTrigger value="approved">Genehmigt</TabsTrigger>
<TabsTrigger value="rejected">Abgelehnt</TabsTrigger>
</TabsList>
</Tabs>
{isLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-24 w-full" />
<Skeleton key={i} className="h-12 w-full" />
))}
</div>
) : requests.length === 0 ? (
<p className="text-muted-foreground">Keine offenen Anfragen.</p>
<p className="text-muted-foreground">Keine Anfragen gefunden.</p>
) : (
<div className="space-y-3">
{requests.map((dr) => (
<Card key={dr.id}>
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<CardTitle className="text-base">
Fall {dr.fall_id || dr.case_id}
</CardTitle>
<Badge variant="outline">{dr.status}</Badge>
</div>
</CardHeader>
<CardContent className="space-y-2">
<p className="text-sm">
<span className="text-muted-foreground">Angefragt von:</span>{' '}
{dr.requester_username || `User #${dr.requester_id}`}
</p>
<p className="text-sm">
<span className="text-muted-foreground">Begründung:</span>{' '}
{dr.reason}
</p>
<p className="text-xs text-muted-foreground">
{new Date(dr.created_at).toLocaleString('de-DE')}
</p>
<div className="flex gap-2 pt-1">
<Button
size="sm"
disabled={reviewMutation.isPending}
onClick={() => handleReview(dr.id, 'approved')}
>
<Check className="size-4 mr-1" />
Genehmigen
</Button>
<Button
size="sm"
variant="destructive"
disabled={reviewMutation.isPending}
onClick={() => handleReview(dr.id, 'rejected')}
>
<X className="size-4 mr-1" />
Ablehnen
</Button>
</div>
</CardContent>
</Card>
))}
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Fall-ID</TableHead>
<TableHead>Antragsteller</TableHead>
<TableHead>Begründung</TableHead>
<TableHead>Status</TableHead>
<TableHead>Datum</TableHead>
<TableHead>Gültig bis</TableHead>
<TableHead className="text-right">Aktionen</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{requests.map((dr) => (
<TableRow key={dr.id}>
<TableCell className="font-medium">{dr.fall_id || `#${dr.case_id}`}</TableCell>
<TableCell>{dr.requester_username || `User #${dr.requester_id}`}</TableCell>
<TableCell className="max-w-xs truncate">{dr.reason}</TableCell>
<TableCell>{statusBadge(dr)}</TableCell>
<TableCell>{new Date(dr.created_at).toLocaleDateString('de-DE')}</TableCell>
<TableCell>
{dr.expires_at
? new Date(dr.expires_at).toLocaleString('de-DE')
: '—'}
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-1">
{dr.status === 'pending' && (
<>
<Button
size="sm"
variant="outline"
disabled={reviewMutation.isPending}
onClick={() => handleReview(dr.id, 'approved')}
>
<Check className="size-4 mr-1" />
Genehmigen
</Button>
<Button
size="sm"
variant="destructive"
disabled={reviewMutation.isPending}
onClick={() => handleReview(dr.id, 'rejected')}
>
<X className="size-4 mr-1" />
Ablehnen
</Button>
</>
)}
{isActiveApproved(dr) && (
<Button
size="sm"
variant="outline"
disabled={revokeMutation.isPending}
onClick={() => revokeMutation.mutate(dr.id)}
>
<ShieldOff className="size-4 mr-1" />
Entziehen
</Button>
)}
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>

View file

@ -0,0 +1,17 @@
import { BarChart3 } from 'lucide-react'
import { Card, CardContent } from '@/components/ui/card'
export function GutachtenStatistikPage() {
return (
<div className="p-6 space-y-4">
<h1 className="text-2xl font-bold">Gutachten-Statistik</h1>
<Card>
<CardContent className="flex flex-col items-center justify-center py-16 text-muted-foreground">
<BarChart3 className="size-12 mb-4" />
<p className="text-lg font-medium">Kommt bald</p>
<p className="text-sm">Diese Seite wird in Kürze mit detaillierten Gutachten-Statistiken verfügbar sein.</p>
</CardContent>
</Card>
</div>
)
}

View file

@ -0,0 +1,94 @@
import { ShieldOff } 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 type { DisclosureRequest } from '@/types'
function statusBadge(dr: DisclosureRequest) {
const now = new Date()
if (dr.status === 'pending') {
return <Badge variant="outline" className="bg-yellow-50 text-yellow-700 border-yellow-300">Ausstehend</Badge>
}
if (dr.status === 'rejected') {
return <Badge variant="outline" className="bg-red-50 text-red-700 border-red-300">Abgelehnt</Badge>
}
// approved
if (dr.expires_at && new Date(dr.expires_at) <= now) {
return <Badge variant="outline" className="bg-gray-50 text-gray-500 border-gray-300">Abgelaufen</Badge>
}
return <Badge variant="outline" className="bg-green-50 text-green-700 border-green-300">Genehmigt</Badge>
}
function isActive(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 MyDisclosuresPage() {
const { data: requests = [], isLoading } = useMyDisclosures()
const revokeMutation = useRevokeDisclosure()
return (
<div className="p-6 space-y-4">
<h1 className="text-2xl font-bold">Meine Freigaben</h1>
{isLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</div>
) : requests.length === 0 ? (
<p className="text-muted-foreground">Sie haben noch keine Freigaben angefragt.</p>
) : (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Fall-ID</TableHead>
<TableHead>Begründung</TableHead>
<TableHead>Status</TableHead>
<TableHead>Angefragt am</TableHead>
<TableHead>Gültig bis</TableHead>
<TableHead className="text-right">Aktion</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{requests.map((dr) => (
<TableRow key={dr.id}>
<TableCell className="font-medium">{dr.fall_id || `#${dr.case_id}`}</TableCell>
<TableCell className="max-w-xs truncate">{dr.reason}</TableCell>
<TableCell>{statusBadge(dr)}</TableCell>
<TableCell>{new Date(dr.created_at).toLocaleDateString('de-DE')}</TableCell>
<TableCell>
{dr.expires_at
? new Date(dr.expires_at).toLocaleString('de-DE')
: '—'}
</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>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</div>
)
}

View file

@ -15,7 +15,7 @@ describe('DisclosuresPage', () => {
await waitFor(() => {
// The mock handler returns [mockDisclosureRequest] with fall_id '2026-06-onko-A123456789'
expect(screen.getByText('Fall 2026-06-onko-A123456789')).toBeInTheDocument()
expect(screen.getByText('2026-06-onko-A123456789')).toBeInTheDocument()
})
// Should show requester username
@ -24,8 +24,9 @@ describe('DisclosuresPage', () => {
// Should show the reason
expect(screen.getByText('Benötige vollständige Patientendaten für Rückruf.')).toBeInTheDocument()
// Should show the status badge
expect(screen.getByText('pending')).toBeInTheDocument()
// Should show the status badge (translated) — multiple "Ausstehend" exist (tab + badge)
const badges = screen.getAllByText('Ausstehend')
expect(badges.length).toBeGreaterThanOrEqual(1)
})
it('shows approve and reject buttons', async () => {
@ -48,7 +49,7 @@ describe('DisclosuresPage', () => {
renderWithProviders(<DisclosuresPage />)
await waitFor(() => {
expect(screen.getByText('Keine offenen Anfragen.')).toBeInTheDocument()
expect(screen.getByText('Keine Anfragen gefunden.')).toBeInTheDocument()
})
})
@ -66,8 +67,8 @@ describe('DisclosuresPage', () => {
// The heading is always visible
expect(screen.getByText('Freigabe-Anfragen')).toBeInTheDocument()
// During loading, the "Keine offenen Anfragen." message should not be visible yet
expect(screen.queryByText('Keine offenen Anfragen.')).not.toBeInTheDocument()
// During loading, the empty message should not be visible yet
expect(screen.queryByText('Keine Anfragen gefunden.')).not.toBeInTheDocument()
// Nor should any disclosure item be visible
expect(screen.queryByText('Genehmigen')).not.toBeInTheDocument()
})

View file

@ -21,3 +21,18 @@ export async function reviewDisclosure(requestId: number, status: 'approved' | '
const res = await api.put<DisclosureRequest>(`/admin/disclosure-requests/${requestId}`, { status })
return res.data
}
export async function getMyDisclosureRequests(): Promise<DisclosureRequest[]> {
const res = await api.get<DisclosureRequest[]>('/cases/my-disclosure-requests')
return res.data
}
export async function revokeDisclosure(requestId: number): Promise<DisclosureRequest> {
const res = await api.put<DisclosureRequest>(`/cases/disclosure-requests/${requestId}/revoke`)
return res.data
}
export async function adminRevokeDisclosure(requestId: number): Promise<DisclosureRequest> {
const res = await api.put<DisclosureRequest>(`/admin/disclosure-requests/${requestId}/revoke`)
return res.data
}