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 = { 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('__all__') const [fallgruppe, setFallgruppe] = useState('__all__') const [hasIcd, setHasIcd] = useState('__all__') const [page, setPage] = useState(1) const [perPage] = useState(50) const [selectedCase, setSelectedCase] = useState(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 | 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 = {} 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 = {} 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 (

{pendingIcdOnly ? 'ICD-Eingabe' : 'Fälle'}

{pendingIcdOnly && (

{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.'}

)}
{pendingIcdOnly && (
)}
{/* Filter bar */} {!pendingIcdOnly && (
handleSearchChange(e.target.value)} className="pl-9" />
Gefilterte Fallliste als Excel exportieren
{presets && presets.length > 0 ? ( presets.map((p) => (
)) ) : (

Keine gespeicherten Filter.

)}
{showSavePreset ? (
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 />
) : ( )}
)} {/* Table */} {loading ? (
{Array.from({ length: 8 }).map((_, i) => ( ))}
) : data && data.items.length > 0 ? ( <> {isAdmin && ( Fall-ID Eindeutige Kennung: Jahr-KW-Fallgruppe-KVNR )} Datum {isAdmin && Nachname} {isAdmin && Vorname} KVNR Krankenversichertennummer des Versicherten Fallgruppe ICD Internationale Klassifikation der Krankheiten (Diagnosecode) Gutachten Schriftliche medizinische Stellungnahme vorhanden Status Aktueller Bearbeitungsstatus des Falls {data.items.map((c) => ( { if (!(pendingIcdOnly && batchMode)) openDetail(c) }} > {isAdmin && ( {c.fall_id || '-'} )} {formatDate(c.datum)} {isAdmin && {c.nachname}} {isAdmin && {c.vorname || '-'}} {c.kvnr || '-'} {FALLGRUPPEN_LABELS[c.fallgruppe] || c.fallgruppe} {pendingIcdOnly && batchMode ? ( ) : c.icd ? ( {c.icd} ) : ( - )} {c.gutachten ? ( ) : ( )} ))}
{/* Pagination */}

{data.total} Fälle insgesamt

Seite {page} von {totalPages || 1}
) : (

Keine Fälle gefunden.

)} {/* Detail Sheet */} {selectedCase && ( )}
) } 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(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) => { if (e.key === 'Enter') { e.preventDefault() save() } if (e.key === 'Escape') { setValue(initialValue) setStatus('idle') setErrorMsg('') } } return (
e.stopPropagation()}> { 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' && } {status === 'saved' && } {status === 'error' && ( {errorMsg} )}
) } function StatusBadges({ c }: { c: Case }) { return (
{c.unterlagen && ( Unterlagen Patientenunterlagen wurden eingereicht )} {c.gutachten && ( Gutachten Medizinisches Gutachten wurde erstellt )} {c.abgerechnet && ( Abgerechnet Fall wurde abgerechnet )} {c.ablehnung && ( Abgelehnt Leistung wurde abgelehnt )} {c.abbruch && ( Abbruch Verfahren wurde abgebrochen )}
) } 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 ( <> Fall {caseData.fall_id || `#${caseData.id}`} {FALLGRUPPEN_LABELS[caseData.fallgruppe] || caseData.fallgruppe} — KW {caseData.kw}/{caseData.jahr}
{/* Edit toolbar */}
{!editing ? ( ) : ( <> )}
{error && ( {error} )} {success && (

Änderungen gespeichert.

)} {/* Status badges (admin only) */} {isAdmin && (
)} {/* Disclosure status / request */} {!isAdmin && caseData.disclosure_granted && caseData.disclosure_expires_at && ( Personendaten sichtbar bis {formatDateTime(caseData.disclosure_expires_at)} )} {!isAdmin && !caseData.disclosure_granted && ( )} {/* Static metadata (always read-only) */}
{/* 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 (

{section.title}

{visibleFields.map((field) => (
))}
) })} {/* ICD section — own endpoint with validation */}
ICD-10-GM Diagnoseschlüssel, z.B. C50.9 für bösartige Neubildung der Brustdrüse
{ setIcdValue(e.target.value); setIcdSuccess(false) }} placeholder="z.B. C50.9" className="flex-1" />
{icdError && ( {icdError} )} {icdSuccess && (

ICD-Code gespeichert.

)}
{/* Coding info (read-only, admin only) */} {isAdmin && (caseData.gutachten_typ || caseData.therapieaenderung) && (

Coding

)} {/* Metadata footer */}

Importiert: {formatDateTime(caseData.imported_at)}

Aktualisiert: {formatDateTime(caseData.updated_at)}

{caseData.import_source &&

Quelle: {caseData.import_source}

}
) } 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

Freigabe-Anfrage gesendet.

} return ( <> Zeitlich begrenzte Freigabe (24h) der Personendaten beantragen {open && (

Begründung für die Einsicht in Personendaten:

setReason(e.target.value)} placeholder="z.B. KVNR-Fehler, Identifikation nötig" /> {error &&

{error}

}
)} ) } function ReadOnlyField({ label, value }: { label: string; value: string | null | undefined }) { return (
{label}

{value || '–'}

) } 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 } }