feat: make all case detail fields editable with edit-mode toggle

Replace individual field editors (KVNR, ICD) with a unified edit-mode
approach using data-driven field configuration. A single "Bearbeiten"
button toggles all fields into edit mode with dirty-tracking and
split-save (KVNR via dedicated endpoint for all users, remaining fields
via admin-only general update endpoint).

- Extend Case TypeScript interface with 17 missing backend fields
- Add declarative field config (7 sections, 30 fields) in fieldConfig.ts
- Add useInlineEdit hook with dirty-tracking and split-save logic
- Add EditableField dual-mode component (text/date/boolean/select/textarea)
- Refactor CaseDetail to render sections from config
- ICD section retains its own endpoint with separate save button

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-24 11:23:03 +00:00
parent 97731552c5
commit 19db4c5def
5 changed files with 499 additions and 108 deletions

View file

@ -1,9 +1,10 @@
import { useState, useEffect, useCallback, useRef } from 'react' import { useState, useEffect, useCallback, useRef } from 'react'
import { import {
Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X,
} from 'lucide-react' } from 'lucide-react'
import api from '@/services/api' import api from '@/services/api'
import type { Case, CaseListResponse } from '@/types' import type { Case, CaseListResponse } from '@/types'
import { useAuth } from '@/context/AuthContext'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge' import { Badge } from '@/components/ui/badge'
@ -18,6 +19,9 @@ import {
} from '@/components/ui/sheet' } from '@/components/ui/sheet'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { Alert, AlertDescription } from '@/components/ui/alert' 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<string, string> = { const FALLGRUPPEN_LABELS: Record<string, string> = {
onko: 'Onkologie', onko: 'Onkologie',
@ -111,7 +115,7 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
setSheetOpen(true) setSheetOpen(true)
} }
const handleIcdSaved = (updated: Case) => { const handleCaseSaved = (updated: Case) => {
setSelectedCase(updated) setSelectedCase(updated)
// Refresh list // Refresh list
setData((prev) => setData((prev) =>
@ -282,7 +286,7 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
{selectedCase && ( {selectedCase && (
<CaseDetail <CaseDetail
caseData={selectedCase} caseData={selectedCase}
onIcdSaved={handleIcdSaved} onCaseSaved={handleCaseSaved}
/> />
)} )}
</SheetContent> </SheetContent>
@ -305,56 +309,42 @@ function StatusBadges({ c }: { c: Case }) {
function CaseDetail({ function CaseDetail({
caseData, caseData,
onIcdSaved, onCaseSaved,
}: { }: {
caseData: Case caseData: Case
onIcdSaved: (updated: Case) => void onCaseSaved: (updated: Case) => void
}) { }) {
const [icdValue, setIcdValue] = useState(caseData.icd || '') const { isAdmin } = useAuth()
const [kvnrValue, setKvnrValue] = useState(caseData.kvnr || '') const {
const [saving, setSaving] = useState(false) editing, formValues, saving, error, success,
const [savingKvnr, setSavingKvnr] = useState(false) startEditing, cancelEditing, saveAll, setFieldValue, canEditField, isDirty,
const [error, setError] = useState('') } = useInlineEdit(caseData, onCaseSaved)
const [success, setSuccess] = useState(false)
const [kvnrSuccess, setKvnrSuccess] = useState(false) // 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(() => { useEffect(() => {
setIcdValue(caseData.icd || '') setIcdValue(caseData.icd || '')
setKvnrValue(caseData.kvnr || '') setIcdError('')
setError('') setIcdSuccess(false)
setSuccess(false) }, [caseData.id, caseData.icd])
setKvnrSuccess(false)
}, [caseData.id, caseData.icd, caseData.kvnr])
const saveKvnr = async () => {
setSavingKvnr(true)
setError('')
setKvnrSuccess(false)
try {
const res = await api.put<Case>(`/cases/${caseData.id}/kvnr`, { kvnr: kvnrValue.trim() || null })
onIcdSaved(res.data)
setKvnrSuccess(true)
} catch {
setError('Fehler beim Speichern der KVNR.')
} finally {
setSavingKvnr(false)
}
}
const saveIcd = async () => { const saveIcd = async () => {
if (!icdValue.trim()) return if (!icdValue.trim()) return
setSaving(true) setIcdSaving(true)
setError('') setIcdError('')
setSuccess(false) setIcdSuccess(false)
try { try {
const res = await api.put<Case>(`/cases/${caseData.id}/icd`, { icd: icdValue.trim() }) const res = await api.put<Case>(`/cases/${caseData.id}/icd`, { icd: icdValue.trim() })
onIcdSaved(res.data) onCaseSaved(res.data)
setSuccess(true) setIcdSuccess(true)
} catch { } catch {
setError('Fehler beim Speichern des ICD-Codes.') setIcdError('Fehler beim Speichern des ICD-Codes.')
} finally { } finally {
setSaving(false) setIcdSaving(false)
} }
} }
@ -370,102 +360,109 @@ function CaseDetail({
</SheetHeader> </SheetHeader>
<div className="px-4 space-y-4"> <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 */} {/* Status badges */}
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<StatusBadges c={caseData} /> <StatusBadges c={caseData} />
</div> </div>
{/* Patient info */} {/* Static metadata (always read-only) */}
<div className="grid grid-cols-2 gap-3 text-sm"> <div className="grid grid-cols-2 gap-3 text-sm">
<DetailField label="Anrede" value={caseData.anrede} /> <ReadOnlyField label="Fall-ID" value={caseData.fall_id} />
<DetailField label="Vorname" value={caseData.vorname} /> <ReadOnlyField label="CRM-Ticket" value={caseData.crm_ticket_id} />
<DetailField label="Nachname" value={caseData.nachname} /> <ReadOnlyField label="Datum" value={formatDate(caseData.datum)} />
<DetailField label="Geburtsdatum" value={caseData.geburtsdatum ? formatDate(caseData.geburtsdatum) : null} /> <ReadOnlyField label="KW / Jahr" value={`${caseData.kw} / ${caseData.jahr}`} />
<div className="col-span-2"> </div>
<label className="text-sm text-muted-foreground">KVNR (Versichertennummer)</label>
<div className="flex gap-2 mt-1"> {/* Editable sections from config */}
<Input {CASE_SECTIONS.map((section) => (
value={kvnrValue} <div key={section.title} className="border-t pt-3 space-y-3">
onChange={(e) => { setKvnrValue(e.target.value); setKvnrSuccess(false) }} <h3 className="text-sm font-semibold">{section.title}</h3>
placeholder="z.B. A123456789" <div className="grid grid-cols-2 gap-3 text-sm">
className="flex-1 font-mono" {section.fields.map((field) => (
/> <div key={field.key} className={field.colSpan === 2 ? 'col-span-2' : ''}>
<Button <EditableField
size="sm" field={field}
variant="outline" value={editing ? formValues[field.key] : caseData[field.key]}
onClick={saveKvnr} editable={canEditField(field, isAdmin)}
disabled={savingKvnr || kvnrValue === (caseData.kvnr || '')} onChange={setFieldValue}
> />
<Save className="size-4 mr-1" /> </div>
{savingKvnr ? '...' : 'Speichern'} ))}
</Button>
</div> </div>
{kvnrSuccess && (
<p className="text-sm text-green-600 mt-1">KVNR gespeichert.</p>
)}
</div> </div>
<DetailField label="Versicherung" value={caseData.versicherung} /> ))}
</div>
{/* Case details */} {/* ICD section — own endpoint with validation */}
<div className="border-t pt-3 grid grid-cols-2 gap-3 text-sm">
<DetailField label="Datum" value={formatDate(caseData.datum)} />
<DetailField label="KW / Jahr" value={`${caseData.kw} / ${caseData.jahr}`} />
<DetailField label="CRM-Ticket" value={caseData.crm_ticket_id} />
<DetailField label="Gutachter" value={caseData.gutachter} />
<DetailField label="Gutachten-Typ" value={caseData.gutachten_typ} />
<DetailField label="Therapieänderung" value={caseData.therapieaenderung} />
</div>
{/* ICD edit */}
<div className="border-t pt-3 space-y-2"> <div className="border-t pt-3 space-y-2">
<label className="text-sm font-medium">ICD-Code</label> <label className="text-sm font-semibold">ICD-Code</label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
value={icdValue} value={icdValue}
onChange={(e) => { setIcdValue(e.target.value); setSuccess(false) }} onChange={(e) => { setIcdValue(e.target.value); setIcdSuccess(false) }}
placeholder="z.B. C50.9" placeholder="z.B. C50.9"
className="flex-1" className="flex-1"
/> />
<Button <Button
size="sm" size="sm"
onClick={saveIcd} onClick={saveIcd}
disabled={saving || !icdValue.trim()} disabled={icdSaving || !icdValue.trim()}
> >
<Save className="size-4 mr-1" /> <Save className="size-4 mr-1" />
{saving ? 'Speichern...' : 'Speichern'} {icdSaving ? 'Speichern...' : 'Speichern'}
</Button> </Button>
</div> </div>
{error && ( {icdError && (
<Alert variant="destructive"> <Alert variant="destructive">
<AlertDescription>{error}</AlertDescription> <AlertDescription>{icdError}</AlertDescription>
</Alert> </Alert>
)} )}
{success && ( {icdSuccess && (
<p className="text-sm text-green-600">ICD-Code gespeichert.</p> <p className="text-sm text-green-600">ICD-Code gespeichert.</p>
)} )}
</div> </div>
{/* Description fields */} {/* Coding info (read-only, managed via coding endpoint) */}
{caseData.kurzbeschreibung && ( {(caseData.gutachten_typ || caseData.therapieaenderung) && (
<div className="border-t pt-3 space-y-1"> <div className="border-t pt-3 space-y-2">
<label className="text-sm font-medium">Kurzbeschreibung</label> <h3 className="text-sm font-semibold">Coding</h3>
<p className="text-sm text-muted-foreground whitespace-pre-wrap">{caseData.kurzbeschreibung}</p> <div className="grid grid-cols-2 gap-3 text-sm">
</div> <ReadOnlyField label="Gutachten-Typ" value={caseData.gutachten_typ} />
)} <ReadOnlyField label="Therapieänderung" value={caseData.therapieaenderung} />
{caseData.fragestellung && ( </div>
<div className="space-y-1">
<label className="text-sm font-medium">Fragestellung</label>
<p className="text-sm text-muted-foreground whitespace-pre-wrap">{caseData.fragestellung}</p>
</div>
)}
{caseData.kommentar && (
<div className="space-y-1">
<label className="text-sm font-medium">Kommentar</label>
<p className="text-sm text-muted-foreground whitespace-pre-wrap">{caseData.kommentar}</p>
</div> </div>
)} )}
{/* Metadata footer */}
<div className="border-t pt-3 text-xs text-muted-foreground space-y-1"> <div className="border-t pt-3 text-xs text-muted-foreground space-y-1">
<p>Importiert: {formatDateTime(caseData.imported_at)}</p> <p>Importiert: {formatDateTime(caseData.imported_at)}</p>
<p>Aktualisiert: {formatDateTime(caseData.updated_at)}</p> <p>Aktualisiert: {formatDateTime(caseData.updated_at)}</p>
@ -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 ( return (
<div> <div>
<span className="text-muted-foreground">{label}</span> <span className="text-muted-foreground text-sm">{label}</span>
<p className="font-medium">{value || '-'}</p> <p className="font-medium text-sm">{value || ''}</p>
</div> </div>
) )
} }

View file

@ -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 (
<div>
<span className="text-muted-foreground text-sm">{field.label}</span>
<p className="font-medium text-sm whitespace-pre-wrap">
{formatDisplayValue(field, value)}
</p>
</div>
)
}
const handleChange = (newVal: unknown) => onChange(field.key, newVal)
return (
<div>
<label className="text-sm text-muted-foreground">{field.label}</label>
<div className="mt-1">
{field.type === 'text' && (
<Input
value={(value as string) ?? ''}
onChange={(e) => handleChange(e.target.value)}
placeholder={field.placeholder}
className="h-8 text-sm"
/>
)}
{field.type === 'textarea' && (
<textarea
value={(value as string) ?? ''}
onChange={(e) => handleChange(e.target.value)}
placeholder={field.placeholder}
rows={3}
className="flex w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
/>
)}
{field.type === 'date' && (
<Input
type="date"
value={(value as string) ?? ''}
onChange={(e) => handleChange(e.target.value || null)}
className="h-8 text-sm"
/>
)}
{field.type === 'boolean' && (
<div className="flex items-center gap-2 h-8">
<Switch
checked={value === true}
onCheckedChange={(checked) => handleChange(checked)}
/>
<span className="text-sm">{value ? 'Ja' : 'Nein'}</span>
</div>
)}
{field.type === 'select' && field.options && (
<Select
value={(value as string) ?? ''}
onValueChange={(v) => handleChange(v || null)}
>
<SelectTrigger className="h-8 text-sm">
<SelectValue placeholder="Auswählen..." />
</SelectTrigger>
<SelectContent>
{field.options.map((opt) => (
<SelectItem key={opt.value} value={opt.value}>
{opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
)}
</div>
</div>
)
}

View file

@ -0,0 +1,95 @@
import type { Case } from '@/types'
export type FieldType = 'text' | 'date' | 'boolean' | 'select' | 'textarea'
export interface FieldConfig {
key: keyof Case
label: string
type: FieldType
colSpan?: 1 | 2
editableBy: 'admin' | 'all'
placeholder?: string
options?: { value: string; label: string }[]
}
export interface SectionConfig {
title: string
fields: FieldConfig[]
}
const ANREDE_OPTIONS = [
{ value: '', label: '' },
{ value: 'Herr', label: 'Herr' },
{ value: 'Frau', label: 'Frau' },
{ value: 'Divers', label: 'Divers' },
]
export const CASE_SECTIONS: SectionConfig[] = [
{
title: 'Persönliche Daten',
fields: [
{ key: 'anrede', label: 'Anrede', type: 'select', editableBy: 'admin', options: ANREDE_OPTIONS },
{ key: 'vorname', label: 'Vorname', type: 'text', editableBy: 'admin', placeholder: 'Vorname' },
{ key: 'nachname', label: 'Nachname', type: 'text', editableBy: 'admin', placeholder: 'Nachname' },
{ key: 'geburtsdatum', label: 'Geburtsdatum', type: 'date', editableBy: 'admin' },
{ key: 'kvnr', label: 'KVNR', type: 'text', editableBy: 'all', placeholder: 'z.B. A123456789' },
{ key: 'versicherung', label: 'Versicherung', type: 'text', editableBy: 'admin', placeholder: 'z.B. DAK' },
],
},
{
title: 'Kontakt',
fields: [
{ key: 'strasse', label: 'Straße', type: 'text', editableBy: 'admin', placeholder: 'Straße + Nr.' },
{ key: 'plz', label: 'PLZ', type: 'text', editableBy: 'admin', placeholder: 'PLZ' },
{ key: 'ort', label: 'Ort', type: 'text', editableBy: 'admin', placeholder: 'Ort' },
{ key: 'email', label: 'E-Mail', type: 'text', editableBy: 'admin', placeholder: 'E-Mail' },
{ key: 'telefonnummer', label: 'Telefon', type: 'text', editableBy: 'admin', placeholder: 'Telefonnummer' },
{ key: 'mobiltelefon', label: 'Mobil', type: 'text', editableBy: 'admin', placeholder: 'Mobilnummer' },
{ key: 'ansprechpartner', label: 'Ansprechpartner', type: 'text', editableBy: 'admin', colSpan: 2, placeholder: 'Ansprechpartner' },
],
},
{
title: 'Falldetails',
fields: [
{ key: 'kurzbeschreibung', label: 'Kurzbeschreibung', type: 'textarea', editableBy: 'admin', colSpan: 2 },
{ key: 'fragestellung', label: 'Fragestellung', type: 'textarea', editableBy: 'admin', colSpan: 2 },
{ key: 'kommentar', label: 'Kommentar', type: 'textarea', editableBy: 'admin', colSpan: 2 },
{ key: 'sonstiges', label: 'Sonstiges', type: 'textarea', editableBy: 'admin', colSpan: 2 },
],
},
{
title: 'Unterlagen',
fields: [
{ key: 'unterlagen', label: 'Unterlagen vorhanden', type: 'boolean', editableBy: 'admin' },
{ key: 'erhalten', label: 'Erhalten', type: 'boolean', editableBy: 'admin' },
{ key: 'unterlagen_verschickt', label: 'Verschickt am', type: 'date', editableBy: 'admin' },
{ key: 'unterlagen_erhalten', label: 'Erhalten am', type: 'date', editableBy: 'admin' },
{ key: 'unterlagen_an_gutachter', label: 'An Gutachter am', type: 'date', editableBy: 'admin' },
],
},
{
title: 'Gutachten',
fields: [
{ key: 'gutachten', label: 'Gutachten vorhanden', type: 'boolean', editableBy: 'admin' },
{ key: 'gutachter', label: 'Gutachter', type: 'text', editableBy: 'admin', placeholder: 'Name des Gutachters' },
{ key: 'gutachten_erstellt', label: 'Erstellt am', type: 'date', editableBy: 'admin' },
{ key: 'gutachten_versendet', label: 'Versendet am', type: 'date', editableBy: 'admin' },
{ key: 'schweigepflicht', label: 'Schweigepflichtentbindung', type: 'boolean', editableBy: 'admin' },
],
},
{
title: 'Status',
fields: [
{ key: 'ablehnung', label: 'Ablehnung', type: 'boolean', editableBy: 'admin' },
{ key: 'abbruch', label: 'Abbruch', type: 'boolean', editableBy: 'admin' },
{ key: 'abbruch_datum', label: 'Abbruch-Datum', type: 'date', editableBy: 'admin' },
],
},
{
title: 'Abrechnung',
fields: [
{ key: 'abgerechnet', label: 'Abgerechnet', type: 'boolean', editableBy: 'admin' },
{ key: 'abrechnung_datum', label: 'Abrechnungsdatum', type: 'date', editableBy: 'admin' },
],
},
]

View file

@ -0,0 +1,165 @@
import { useState, useCallback, useEffect } from 'react'
import api from '@/services/api'
import type { Case } from '@/types'
import { CASE_SECTIONS, type FieldConfig } from './fieldConfig'
type FormValues = Record<string, unknown>
interface UseInlineEditReturn {
editing: boolean
formValues: FormValues
saving: boolean
error: string
success: boolean
startEditing: () => void
cancelEditing: () => void
saveAll: () => Promise<Case | null>
setFieldValue: (key: string, value: unknown) => void
canEditField: (field: FieldConfig, isAdmin: boolean) => boolean
isDirty: boolean
}
function extractFormValues(caseData: Case): FormValues {
const values: FormValues = {}
for (const section of CASE_SECTIONS) {
for (const field of section.fields) {
values[field.key] = caseData[field.key]
}
}
return values
}
function getDirtyFields(original: FormValues, current: FormValues): FormValues {
const dirty: FormValues = {}
for (const key of Object.keys(current)) {
const orig = original[key] ?? null
const curr = current[key] ?? null
if (orig !== curr) {
dirty[key] = curr
}
}
return dirty
}
export function useInlineEdit(
caseData: Case,
onSaved: (updated: Case) => void,
): UseInlineEditReturn {
const [editing, setEditing] = useState(false)
const [formValues, setFormValues] = useState<FormValues>(() => extractFormValues(caseData))
const [originalValues, setOriginalValues] = useState<FormValues>(() => extractFormValues(caseData))
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
const [success, setSuccess] = useState(false)
// Reset when case changes
useEffect(() => {
const vals = extractFormValues(caseData)
setFormValues(vals)
setOriginalValues(vals)
setEditing(false)
setError('')
setSuccess(false)
}, [caseData.id, caseData.updated_at])
const startEditing = useCallback(() => {
setFormValues(extractFormValues(caseData))
setOriginalValues(extractFormValues(caseData))
setEditing(true)
setError('')
setSuccess(false)
}, [caseData])
const cancelEditing = useCallback(() => {
setFormValues(extractFormValues(caseData))
setEditing(false)
setError('')
setSuccess(false)
}, [caseData])
const setFieldValue = useCallback((key: string, value: unknown) => {
setFormValues((prev) => ({ ...prev, [key]: value }))
setSuccess(false)
}, [])
const isDirty = Object.keys(getDirtyFields(originalValues, formValues)).length > 0
const saveAll = useCallback(async (): Promise<Case | null> => {
const dirty = getDirtyFields(originalValues, formValues)
if (Object.keys(dirty).length === 0) {
setEditing(false)
return null
}
setSaving(true)
setError('')
setSuccess(false)
try {
let lastResult: Case | null = null
// Split: KVNR goes to its own endpoint (accessible by all users)
if ('kvnr' in dirty) {
const kvnrVal = dirty.kvnr as string | null
const res = await api.put<Case>(`/cases/${caseData.id}/kvnr`, {
kvnr: kvnrVal?.trim() || null,
})
lastResult = res.data
delete dirty.kvnr
}
// Remaining fields go to the admin-only general update endpoint
if (Object.keys(dirty).length > 0) {
// Convert empty strings to null for optional text fields
const payload: FormValues = {}
for (const [key, value] of Object.entries(dirty)) {
if (typeof value === 'string' && value.trim() === '') {
payload[key] = null
} else {
payload[key] = value
}
}
const res = await api.put<Case>(`/cases/${caseData.id}`, payload)
lastResult = res.data
}
if (lastResult) {
onSaved(lastResult)
setSuccess(true)
setEditing(false)
}
return lastResult
} catch (err: unknown) {
const msg =
err instanceof Error ? err.message : 'Fehler beim Speichern.'
setError(msg)
return null
} finally {
setSaving(false)
}
}, [caseData.id, formValues, originalValues, onSaved])
const canEditField = useCallback(
(field: FieldConfig, isAdmin: boolean): boolean => {
if (!editing) return false
if (field.editableBy === 'all') return true
return isAdmin
},
[editing],
)
return {
editing,
formValues,
saving,
error,
success,
startEditing,
cancelEditing,
saveAll,
setFieldValue,
canEditField,
isDirty,
}
}

View file

@ -44,17 +44,37 @@ export interface Case {
versicherung: string versicherung: string
icd: string | null icd: string | null
fallgruppe: string fallgruppe: string
strasse: string | null
plz: string | null
ort: string | null
email: string | null
ansprechpartner: string | null
telefonnummer: string | null
mobiltelefon: string | null
unterlagen: boolean unterlagen: boolean
unterlagen_verschickt: string | null
erhalten: boolean | null
unterlagen_erhalten: string | null
unterlagen_an_gutachter: string | null
gutachten: boolean gutachten: boolean
gutachter: string | null gutachter: string | null
gutachten_typ: string | null gutachten_erstellt: string | null
therapieaenderung: string | null gutachten_versendet: string | null
schweigepflicht: boolean
ablehnung: boolean ablehnung: boolean
abbruch: boolean abbruch: boolean
abbruch_datum: string | null
gutachten_typ: string | null
therapieaenderung: string | null
ta_diagnosekorrektur: boolean
ta_unterversorgung: boolean
ta_uebertherapie: boolean
kurzbeschreibung: string | null kurzbeschreibung: string | null
fragestellung: string | null fragestellung: string | null
kommentar: string | null kommentar: string | null
sonstiges: string | null
abgerechnet: boolean abgerechnet: boolean
abrechnung_datum: string | null
import_source: string | null import_source: string | null
imported_at: string imported_at: string
updated_at: string updated_at: string