dak.c2s/frontend/src/pages/CasesPage.tsx
CCS Admin c0f78278e4 feat: hide Fall-ID column and simplify export for DAK-Mitarbeiter
Fall-ID is redundant for DAK staff since KVNR is shown separately.
Export now only includes Datum, KVNR, Fallgruppe, ICD, Gutachten, Status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:08:29 +00:00

869 lines
32 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useCallback, useRef } from 'react'
import type { KeyboardEvent } from 'react'
import {
Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X, Info, Loader2, Download, Star, Trash2,
} from 'lucide-react'
import type { Case } from '@/types'
import { useAuth } from '@/context/AuthContext'
import { useCases, usePendingIcdCases, useIcdUpdate, type CaseFilters } from '@/hooks/useCases'
import api from '@/services/api'
import { useFilterPresets, useCreateFilterPreset, useDeleteFilterPreset } from '@/hooks/useFilterPresets'
import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select'
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from '@/components/ui/table'
import {
Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription,
} from '@/components/ui/sheet'
import { Skeleton } from '@/components/ui/skeleton'
import { Switch } from '@/components/ui/switch'
import { Label } from '@/components/ui/label'
import {
Popover, PopoverContent, PopoverTrigger,
} from '@/components/ui/popover'
import { Alert, AlertDescription } from '@/components/ui/alert'
import {
Tooltip, TooltipContent, TooltipTrigger,
} from '@/components/ui/tooltip'
import { CASE_SECTIONS } from './cases/fieldConfig'
import { useInlineEdit } from './cases/useInlineEdit'
import { EditableField } from './cases/EditableField'
import { requestDisclosure } from '@/services/disclosureService'
const FALLGRUPPEN_LABELS: Record<string, string> = {
onko: 'Onkologie',
kardio: 'Kardiologie',
intensiv: 'Intensivmedizin',
galle: 'Gallenblase',
sd: 'Schilddrüse',
}
const FALLGRUPPEN_OPTIONS = [
{ value: '__all__', label: 'Alle Fallgruppen' },
{ value: 'onko', label: 'Onkologie' },
{ value: 'kardio', label: 'Kardiologie' },
{ value: 'intensiv', label: 'Intensivmedizin' },
{ value: 'galle', label: 'Gallenblase' },
{ value: 'sd', label: 'Schilddrüse' },
]
const ICD_OPTIONS = [
{ value: '__all__', label: 'Alle' },
{ value: 'true', label: 'Mit ICD' },
{ value: 'false', label: 'Ohne ICD' },
]
interface CasesPageProps {
/** When true, fetches from pending-icd endpoint instead */
pendingIcdOnly?: boolean
}
export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
const { isAdmin } = useAuth()
const currentYear = new Date().getFullYear()
const [search, setSearch] = useState('')
const [debouncedSearch, setDebouncedSearch] = useState('')
const [jahr, setJahr] = useState<string>('__all__')
const [fallgruppe, setFallgruppe] = useState<string>('__all__')
const [hasIcd, setHasIcd] = useState<string>('__all__')
const [page, setPage] = useState(1)
const [perPage] = useState(50)
const [selectedCase, setSelectedCase] = useState<Case | null>(null)
const [sheetOpen, setSheetOpen] = useState(false)
const [batchMode, setBatchMode] = useState(false)
const [exporting, setExporting] = useState(false)
const [presetOpen, setPresetOpen] = useState(false)
const [presetName, setPresetName] = useState('')
const [showSavePreset, setShowSavePreset] = useState(false)
const { data: presets } = useFilterPresets()
const createPreset = useCreateFilterPreset()
const deletePreset = useDeleteFilterPreset()
// Debounce search
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const handleSearchChange = useCallback((val: string) => {
setSearch(val)
if (debounceRef.current) clearTimeout(debounceRef.current)
debounceRef.current = setTimeout(() => {
setDebouncedSearch(val)
setPage(1)
}, 300)
}, [])
useEffect(() => {
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current)
}
}, [])
// Fetch cases via TanStack Query
const filters: CaseFilters = {
page, per_page: perPage,
...(debouncedSearch ? { search: debouncedSearch } : {}),
...(jahr !== '__all__' ? { jahr: Number(jahr) } : {}),
...(fallgruppe !== '__all__' ? { fallgruppe } : {}),
...(hasIcd !== '__all__' ? { has_icd: hasIcd } : {}),
}
const casesQuery = useCases(filters, { enabled: !pendingIcdOnly })
const pendingQuery = usePendingIcdCases(page, perPage, { enabled: pendingIcdOnly })
const activeQuery = pendingIcdOnly ? pendingQuery : casesQuery
const data = activeQuery.data ?? null
const loading = activeQuery.isLoading
const totalPages = data ? Math.ceil(data.total / perPage) : 0
const years = Array.from({ length: 5 }, (_, i) => currentYear - i)
const openDetail = (c: Case) => {
setSelectedCase(c)
setSheetOpen(true)
}
const exportToExcel = async () => {
setExporting(true)
try {
const params: Record<string, string | number> = {}
if (jahr !== '__all__') params.jahr = Number(jahr)
if (fallgruppe !== '__all__') params.fallgruppe = fallgruppe
if (hasIcd !== '__all__') params.has_icd = hasIcd
if (debouncedSearch) params.search = debouncedSearch
const res = await api.get('/cases/export', { params, responseType: 'blob' })
const blob = new Blob([res.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `Fallliste_${new Date().toISOString().slice(0, 10)}.xlsx`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
} catch {
// silently fail — user sees no file download
} finally {
setExporting(false)
}
}
const applyPreset = (filters: { jahr?: number; fallgruppe?: string; has_icd?: string; search?: string }) => {
setJahr(filters.jahr != null ? String(filters.jahr) : '__all__')
setFallgruppe(filters.fallgruppe || '__all__')
setHasIcd(filters.has_icd || '__all__')
setSearch(filters.search || '')
setDebouncedSearch(filters.search || '')
setPage(1)
setPresetOpen(false)
}
const savePreset = async () => {
if (!presetName.trim()) return
const filterValues: Record<string, string | number> = {}
if (jahr !== '__all__') filterValues.jahr = Number(jahr)
if (fallgruppe !== '__all__') filterValues.fallgruppe = fallgruppe
if (hasIcd !== '__all__') filterValues.has_icd = hasIcd
if (debouncedSearch) filterValues.search = debouncedSearch
await createPreset.mutateAsync({ name: presetName.trim(), filters: filterValues })
setPresetName('')
setShowSavePreset(false)
}
return (
<div className="p-6 space-y-4">
<div className="flex items-start justify-between">
<div>
<h1 className="text-2xl font-bold">
{pendingIcdOnly ? 'ICD-Eingabe' : 'Fälle'}
</h1>
{pendingIcdOnly && (
<p className="text-sm text-muted-foreground mt-1">
{batchMode
? 'ICD-Codes direkt in der Tabelle eingeben. Enter zum Speichern, Tab zum nächsten Feld.'
: 'Fälle, die noch einen ICD-10-Diagnosecode benötigen. Klicken Sie auf einen Fall, um den Code einzugeben.'}
</p>
)}
</div>
{pendingIcdOnly && (
<div className="flex items-center gap-2">
<Switch id="batch-mode" checked={batchMode} onCheckedChange={setBatchMode} />
<Label htmlFor="batch-mode" className="text-sm cursor-pointer">Batch-Modus</Label>
</div>
)}
</div>
{/* Filter bar */}
{!pendingIcdOnly && (
<div className="flex flex-wrap items-center gap-3">
<div className="relative flex-1 min-w-[200px] max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
placeholder={isAdmin ? "Suche nach Name, Fall-ID, KVNR..." : "Suche nach Fall-ID, KVNR..."}
value={search}
onChange={(e) => handleSearchChange(e.target.value)}
className="pl-9"
/>
</div>
<Select value={jahr} onValueChange={(v) => { setJahr(v); setPage(1) }}>
<SelectTrigger className="w-[120px]">
<SelectValue placeholder="Jahr" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__all__">Alle Jahre</SelectItem>
{years.map((y) => (
<SelectItem key={y} value={String(y)}>{y}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={fallgruppe} onValueChange={(v) => { setFallgruppe(v); setPage(1) }}>
<SelectTrigger className="w-[170px]">
<SelectValue placeholder="Fallgruppe" />
</SelectTrigger>
<SelectContent>
{FALLGRUPPEN_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={hasIcd} onValueChange={(v) => { setHasIcd(v); setPage(1) }}>
<SelectTrigger className="w-[130px]">
<SelectValue placeholder="ICD" />
</SelectTrigger>
<SelectContent>
{ICD_OPTIONS.map((o) => (
<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
))}
</SelectContent>
</Select>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="icon" onClick={exportToExcel} disabled={exporting}>
{exporting ? <Loader2 className="size-4 animate-spin" /> : <Download className="size-4" />}
</Button>
</TooltipTrigger>
<TooltipContent>Gefilterte Fallliste als Excel exportieren</TooltipContent>
</Tooltip>
<Popover open={presetOpen} onOpenChange={setPresetOpen}>
<PopoverTrigger asChild>
<Button variant="outline" size="sm" className="gap-1.5">
<Star className="size-4" />
Filter
{presets && presets.length > 0 && (
<Badge variant="secondary" className="ml-1 px-1.5 py-0 text-xs">{presets.length}</Badge>
)}
</Button>
</PopoverTrigger>
<PopoverContent className="w-72 p-2" align="end">
<div className="space-y-1">
{presets && presets.length > 0 ? (
presets.map((p) => (
<div key={p.id} className="flex items-center gap-1 group">
<button
className="flex-1 text-left text-sm px-2 py-1.5 rounded hover:bg-accent transition-colors truncate"
onClick={() => applyPreset(p.filters)}
>
{p.name}
</button>
<Button
variant="ghost"
size="icon"
className="size-7 opacity-0 group-hover:opacity-100 transition-opacity shrink-0"
onClick={() => deletePreset.mutate(p.id)}
>
<Trash2 className="size-3.5 text-muted-foreground" />
</Button>
</div>
))
) : (
<p className="text-sm text-muted-foreground px-2 py-1.5">Keine gespeicherten Filter.</p>
)}
<div className="border-t pt-1 mt-1">
{showSavePreset ? (
<div className="flex items-center gap-1.5 px-1">
<Input
value={presetName}
onChange={(e) => setPresetName(e.target.value)}
onKeyDown={(e) => { if (e.key === 'Enter') { e.preventDefault(); savePreset() } if (e.key === 'Escape') setShowSavePreset(false) }}
placeholder="z.B. Onko ohne ICD"
className="h-8 text-sm"
autoFocus
/>
<Button size="sm" className="h-8 px-2" onClick={savePreset} disabled={!presetName.trim() || createPreset.isPending}>
<Save className="size-3.5" />
</Button>
</div>
) : (
<button
className="w-full text-left text-sm px-2 py-1.5 rounded hover:bg-accent transition-colors text-muted-foreground"
onClick={() => setShowSavePreset(true)}
>
+ Aktuellen Filter speichern
</button>
)}
</div>
</div>
</PopoverContent>
</Popover>
</div>
)}
{/* Table */}
{loading ? (
<div className="space-y-2">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full" />
))}
</div>
) : data && data.items.length > 0 ? (
<>
<Table>
<TableHeader>
<TableRow>
{isAdmin && (
<TableHead>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center gap-1 cursor-help border-b border-dashed border-muted-foreground/40">
Fall-ID
<Info className="size-3 text-muted-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>Eindeutige Kennung: Jahr-KW-Fallgruppe-KVNR</TooltipContent>
</Tooltip>
</TableHead>
)}
<TableHead>Datum</TableHead>
{isAdmin && <TableHead>Nachname</TableHead>}
{isAdmin && <TableHead>Vorname</TableHead>}
<TableHead>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center gap-1 cursor-help border-b border-dashed border-muted-foreground/40">
KVNR
<Info className="size-3 text-muted-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>Krankenversichertennummer des Versicherten</TooltipContent>
</Tooltip>
</TableHead>
<TableHead>Fallgruppe</TableHead>
<TableHead>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center gap-1 cursor-help border-b border-dashed border-muted-foreground/40">
ICD
<Info className="size-3 text-muted-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>Internationale Klassifikation der Krankheiten (Diagnosecode)</TooltipContent>
</Tooltip>
</TableHead>
<TableHead>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center gap-1 cursor-help border-b border-dashed border-muted-foreground/40">
Gutachten
<Info className="size-3 text-muted-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>Schriftliche medizinische Stellungnahme vorhanden</TooltipContent>
</Tooltip>
</TableHead>
<TableHead>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center gap-1 cursor-help border-b border-dashed border-muted-foreground/40">
Status
<Info className="size-3 text-muted-foreground" />
</span>
</TooltipTrigger>
<TooltipContent>Aktueller Bearbeitungsstatus des Falls</TooltipContent>
</Tooltip>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data.items.map((c) => (
<TableRow
key={c.id}
className={pendingIcdOnly && batchMode ? '' : 'cursor-pointer'}
onClick={() => { if (!(pendingIcdOnly && batchMode)) openDetail(c) }}
>
{isAdmin && (
<TableCell className="font-mono text-sm">
{c.fall_id || '-'}
</TableCell>
)}
<TableCell>{formatDate(c.datum)}</TableCell>
{isAdmin && <TableCell className="font-medium">{c.nachname}</TableCell>}
{isAdmin && <TableCell>{c.vorname || '-'}</TableCell>}
<TableCell className="font-mono text-sm">
{c.kvnr || '-'}
</TableCell>
<TableCell>
<Badge variant="outline">
{FALLGRUPPEN_LABELS[c.fallgruppe] || c.fallgruppe}
</Badge>
</TableCell>
<TableCell>
{pendingIcdOnly && batchMode ? (
<InlineIcdInput caseId={c.id} initialValue={c.icd || ''} />
) : c.icd ? (
<span className="font-mono text-sm">{c.icd}</span>
) : (
<span className="text-muted-foreground">-</span>
)}
</TableCell>
<TableCell>
{c.gutachten ? (
<CheckCircle className="size-4 text-green-600" />
) : (
<XCircle className="size-4 text-muted-foreground" />
)}
</TableCell>
<TableCell>
<StatusBadges c={c} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{/* Pagination */}
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
{data.total} Fälle insgesamt
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
disabled={page <= 1}
onClick={() => setPage((p) => p - 1)}
>
<ChevronLeft className="size-4" />
</Button>
<span className="text-sm">
Seite {page} von {totalPages || 1}
</span>
<Button
variant="outline"
size="sm"
disabled={page >= totalPages}
onClick={() => setPage((p) => p + 1)}
>
<ChevronRight className="size-4" />
</Button>
</div>
</div>
</>
) : (
<p className="text-muted-foreground py-8 text-center">
Keine Fälle gefunden.
</p>
)}
{/* Detail Sheet */}
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
{selectedCase && (
<CaseDetail caseData={selectedCase} />
)}
</SheetContent>
</Sheet>
</div>
)
}
function InlineIcdInput({ caseId, initialValue }: { caseId: number; initialValue: string }) {
const [value, setValue] = useState(initialValue)
const [status, setStatus] = useState<'idle' | 'saving' | 'saved' | 'error'>('idle')
const [errorMsg, setErrorMsg] = useState('')
const icdMutation = useIcdUpdate()
const inputRef = useRef<HTMLInputElement>(null)
const save = async () => {
const trimmed = value.trim()
if (!trimmed || trimmed === initialValue) return
setStatus('saving')
setErrorMsg('')
try {
await icdMutation.mutateAsync({ id: caseId, icd: trimmed })
setStatus('saved')
} catch (err: any) {
const detail = err?.response?.data?.detail
setErrorMsg(typeof detail === 'string' ? detail : 'Ungültiger ICD-Code')
setStatus('error')
}
}
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
e.preventDefault()
save()
}
if (e.key === 'Escape') {
setValue(initialValue)
setStatus('idle')
setErrorMsg('')
}
}
return (
<div className="flex items-center gap-1.5" onClick={(e) => e.stopPropagation()}>
<Input
ref={inputRef}
value={value}
onChange={(e) => { setValue(e.target.value); if (status === 'saved' || status === 'error') setStatus('idle') }}
onKeyDown={handleKeyDown}
onBlur={save}
placeholder="z.B. C50.9"
className={`h-8 w-32 font-mono text-sm ${
status === 'saved' ? 'border-green-500 focus-visible:ring-green-500' :
status === 'error' ? 'border-red-500 focus-visible:ring-red-500' : ''
}`}
disabled={status === 'saving'}
/>
{status === 'saving' && <Loader2 className="size-4 text-muted-foreground animate-spin" />}
{status === 'saved' && <CheckCircle className="size-4 text-green-600" />}
{status === 'error' && (
<Tooltip>
<TooltipTrigger asChild>
<XCircle className="size-4 text-red-500 cursor-help" />
</TooltipTrigger>
<TooltipContent>{errorMsg}</TooltipContent>
</Tooltip>
)}
</div>
)
}
function StatusBadges({ c }: { c: Case }) {
return (
<div className="flex gap-1">
{c.unterlagen && (
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="secondary" className="text-xs cursor-help">Unterlagen</Badge>
</TooltipTrigger>
<TooltipContent>Patientenunterlagen wurden eingereicht</TooltipContent>
</Tooltip>
)}
{c.gutachten && (
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="secondary" className="text-xs cursor-help">Gutachten</Badge>
</TooltipTrigger>
<TooltipContent>Medizinisches Gutachten wurde erstellt</TooltipContent>
</Tooltip>
)}
{c.abgerechnet && (
<Tooltip>
<TooltipTrigger asChild>
<Badge className="text-xs cursor-help">Abgerechnet</Badge>
</TooltipTrigger>
<TooltipContent>Fall wurde abgerechnet</TooltipContent>
</Tooltip>
)}
{c.ablehnung && (
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="destructive" className="text-xs cursor-help">Abgelehnt</Badge>
</TooltipTrigger>
<TooltipContent>Leistung wurde abgelehnt</TooltipContent>
</Tooltip>
)}
{c.abbruch && (
<Tooltip>
<TooltipTrigger asChild>
<Badge variant="destructive" className="text-xs cursor-help">Abbruch</Badge>
</TooltipTrigger>
<TooltipContent>Verfahren wurde abgebrochen</TooltipContent>
</Tooltip>
)}
</div>
)
}
function CaseDetail({ caseData }: { caseData: Case }) {
const { isAdmin } = useAuth()
const {
editing, formValues, saving, error, success,
startEditing, cancelEditing, saveAll, setFieldValue, canEditField, isDirty,
} = useInlineEdit(caseData)
// ICD has its own endpoint and state
const icdMutation = useIcdUpdate()
const [icdValue, setIcdValue] = useState(caseData.icd || '')
const [icdError, setIcdError] = useState('')
const [icdSuccess, setIcdSuccess] = useState(false)
useEffect(() => {
setIcdValue(caseData.icd || '')
setIcdError('')
setIcdSuccess(false)
}, [caseData.id, caseData.icd])
const saveIcd = async () => {
if (!icdValue.trim()) return
setIcdError('')
setIcdSuccess(false)
try {
await icdMutation.mutateAsync({ id: caseData.id, icd: icdValue.trim() })
setIcdSuccess(true)
} catch {
setIcdError('Fehler beim Speichern des ICD-Codes.')
}
}
return (
<>
<SheetHeader>
<SheetTitle>
Fall {caseData.fall_id || `#${caseData.id}`}
</SheetTitle>
<SheetDescription>
{FALLGRUPPEN_LABELS[caseData.fallgruppe] || caseData.fallgruppe} &mdash; KW {caseData.kw}/{caseData.jahr}
</SheetDescription>
</SheetHeader>
<div className="px-4 space-y-4">
{/* Edit toolbar */}
<div className="flex items-center gap-2">
{!editing ? (
<Button size="sm" variant="outline" onClick={startEditing}>
<Pencil className="size-4 mr-1" />
Bearbeiten
</Button>
) : (
<>
<Button size="sm" onClick={saveAll} disabled={saving || !isDirty}>
<Save className="size-4 mr-1" />
{saving ? 'Speichern...' : 'Speichern'}
</Button>
<Button size="sm" variant="outline" onClick={cancelEditing} disabled={saving}>
<X className="size-4 mr-1" />
Abbrechen
</Button>
</>
)}
</div>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{success && (
<p className="text-sm text-green-600">Änderungen gespeichert.</p>
)}
{/* Status badges (admin only) */}
{isAdmin && (
<div className="flex flex-wrap gap-2">
<StatusBadges c={caseData} />
</div>
)}
{/* Disclosure status / request */}
{!isAdmin && caseData.disclosure_granted && caseData.disclosure_expires_at && (
<Alert>
<AlertDescription>
Personendaten sichtbar bis {formatDateTime(caseData.disclosure_expires_at)}
</AlertDescription>
</Alert>
)}
{!isAdmin && !caseData.disclosure_granted && (
<DisclosureRequestButton caseId={caseData.id} />
)}
{/* Static metadata (always read-only) */}
<div className="grid grid-cols-2 gap-3 text-sm">
<ReadOnlyField label="Fall-ID" value={caseData.fall_id} />
<ReadOnlyField label="CRM-Ticket" value={caseData.crm_ticket_id} />
<ReadOnlyField label="Datum" value={formatDate(caseData.datum)} />
<ReadOnlyField label="KW / Jahr" value={`${caseData.kw} / ${caseData.jahr}`} />
</div>
{/* Editable sections from config */}
{CASE_SECTIONS.map((section) => {
if (section.visibleTo === 'admin' && !isAdmin) return null
const visibleFields = section.fields.filter((field) => {
if (field.visibleTo === 'admin' && !isAdmin && !caseData.disclosure_granted) return false
return true
})
if (visibleFields.length === 0) return null
return (
<div key={section.title} className="border-t pt-3 space-y-3">
<h3 className="text-sm font-semibold">{section.title}</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
{visibleFields.map((field) => (
<div key={field.key} className={field.colSpan === 2 ? 'col-span-2' : ''}>
<EditableField
field={field}
value={editing ? formValues[field.key] : caseData[field.key]}
editable={canEditField(field, isAdmin)}
onChange={setFieldValue}
/>
</div>
))}
</div>
</div>
)
})}
{/* ICD section — own endpoint with validation */}
<div className="border-t pt-3 space-y-2">
<Tooltip>
<TooltipTrigger asChild>
<label className="text-sm font-semibold inline-flex items-center gap-1 cursor-help">
ICD-Code
<Info className="size-3 text-muted-foreground" />
</label>
</TooltipTrigger>
<TooltipContent>ICD-10-GM Diagnoseschlüssel, z.B. C50.9 für bösartige Neubildung der Brustdrüse</TooltipContent>
</Tooltip>
<div className="flex gap-2">
<Input
value={icdValue}
onChange={(e) => { setIcdValue(e.target.value); setIcdSuccess(false) }}
placeholder="z.B. C50.9"
className="flex-1"
/>
<Button
size="sm"
onClick={saveIcd}
disabled={icdMutation.isPending || !icdValue.trim()}
>
<Save className="size-4 mr-1" />
{icdMutation.isPending ? 'Speichern...' : 'Speichern'}
</Button>
</div>
{icdError && (
<Alert variant="destructive">
<AlertDescription>{icdError}</AlertDescription>
</Alert>
)}
{icdSuccess && (
<p className="text-sm text-green-600">ICD-Code gespeichert.</p>
)}
</div>
{/* Coding info (read-only, admin only) */}
{isAdmin && (caseData.gutachten_typ || caseData.therapieaenderung) && (
<div className="border-t pt-3 space-y-2">
<h3 className="text-sm font-semibold">Coding</h3>
<div className="grid grid-cols-2 gap-3 text-sm">
<ReadOnlyField label="Gutachten-Typ" value={caseData.gutachten_typ} />
<ReadOnlyField label="Therapieänderung" value={caseData.therapieaenderung} />
</div>
</div>
)}
{/* Metadata footer */}
<div className="border-t pt-3 text-xs text-muted-foreground space-y-1">
<p>Importiert: {formatDateTime(caseData.imported_at)}</p>
<p>Aktualisiert: {formatDateTime(caseData.updated_at)}</p>
{caseData.import_source && <p>Quelle: {caseData.import_source}</p>}
</div>
</div>
</>
)
}
function DisclosureRequestButton({ caseId }: { caseId: number }) {
const [open, setOpen] = useState(false)
const [reason, setReason] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)
const submit = async () => {
if (!reason.trim()) return
setLoading(true)
setError('')
try {
await requestDisclosure(caseId, reason.trim())
setSuccess(true)
setOpen(false)
} catch (err: any) {
const detail = err.response?.data?.detail
setError(typeof detail === 'string' ? detail : 'Fehler beim Senden der Anfrage')
} finally {
setLoading(false)
}
}
if (success) {
return <p className="text-sm text-muted-foreground">Freigabe-Anfrage gesendet.</p>
}
return (
<>
<Tooltip>
<TooltipTrigger asChild>
<Button size="sm" variant="outline" onClick={() => setOpen(true)}>
Personendaten anfordern
</Button>
</TooltipTrigger>
<TooltipContent>Zeitlich begrenzte Freigabe (24h) der Personendaten beantragen</TooltipContent>
</Tooltip>
{open && (
<div className="space-y-2 border rounded-lg p-3">
<p className="text-sm text-muted-foreground">
Begründung für die Einsicht in Personendaten:
</p>
<Input
value={reason}
onChange={(e) => setReason(e.target.value)}
placeholder="z.B. KVNR-Fehler, Identifikation nötig"
/>
{error && <p className="text-sm text-red-600">{error}</p>}
<div className="flex gap-2">
<Button size="sm" onClick={submit} disabled={loading || !reason.trim()}>
{loading ? 'Senden...' : 'Anfrage senden'}
</Button>
<Button size="sm" variant="outline" onClick={() => { setOpen(false); setError('') }}>
Abbrechen
</Button>
</div>
</div>
)}
</>
)
}
function ReadOnlyField({ label, value }: { label: string; value: string | null | undefined }) {
return (
<div>
<span className="text-muted-foreground text-sm">{label}</span>
<p className="font-medium text-sm">{value || ''}</p>
</div>
)
}
function formatDate(dateStr: string): string {
try {
return new Date(dateStr).toLocaleDateString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric',
})
} catch {
return dateStr
}
}
function formatDateTime(dateStr: string): string {
try {
return new Date(dateStr).toLocaleString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
})
} catch {
return dateStr
}
}