feat: add batch ICD input mode to ICD page

Adds a Switch toggle for "Batch-Modus" on the ICD page that enables
inline ICD code entry directly in the table. Features: Enter to save,
Escape to cancel, Tab to navigate, visual feedback (green/red border,
spinner, checkmark/error icon with tooltip).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-28 14:07:33 +00:00
parent 2f0a556371
commit eb39346f02
2 changed files with 91 additions and 12 deletions

View file

@ -9,7 +9,7 @@
## Mittlere Priorität
- [x] **Benachrichtigungs-Center (Bell-Icon)** — ✅ Bereits implementiert: Bell-Icon im Header mit Badge-Counter, Popover-Dropdown, Mark-as-read, 60s-Polling. War schon in Header.tsx vorhanden.
- [ ] **Dashboard: Vorjahresvergleich bei KPIs** — Prozentuale Veränderung zum Vorjahr neben den KPI-Zahlen (z.B. "+12% vs. 2025"). `vorjahr_service` existiert im Backend.
- [x] **Dashboard: Vorjahresvergleich bei KPIs** — ✅ Implementiert: prev_kpis im Dashboard-Endpoint, KpiCards zeigen farbige Trend-Indikatoren (+X% grün, -X% rot) mit Vorjahresvergleich.
- [ ] **Batch-ICD-Eingabe** — Inline-Tabelle auf der ICD-Seite mit direkter ICD-Eingabe pro Zeile statt Einzelklick auf jeden Fall.
- [x] **Dark Mode Toggle** — ✅ Bereits implementiert: Sun/Moon-Toggle im Header, useTheme Hook aktiv.

View file

@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import type { KeyboardEvent } from 'react'
import {
Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X, Info,
Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X, Info, Loader2,
} from 'lucide-react'
import type { Case } from '@/types'
import { useAuth } from '@/context/AuthContext'
@ -18,6 +19,8 @@ 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 { Alert, AlertDescription } from '@/components/ui/alert'
import {
Tooltip, TooltipContent, TooltipTrigger,
@ -67,6 +70,7 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
const [perPage] = useState(50)
const [selectedCase, setSelectedCase] = useState<Case | null>(null)
const [sheetOpen, setSheetOpen] = useState(false)
const [batchMode, setBatchMode] = useState(false)
// Debounce search
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
@ -110,16 +114,26 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
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">
Fälle, die noch einen ICD-10-Diagnosecode benötigen. Klicken Sie auf einen Fall, um den Code einzugeben.
{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 && (
@ -244,8 +258,8 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
{data.items.map((c) => (
<TableRow
key={c.id}
className="cursor-pointer"
onClick={() => openDetail(c)}
className={pendingIcdOnly && batchMode ? '' : 'cursor-pointer'}
onClick={() => { if (!(pendingIcdOnly && batchMode)) openDetail(c) }}
>
<TableCell className="font-mono text-sm">
{c.fall_id || '-'}
@ -262,7 +276,9 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
</Badge>
</TableCell>
<TableCell>
{c.icd ? (
{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>
@ -329,6 +345,69 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
)
}
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">