diff --git a/frontend/src/pages/CasesPage.tsx b/frontend/src/pages/CasesPage.tsx index cc66288..fe107cf 100644 --- a/frontend/src/pages/CasesPage.tsx +++ b/frontend/src/pages/CasesPage.tsx @@ -1,9 +1,10 @@ import { useState, useEffect, useCallback, useRef } from 'react' import { - Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, + Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X, } from 'lucide-react' import api from '@/services/api' import type { Case, CaseListResponse } from '@/types' +import { useAuth } from '@/context/AuthContext' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' @@ -18,6 +19,9 @@ import { } from '@/components/ui/sheet' import { Skeleton } from '@/components/ui/skeleton' import { Alert, AlertDescription } from '@/components/ui/alert' +import { CASE_SECTIONS } from './cases/fieldConfig' +import { useInlineEdit } from './cases/useInlineEdit' +import { EditableField } from './cases/EditableField' const FALLGRUPPEN_LABELS: Record = { onko: 'Onkologie', @@ -111,7 +115,7 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) { setSheetOpen(true) } - const handleIcdSaved = (updated: Case) => { + const handleCaseSaved = (updated: Case) => { setSelectedCase(updated) // Refresh list setData((prev) => @@ -282,7 +286,7 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) { {selectedCase && ( )} @@ -305,56 +309,42 @@ function StatusBadges({ c }: { c: Case }) { function CaseDetail({ caseData, - onIcdSaved, + onCaseSaved, }: { caseData: Case - onIcdSaved: (updated: Case) => void + onCaseSaved: (updated: Case) => void }) { - const [icdValue, setIcdValue] = useState(caseData.icd || '') - const [kvnrValue, setKvnrValue] = useState(caseData.kvnr || '') - const [saving, setSaving] = useState(false) - const [savingKvnr, setSavingKvnr] = useState(false) - const [error, setError] = useState('') - const [success, setSuccess] = useState(false) - const [kvnrSuccess, setKvnrSuccess] = useState(false) + const { isAdmin } = useAuth() + const { + editing, formValues, saving, error, success, + startEditing, cancelEditing, saveAll, setFieldValue, canEditField, isDirty, + } = useInlineEdit(caseData, onCaseSaved) + + // ICD has its own endpoint and state + const [icdValue, setIcdValue] = useState(caseData.icd || '') + const [icdSaving, setIcdSaving] = useState(false) + const [icdError, setIcdError] = useState('') + const [icdSuccess, setIcdSuccess] = useState(false) - // Reset on case change useEffect(() => { setIcdValue(caseData.icd || '') - setKvnrValue(caseData.kvnr || '') - setError('') - setSuccess(false) - setKvnrSuccess(false) - }, [caseData.id, caseData.icd, caseData.kvnr]) - - const saveKvnr = async () => { - setSavingKvnr(true) - setError('') - setKvnrSuccess(false) - try { - const res = await api.put(`/cases/${caseData.id}/kvnr`, { kvnr: kvnrValue.trim() || null }) - onIcdSaved(res.data) - setKvnrSuccess(true) - } catch { - setError('Fehler beim Speichern der KVNR.') - } finally { - setSavingKvnr(false) - } - } + setIcdError('') + setIcdSuccess(false) + }, [caseData.id, caseData.icd]) const saveIcd = async () => { if (!icdValue.trim()) return - setSaving(true) - setError('') - setSuccess(false) + setIcdSaving(true) + setIcdError('') + setIcdSuccess(false) try { const res = await api.put(`/cases/${caseData.id}/icd`, { icd: icdValue.trim() }) - onIcdSaved(res.data) - setSuccess(true) + onCaseSaved(res.data) + setIcdSuccess(true) } catch { - setError('Fehler beim Speichern des ICD-Codes.') + setIcdError('Fehler beim Speichern des ICD-Codes.') } finally { - setSaving(false) + setIcdSaving(false) } } @@ -370,102 +360,109 @@ function CaseDetail({
+ {/* Edit toolbar */} +
+ {!editing ? ( + + ) : ( + <> + + + + )} +
+ + {error && ( + + {error} + + )} + {success && ( +

Änderungen gespeichert.

+ )} + {/* Status badges */}
- {/* Patient info */} + {/* Static metadata (always read-only) */}
- - - - -
- -
- { setKvnrValue(e.target.value); setKvnrSuccess(false) }} - placeholder="z.B. A123456789" - className="flex-1 font-mono" - /> - + + + + +
+ + {/* Editable sections from config */} + {CASE_SECTIONS.map((section) => ( +
+

{section.title}

+
+ {section.fields.map((field) => ( +
+ +
+ ))}
- {kvnrSuccess && ( -

KVNR gespeichert.

- )}
- -
+ ))} - {/* Case details */} -
- - - - - - -
- - {/* ICD edit */} + {/* ICD section — own endpoint with validation */}
- +
{ setIcdValue(e.target.value); setSuccess(false) }} + onChange={(e) => { setIcdValue(e.target.value); setIcdSuccess(false) }} placeholder="z.B. C50.9" className="flex-1" />
- {error && ( + {icdError && ( - {error} + {icdError} )} - {success && ( + {icdSuccess && (

ICD-Code gespeichert.

)}
- {/* Description fields */} - {caseData.kurzbeschreibung && ( -
- -

{caseData.kurzbeschreibung}

-
- )} - {caseData.fragestellung && ( -
- -

{caseData.fragestellung}

-
- )} - {caseData.kommentar && ( -
- -

{caseData.kommentar}

+ {/* Coding info (read-only, managed via coding endpoint) */} + {(caseData.gutachten_typ || caseData.therapieaenderung) && ( +
+

Coding

+
+ + +
)} + {/* Metadata footer */}

Importiert: {formatDateTime(caseData.imported_at)}

Aktualisiert: {formatDateTime(caseData.updated_at)}

@@ -476,11 +473,11 @@ function CaseDetail({ ) } -function DetailField({ label, value }: { label: string; value: string | null | undefined }) { +function ReadOnlyField({ label, value }: { label: string; value: string | null | undefined }) { return (
- {label} -

{value || '-'}

+ {label} +

{value || '–'}

) } diff --git a/frontend/src/pages/cases/EditableField.tsx b/frontend/src/pages/cases/EditableField.tsx new file mode 100644 index 0000000..f84b879 --- /dev/null +++ b/frontend/src/pages/cases/EditableField.tsx @@ -0,0 +1,114 @@ +import type { FieldConfig } from './fieldConfig' +import { Input } from '@/components/ui/input' +import { Switch } from '@/components/ui/switch' +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from '@/components/ui/select' + +interface EditableFieldProps { + field: FieldConfig + value: unknown + editable: boolean + onChange: (key: string, value: unknown) => void +} + +function formatDisplayDate(val: string | null | undefined): string { + if (!val) return '–' + try { + return new Date(val).toLocaleDateString('de-DE', { + day: '2-digit', month: '2-digit', year: 'numeric', + }) + } catch { + return val + } +} + +function formatDisplayBoolean(val: unknown): string { + if (val === true) return 'Ja' + if (val === false) return 'Nein' + return '–' +} + +function formatDisplayValue(field: FieldConfig, value: unknown): string { + if (field.type === 'date') return formatDisplayDate(value as string | null) + if (field.type === 'boolean') return formatDisplayBoolean(value) + if (field.type === 'select' && field.options) { + const opt = field.options.find((o) => o.value === (value ?? '')) + return opt?.label || (value as string) || '–' + } + return (value as string) || '–' +} + +export function EditableField({ field, value, editable, onChange }: EditableFieldProps) { + if (!editable) { + return ( +
+ {field.label} +

+ {formatDisplayValue(field, value)} +

+
+ ) + } + + const handleChange = (newVal: unknown) => onChange(field.key, newVal) + + return ( +
+ +
+ {field.type === 'text' && ( + handleChange(e.target.value)} + placeholder={field.placeholder} + className="h-8 text-sm" + /> + )} + {field.type === 'textarea' && ( +