feat: add inline preview for Wochenübersicht reports

- Backend stores per-KW summary in report_data (count, ICD status, FG split)
- WochenuebersichtViewer component shows expandable KW table with totals
- Expand/collapse pattern matching ReportsPage (ChevronRight/Down, useReportData)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-27 15:26:20 +00:00
parent efeb619b06
commit 27b3810250
2 changed files with 181 additions and 33 deletions

View file

@ -360,11 +360,29 @@ def generate_wochenuebersicht(
) )
.first() .first()
) )
# Build per-KW summary for frontend preview
fg1, fg2 = fallgruppen
fg1_label, fg2_label = type_cfg["fg_labels"]
weeks_summary = []
for kw in sorted(cases_by_kw.keys(), reverse=True):
kw_cases = cases_by_kw[kw]
weeks_summary.append({
"kw": kw,
"count": len(kw_cases),
"with_icd": sum(1 for c in kw_cases if c.icd),
"fg1_count": sum(1 for c in kw_cases if c.fallgruppe == fg1),
"fg2_count": sum(1 for c in kw_cases if c.fallgruppe == fg2),
"gutachten": sum(1 for c in kw_cases if c.gutachten),
})
report_data = { report_data = {
"export_type": export_type, "export_type": export_type,
"kw_von": kw_von, "kw_von": kw_von,
"kw_bis": kw_bis, "kw_bis": kw_bis,
"case_count": len(cases), "case_count": len(cases),
"fg1_label": fg1_label,
"fg2_label": fg2_label,
"weeks": weeks_summary,
} }
if report: if report:
report.report_date = date.today() report.report_date = date.today()

View file

@ -1,9 +1,10 @@
import { useCallback, useState } from 'react' import { Fragment, useCallback, useState } from 'react'
import { Download, FileSpreadsheet, Loader2, Plus, Upload } from 'lucide-react' import { ChevronDown, ChevronRight, Download, FileSpreadsheet, Loader2, Plus, Upload } from 'lucide-react'
import api from '@/services/api' import api from '@/services/api'
import { useAuth } from '@/context/AuthContext' import { useAuth } from '@/context/AuthContext'
import { import {
useReports, useReports,
useReportData,
useGenerateWochenuebersicht, useGenerateWochenuebersicht,
useDeleteReports, useDeleteReports,
useUploadWochenuebersichtIcd, useUploadWochenuebersichtIcd,
@ -16,7 +17,7 @@ import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow, Table, TableBody, TableCell, TableFooter, TableHead, TableHeader, TableRow,
} from '@/components/ui/table' } from '@/components/ui/table'
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'
@ -86,6 +87,10 @@ export function WochenuebersichtPage() {
const [uploadSuccess, setUploadSuccess] = useState('') const [uploadSuccess, setUploadSuccess] = useState('')
const uploadMutation = useUploadWochenuebersichtIcd() const uploadMutation = useUploadWochenuebersichtIcd()
// Inline expansion state
const [expandedId, setExpandedId] = useState<number | null>(null)
const { data: reportData, isLoading: reportDataLoading } = useReportData(expandedId)
// Drag state // Drag state
const [isDragOver, setIsDragOver] = useState(false) const [isDragOver, setIsDragOver] = useState(false)
@ -296,38 +301,72 @@ export function WochenuebersichtPage() {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{reports.map((r) => ( {reports.map((r) => {
<TableRow key={r.id}> const isExpanded = expandedId === r.id
<TableCell>{formatDate(r.report_date)}</TableCell> return (
<TableCell>{r.jahr}</TableCell> <Fragment key={r.id}>
<TableCell>KW {r.kw}</TableCell> <TableRow
<TableCell>{REPORT_TYPE_LABELS[r.report_type] ?? r.report_type}</TableCell> className="cursor-pointer hover:bg-muted/50"
<TableCell>{formatDateTime(r.generated_at)}</TableCell> onClick={() => setExpandedId(isExpanded ? null : r.id)}
<TableCell className="text-right">
<Button
variant="outline"
size="sm"
onClick={() => downloadReport(r.id)}
> >
<Download className="mr-1.5 h-4 w-4" /> <TableCell>
Download <span className="inline-flex items-center gap-1.5">
</Button> {isExpanded
{isAdmin && ( ? <ChevronDown className="size-4 text-muted-foreground" />
<Button : <ChevronRight className="size-4 text-muted-foreground" />}
variant="ghost" {formatDate(r.report_date)}
size="sm" </span>
className="ml-2 text-destructive" </TableCell>
onClick={async () => { <TableCell>{r.jahr}</TableCell>
await deleteMutation.mutateAsync([r.id]) <TableCell>KW {r.kw}</TableCell>
}} <TableCell>{REPORT_TYPE_LABELS[r.report_type] ?? r.report_type}</TableCell>
disabled={deleteMutation.isPending} <TableCell>{formatDateTime(r.generated_at)}</TableCell>
> <TableCell className="text-right" onClick={(e) => e.stopPropagation()}>
Löschen <Button
</Button> variant="outline"
size="sm"
onClick={() => downloadReport(r.id)}
>
<Download className="mr-1.5 h-4 w-4" />
Download
</Button>
{isAdmin && (
<Button
variant="ghost"
size="sm"
className="ml-2 text-destructive"
onClick={async () => {
await deleteMutation.mutateAsync([r.id])
}}
disabled={deleteMutation.isPending}
>
Löschen
</Button>
)}
</TableCell>
</TableRow>
{isExpanded && (
<TableRow>
<TableCell colSpan={6} className="p-0">
{reportDataLoading ? (
<div className="p-6">
<Skeleton className="h-48 w-full" />
</div>
) : reportData ? (
<div className="p-4 border-t bg-muted/20">
<WochenuebersichtViewer data={reportData} />
</div>
) : (
<div className="p-6 text-center text-muted-foreground">
Keine Vorschaudaten verfügbar.
</div>
)}
</TableCell>
</TableRow>
)} )}
</TableCell> </Fragment>
</TableRow> )
))} })}
</TableBody> </TableBody>
</Table> </Table>
) : ( ) : (
@ -445,3 +484,94 @@ export function WochenuebersichtPage() {
</div> </div>
) )
} }
// ---------------------------------------------------------------------------
// Wochenübersicht Viewer (inline preview)
// ---------------------------------------------------------------------------
interface WeekSummary {
kw: number
count: number
with_icd: number
fg1_count: number
fg2_count: number
gutachten: number
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function WochenuebersichtViewer({ data }: { data: Record<string, any> }) {
const weeks: WeekSummary[] = data?.weeks ?? []
const fg1Label: string = data?.fg1_label ?? 'FG1'
const fg2Label: string = data?.fg2_label ?? 'FG2'
const kwVon: number = data?.kw_von ?? '?'
const kwBis: number = data?.kw_bis ?? '?'
const totalCases: number = data?.case_count ?? 0
if (!weeks.length) {
return (
<p className="py-4 text-center text-muted-foreground">
Keine Vorschaudaten verfügbar.
</p>
)
}
const totals = weeks.reduce(
(acc, w) => ({
count: acc.count + w.count,
with_icd: acc.with_icd + w.with_icd,
fg1: acc.fg1 + w.fg1_count,
fg2: acc.fg2 + w.fg2_count,
gutachten: acc.gutachten + w.gutachten,
}),
{ count: 0, with_icd: 0, fg1: 0, fg2: 0, gutachten: 0 },
)
return (
<div className="space-y-3">
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<span>KW {kwVon}{kwBis}</span>
<span>{totalCases} Fälle gesamt</span>
<span>{totals.with_icd} mit ICD</span>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>KW</TableHead>
<TableHead className="text-right">Fälle</TableHead>
<TableHead className="text-right">{fg1Label}</TableHead>
<TableHead className="text-right">{fg2Label}</TableHead>
<TableHead className="text-right">Gutachten</TableHead>
<TableHead className="text-right">mit ICD</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{weeks.map((w) => (
<TableRow key={w.kw}>
<TableCell>KW {w.kw}</TableCell>
<TableCell className="text-right">{w.count}</TableCell>
<TableCell className="text-right">{w.fg1_count}</TableCell>
<TableCell className="text-right">{w.fg2_count}</TableCell>
<TableCell className="text-right">{w.gutachten}</TableCell>
<TableCell className="text-right">
{w.with_icd}/{w.count}
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TableCell className="font-bold">Gesamt</TableCell>
<TableCell className="text-right font-bold">{totals.count}</TableCell>
<TableCell className="text-right font-bold">{totals.fg1}</TableCell>
<TableCell className="text-right font-bold">{totals.fg2}</TableCell>
<TableCell className="text-right font-bold">{totals.gutachten}</TableCell>
<TableCell className="text-right font-bold">
{totals.with_icd}/{totals.count}
</TableCell>
</TableRow>
</TableFooter>
</Table>
</div>
)
}