mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 16:03:41 +00:00
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:
parent
2f0a556371
commit
eb39346f02
2 changed files with 91 additions and 12 deletions
|
|
@ -9,7 +9,7 @@
|
||||||
## Mittlere Priorität
|
## 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.
|
- [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.
|
- [ ] **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.
|
- [x] **Dark Mode Toggle** — ✅ Bereits implementiert: Sun/Moon-Toggle im Header, useTheme Hook aktiv.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||||
|
import type { KeyboardEvent } from 'react'
|
||||||
import {
|
import {
|
||||||
Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X, Info,
|
Search, ChevronLeft, ChevronRight, Save, CheckCircle, XCircle, Pencil, X, Info, Loader2,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import type { Case } from '@/types'
|
import type { Case } from '@/types'
|
||||||
import { useAuth } from '@/context/AuthContext'
|
import { useAuth } from '@/context/AuthContext'
|
||||||
|
|
@ -18,6 +19,8 @@ import {
|
||||||
Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription,
|
Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription,
|
||||||
} from '@/components/ui/sheet'
|
} from '@/components/ui/sheet'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
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 { Alert, AlertDescription } from '@/components/ui/alert'
|
||||||
import {
|
import {
|
||||||
Tooltip, TooltipContent, TooltipTrigger,
|
Tooltip, TooltipContent, TooltipTrigger,
|
||||||
|
|
@ -67,6 +70,7 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
const [perPage] = useState(50)
|
const [perPage] = useState(50)
|
||||||
const [selectedCase, setSelectedCase] = useState<Case | null>(null)
|
const [selectedCase, setSelectedCase] = useState<Case | null>(null)
|
||||||
const [sheetOpen, setSheetOpen] = useState(false)
|
const [sheetOpen, setSheetOpen] = useState(false)
|
||||||
|
const [batchMode, setBatchMode] = useState(false)
|
||||||
|
|
||||||
// Debounce search
|
// Debounce search
|
||||||
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
@ -110,14 +114,24 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<div>
|
<div className="flex items-start justify-between">
|
||||||
<h1 className="text-2xl font-bold">
|
<div>
|
||||||
{pendingIcdOnly ? 'ICD-Eingabe' : 'Fälle'}
|
<h1 className="text-2xl font-bold">
|
||||||
</h1>
|
{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 && (
|
{pendingIcdOnly && (
|
||||||
<p className="text-sm text-muted-foreground mt-1">
|
<div className="flex items-center gap-2">
|
||||||
Fälle, die noch einen ICD-10-Diagnosecode benötigen. Klicken Sie auf einen Fall, um den Code einzugeben.
|
<Switch id="batch-mode" checked={batchMode} onCheckedChange={setBatchMode} />
|
||||||
</p>
|
<Label htmlFor="batch-mode" className="text-sm cursor-pointer">Batch-Modus</Label>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -244,8 +258,8 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
{data.items.map((c) => (
|
{data.items.map((c) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={c.id}
|
key={c.id}
|
||||||
className="cursor-pointer"
|
className={pendingIcdOnly && batchMode ? '' : 'cursor-pointer'}
|
||||||
onClick={() => openDetail(c)}
|
onClick={() => { if (!(pendingIcdOnly && batchMode)) openDetail(c) }}
|
||||||
>
|
>
|
||||||
<TableCell className="font-mono text-sm">
|
<TableCell className="font-mono text-sm">
|
||||||
{c.fall_id || '-'}
|
{c.fall_id || '-'}
|
||||||
|
|
@ -262,7 +276,9 @@ export function CasesPage({ pendingIcdOnly = false }: CasesPageProps) {
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<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="font-mono text-sm">{c.icd}</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-muted-foreground">-</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 }) {
|
function StatusBadges({ c }: { c: Case }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue