dak.c2s/frontend/src/components/ReportViewer.tsx
CCS Admin bbcb0c489a fix: replace Unicode escape sequences with UTF-8 characters in ReportViewer
Unicode escapes like \u00e4 render as literal text in JSX content
instead of the intended German umlauts (ä, ü, Ü).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:19:37 +00:00

418 lines
13 KiB
TypeScript

import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import {
Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow,
} from '@/components/ui/table'
const FALLGRUPPEN_LABELS: Record<string, string> = {
onko: 'Onkologie',
kardio: 'Kardiologie',
intensiv: 'Intensivmedizin',
galle: 'Gallenblase',
sd: 'Schilddrüse',
}
const FALLGRUPPEN_KEYS = ['onko', 'kardio', 'intensiv', 'galle', 'sd'] as const
function fmt(n: number): string {
return n.toLocaleString('de-DE')
}
function NoData() {
return (
<p className="py-8 text-center text-muted-foreground">Keine Daten verfügbar.</p>
)
}
// ---------------------------------------------------------------------------
// Sheet 1 — KW gesamt
// ---------------------------------------------------------------------------
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function Sheet1({ data }: { data: any }) {
if (!data?.weekly?.length) return <NoData />
const COLS = [
{ key: 'erstberatungen', label: 'Erstberatungen' },
{ key: 'unterlagen', label: 'Unterlagen' },
{ key: 'ablehnungen', label: 'Ablehnungen' },
{ key: 'keine_rm', label: 'Keine Rückmeldung' },
{ key: 'gutachten', label: 'Gutachten' },
] as const
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rows = (data.weekly as any[]).filter((row) =>
COLS.some((c) => (row[c.key] ?? 0) !== 0),
)
const totals = COLS.reduce(
(acc, c) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
acc[c.key] = rows.reduce((s: number, r: any) => s + (r[c.key] ?? 0), 0)
return acc
},
{} as Record<string, number>,
)
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>KW</TableHead>
{COLS.map((c) => (
<TableHead key={c.key} className="text-right">{c.label}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row) => (
<TableRow key={row.kw}>
<TableCell>KW {row.kw}</TableCell>
{COLS.map((c) => (
<TableCell key={c.key} className="text-right">{fmt(row[c.key] ?? 0)}</TableCell>
))}
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell className="font-bold">Gesamt</TableCell>
{COLS.map((c) => (
<TableCell key={c.key} className="text-right font-bold">{fmt(totals[c.key])}</TableCell>
))}
</TableRow>
</TableFooter>
</Table>
)
}
// ---------------------------------------------------------------------------
// Sheet 2 — Fachgebiete
// ---------------------------------------------------------------------------
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function Sheet2({ data, fallgruppen }: { data: any; fallgruppen: readonly string[] }) {
if (!data?.weekly?.length) return <NoData />
const SUB_COLS = ['anzahl', 'gutachten', 'keine_rm'] as const
const SUB_LABELS: Record<string, string> = {
anzahl: 'Anzahl',
gutachten: 'Gutachten',
keine_rm: 'Keine RM',
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isRowEmpty = (row: any) =>
fallgruppen.every((fg) =>
SUB_COLS.every((sc) => (row[fg]?.[sc] ?? 0) === 0),
)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rows = (data.weekly as any[]).filter((row) => !isRowEmpty(row))
const totals = fallgruppen.reduce(
(acc, fg) => {
acc[fg] = SUB_COLS.reduce(
(sub, sc) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sub[sc] = rows.reduce((s: number, r: any) => s + (r[fg]?.[sc] ?? 0), 0)
return sub
},
{} as Record<string, number>,
)
return acc
},
{} as Record<string, Record<string, number>>,
)
return (
<Table>
<TableHeader>
<TableRow>
<TableHead rowSpan={2} className="align-bottom">KW</TableHead>
{fallgruppen.map((fg) => (
<TableHead key={fg} colSpan={SUB_COLS.length} className="text-center border-l">
{FALLGRUPPEN_LABELS[fg] ?? fg}
</TableHead>
))}
</TableRow>
<TableRow>
{fallgruppen.map((fg) =>
SUB_COLS.map((sc) => (
<TableHead key={`${fg}-${sc}`} className="text-right border-l first:border-l">
{SUB_LABELS[sc]}
</TableHead>
)),
)}
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row) => (
<TableRow key={row.kw}>
<TableCell>KW {row.kw}</TableCell>
{fallgruppen.map((fg) =>
SUB_COLS.map((sc) => (
<TableCell key={`${fg}-${sc}`} className="text-right border-l">
{fmt(row[fg]?.[sc] ?? 0)}
</TableCell>
)),
)}
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell className="font-bold">Gesamt</TableCell>
{fallgruppen.map((fg) =>
SUB_COLS.map((sc) => (
<TableCell key={`${fg}-${sc}`} className="text-right font-bold border-l">
{fmt(totals[fg][sc])}
</TableCell>
)),
)}
</TableRow>
</TableFooter>
</Table>
)
}
// ---------------------------------------------------------------------------
// Sheet 3 — Gutachten
// ---------------------------------------------------------------------------
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function Sheet3({ data, fallgruppen }: { data: any; fallgruppen: readonly string[] }) {
if (!data?.weekly?.length) return <NoData />
const SUB_COLS = ['gutachten', 'alternative', 'bestaetigung'] as const
const SUB_LABELS: Record<string, string> = {
gutachten: 'Gutachten',
alternative: 'Alternative',
bestaetigung: 'Bestätigung',
}
const GROUP_KEYS = ['gesamt', ...fallgruppen] as const
const GROUP_LABELS: Record<string, string> = {
gesamt: 'Gesamt',
...FALLGRUPPEN_LABELS,
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const isRowEmpty = (row: any) =>
GROUP_KEYS.every((grp) =>
SUB_COLS.every((sc) => (row[grp]?.[sc] ?? 0) === 0),
)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rows = (data.weekly as any[]).filter((row) => !isRowEmpty(row))
const totals = GROUP_KEYS.reduce(
(acc, grp) => {
acc[grp] = SUB_COLS.reduce(
(sub, sc) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sub[sc] = rows.reduce((s: number, r: any) => s + (r[grp]?.[sc] ?? 0), 0)
return sub
},
{} as Record<string, number>,
)
return acc
},
{} as Record<string, Record<string, number>>,
)
return (
<Table>
<TableHeader>
<TableRow>
<TableHead rowSpan={2} className="align-bottom">KW</TableHead>
{GROUP_KEYS.map((grp) => (
<TableHead key={grp} colSpan={SUB_COLS.length} className="text-center border-l">
{GROUP_LABELS[grp] ?? grp}
</TableHead>
))}
</TableRow>
<TableRow>
{GROUP_KEYS.map((grp) =>
SUB_COLS.map((sc) => (
<TableHead key={`${grp}-${sc}`} className="text-right border-l">
{SUB_LABELS[sc]}
</TableHead>
)),
)}
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row) => (
<TableRow key={row.kw}>
<TableCell>KW {row.kw}</TableCell>
{GROUP_KEYS.map((grp) =>
SUB_COLS.map((sc) => (
<TableCell key={`${grp}-${sc}`} className="text-right border-l">
{fmt(row[grp]?.[sc] ?? 0)}
</TableCell>
)),
)}
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell className="font-bold">Gesamt</TableCell>
{GROUP_KEYS.map((grp) =>
SUB_COLS.map((sc) => (
<TableCell key={`${grp}-${sc}`} className="text-right font-bold border-l">
{fmt(totals[grp][sc])}
</TableCell>
)),
)}
</TableRow>
</TableFooter>
</Table>
)
}
// ---------------------------------------------------------------------------
// Sheet 4 — Therapieänderungen
// ---------------------------------------------------------------------------
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function Sheet4({ data }: { data: any }) {
if (!data?.weekly?.length) return <NoData />
const COLS = [
{ key: 'gutachten', label: 'Gutachten' },
{ key: 'ta_ja', label: 'Therapieänderung Ja' },
{ key: 'ta_nein', label: 'Therapieänderung Nein' },
{ key: 'diagnosekorrektur', label: 'Diagnosekorrektur' },
{ key: 'unterversorgung', label: 'Unterversorgung' },
{ key: 'uebertherapie', label: 'Übertherapie' },
] as const
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rows = (data.weekly as any[]).filter((row) =>
COLS.some((c) => (row[c.key] ?? 0) !== 0),
)
const totals = COLS.reduce(
(acc, c) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
acc[c.key] = rows.reduce((s: number, r: any) => s + (r[c.key] ?? 0), 0)
return acc
},
{} as Record<string, number>,
)
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>KW</TableHead>
{COLS.map((c) => (
<TableHead key={c.key} className="text-right">{c.label}</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row) => (
<TableRow key={row.kw}>
<TableCell>KW {row.kw}</TableCell>
{COLS.map((c) => (
<TableCell key={c.key} className="text-right">{fmt(row[c.key] ?? 0)}</TableCell>
))}
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell className="font-bold">Gesamt</TableCell>
{COLS.map((c) => (
<TableCell key={c.key} className="text-right font-bold">{fmt(totals[c.key])}</TableCell>
))}
</TableRow>
</TableFooter>
</Table>
)
}
// ---------------------------------------------------------------------------
// Sheet 5 — ICD onko
// ---------------------------------------------------------------------------
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function Sheet5({ data }: { data: any }) {
if (!data?.icd_codes?.length) return <NoData />
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const rows = data.icd_codes as any[]
const total = rows.reduce((s: number, r) => s + (r.count ?? 0), 0)
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>ICD-Code</TableHead>
<TableHead className="text-right">Anzahl</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rows.map((row) => (
<TableRow key={row.icd}>
<TableCell>{row.icd}</TableCell>
<TableCell className="text-right">{fmt(row.count ?? 0)}</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell className="font-bold">Gesamt</TableCell>
<TableCell className="text-right font-bold">{fmt(total)}</TableCell>
</TableRow>
</TableFooter>
</Table>
)
}
// ---------------------------------------------------------------------------
// ReportViewer — Main Component
// ---------------------------------------------------------------------------
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function ReportViewer({ data }: { data: Record<string, any> }) {
const fallgruppen: readonly string[] = data?.fallgruppen ?? FALLGRUPPEN_KEYS
const hasOnko = fallgruppen.includes('onko')
return (
<Tabs defaultValue="sheet1" className="w-full">
<TabsList className="w-full justify-start">
<TabsTrigger value="sheet1">KW gesamt</TabsTrigger>
<TabsTrigger value="sheet2">Fachgebiete</TabsTrigger>
<TabsTrigger value="sheet3">Gutachten</TabsTrigger>
<TabsTrigger value="sheet4">Therapieänderungen</TabsTrigger>
{hasOnko && <TabsTrigger value="sheet5">ICD onko</TabsTrigger>}
</TabsList>
<TabsContent value="sheet1">
<Sheet1 data={data?.sheet1} />
</TabsContent>
<TabsContent value="sheet2">
<Sheet2 data={data?.sheet2} fallgruppen={fallgruppen} />
</TabsContent>
<TabsContent value="sheet3">
<Sheet3 data={data?.sheet3} fallgruppen={fallgruppen} />
</TabsContent>
<TabsContent value="sheet4">
<Sheet4 data={data?.sheet4} />
</TabsContent>
{hasOnko && (
<TabsContent value="sheet5">
<Sheet5 data={data?.sheet5} />
</TabsContent>
)}
</Tabs>
)
}