diff --git a/backend/app/api/reports.py b/backend/app/api/reports.py index c6e122e..dc98739 100644 --- a/backend/app/api/reports.py +++ b/backend/app/api/reports.py @@ -94,6 +94,27 @@ def top_icd( return {"items": items} +@router.get("/gutachten-statistik") +def gutachten_statistik( + jahr: int | None = Query(None), + db: Session = Depends(get_db), + user: User = Depends(get_current_user), +): + """Return combined gutachten statistics (KPIs + weekly breakdowns). + + Defaults to the current ISO year if *jahr* is not provided. + Accessible to both admin and dak_mitarbeiter users. + """ + if not jahr: + from app.utils.kw_utils import date_to_jahr + + jahr = date_to_jahr(date.today()) + + from app.services.report_service import calculate_gutachten_statistik + + return calculate_gutachten_statistik(db, jahr) + + @router.get("/weekly/{jahr}/{kw}") def weekly_report( jahr: int, diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py index 69fdc2f..43a06e1 100644 --- a/backend/app/services/report_service.py +++ b/backend/app/services/report_service.py @@ -692,6 +692,107 @@ def calculate_top_icd(db: Session, jahr: int, limit: int = 10) -> list[dict]: return [{"icd": row.icd_code, "count": int(row.cnt)} for row in rows] +# --------------------------------------------------------------------------- +# Dashboard: Gutachten-Statistik (combined sheet3 + sheet4 + KPIs) +# --------------------------------------------------------------------------- + +def calculate_gutachten_statistik(db: Session, jahr: int) -> dict: + """Return combined gutachten statistics for the statistics page. + + Combines: + - KPI aggregates (total, bestätigung, alternative, uncodiert, therapy changes) + - Sheet 3 weekly data (gutachten types per KW) + - Sheet 4 weekly data (therapy changes per KW) + + Returns:: + + { + "kpis": { + "total_gutachten": int, + "bestaetigung": int, + "alternative": int, + "uncodiert": int, + "ta_ja": int, + "ta_nein": int, + "diagnosekorrektur": int, + "unterversorgung": int, + "uebertherapie": int, + }, + "gutachten_weekly": [{"kw": 1, "bestaetigung": X, "alternative": X}, ...], + "therapie_weekly": [{"kw": 1, "ta_ja": X, "ta_nein": X, ...}, ...], + } + """ + v_filter = Case.versicherung == settings.VERSICHERUNG_FILTER + + # KPI aggregates + total_gutachten = ( + db.query(func.count(Case.id)) + .filter(v_filter, Case.jahr == jahr, Case.gutachten == True) # noqa: E712 + .scalar() or 0 + ) + typ_rows = ( + db.query(Case.gutachten_typ, func.count(Case.id).label("cnt")) + .filter(v_filter, Case.jahr == jahr, Case.gutachten == True) # noqa: E712 + .group_by(Case.gutachten_typ) + .all() + ) + bestaetigung = 0 + alternative = 0 + uncodiert = 0 + for row in typ_rows: + cnt = _int(row.cnt) + if row.gutachten_typ == "Bestätigung": + bestaetigung = cnt + elif row.gutachten_typ == "Alternative": + alternative = cnt + else: + uncodiert = cnt + + # Therapy change aggregates + ta_row = ( + db.query( + func.sum((Case.therapieaenderung == "Ja").cast(Integer)).label("ta_ja"), + func.sum((Case.therapieaenderung == "Nein").cast(Integer)).label("ta_nein"), + func.sum(Case.ta_diagnosekorrektur.cast(Integer)).label("dk"), + func.sum(Case.ta_unterversorgung.cast(Integer)).label("uv"), + func.sum(Case.ta_uebertherapie.cast(Integer)).label("ut"), + ) + .filter(v_filter, Case.jahr == jahr, Case.gutachten == True) # noqa: E712 + .first() + ) + + # Sheet 3 weekly (simplified: gesamt only) + sheet3 = calculate_sheet3_data(db, jahr) + gutachten_weekly = [ + { + "kw": w["kw"], + "bestaetigung": w["gesamt"]["bestaetigung"], + "alternative": w["gesamt"]["alternative"], + } + for w in sheet3["weekly"] + ] + + # Sheet 4 weekly + sheet4 = calculate_sheet4_data(db, jahr) + therapie_weekly = sheet4["weekly"] + + return { + "kpis": { + "total_gutachten": total_gutachten, + "bestaetigung": bestaetigung, + "alternative": alternative, + "uncodiert": uncodiert, + "ta_ja": _int(ta_row.ta_ja) if ta_row else 0, + "ta_nein": _int(ta_row.ta_nein) if ta_row else 0, + "diagnosekorrektur": _int(ta_row.dk) if ta_row else 0, + "unterversorgung": _int(ta_row.uv) if ta_row else 0, + "uebertherapie": _int(ta_row.ut) if ta_row else 0, + }, + "gutachten_weekly": gutachten_weekly, + "therapie_weekly": therapie_weekly, + } + + # --------------------------------------------------------------------------- # Full report generation (all 5 sheets) # --------------------------------------------------------------------------- diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 0000000..a7af9f3 --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,20 @@ +# DAK Portal — Feature Roadmap + +## Hohe Priorität + +- [ ] **Gutachten-Statistik Seite** — Route `/gutachten-statistik` existiert als Platzhalter. Visualisierung von Gutachten-Typen (Bestätigung/Alternative), Therapieänderungen, Diagnosekorrektur/Unterversorgung/Übertherapie und Trends pro KW/Jahr. +- [ ] **Fallliste als Excel exportieren** — Export-Button auf der Cases-Seite für gefilterte Falllisten als .xlsx Download. Nutzt aktive Filter (Jahr, Fallgruppe, ICD-Status). +- [ ] **E-Mail-Benachrichtigungen bei Freigabe-Entscheidung** — DAK-Mitarbeiter per E-Mail informieren, wenn ihre Freigabe-Anfrage genehmigt oder abgelehnt wurde. SMTP ist bereits konfiguriert. + +## Mittlere Priorität + +- [ ] **Benachrichtigungs-Center (Bell-Icon)** — UI-Element im Header mit Badge-Counter. `useNotifications()` Hook mit 60s-Polling existiert bereits, braucht nur ein Frontend-Element. +- [ ] **Dashboard: Vorjahresvergleich bei KPIs** — Prozentuale Veränderung zum Vorjahr neben den KPI-Zahlen (z.B. "+12% vs. 2025"). `vorjahr_service` existiert im Backend. +- [ ] **Batch-ICD-Eingabe** — Inline-Tabelle auf der ICD-Seite mit direkter ICD-Eingabe pro Zeile statt Einzelklick auf jeden Fall. +- [ ] **Dark Mode Toggle** — Toggle in Header oder Kontoverwaltung. `useTheme` Hook existiert bereits. + +## Niedrige Priorität + +- [ ] **Erweiterte Suche mit Filterspeicherung** — Häufig genutzte Filter als Presets speichern (z.B. "Onko ohne ICD 2026"). +- [ ] **Dashboard: Durchlaufzeiten** — Durchschnittliche Dauer von Fallerfassung bis Gutachten visualisieren. +- [ ] **Passwort-Reset per E-Mail** — Self-Service "Passwort vergessen" auf der Login-Seite. Aktuell nur Admin-seitig möglich. diff --git a/frontend/src/hooks/useDashboard.ts b/frontend/src/hooks/useDashboard.ts index d9c100f..e1c5929 100644 --- a/frontend/src/hooks/useDashboard.ts +++ b/frontend/src/hooks/useDashboard.ts @@ -1,6 +1,6 @@ import { useQuery } from '@tanstack/react-query' import api from '@/services/api' -import type { DashboardResponse, YearlyComparisonResponse, TopIcdResponse } from '@/types' +import type { DashboardResponse, YearlyComparisonResponse, TopIcdResponse, GutachtenStatistikResponse } from '@/types' export function useDashboard(jahr: number) { return useQuery({ @@ -23,3 +23,10 @@ export function useTopIcd(jahr: number) { queryFn: () => api.get('/reports/dashboard/top-icd', { params: { jahr } }).then(r => r.data), }) } + +export function useGutachtenStatistik(jahr: number) { + return useQuery({ + queryKey: ['gutachten-statistik', jahr], + queryFn: () => api.get('/reports/gutachten-statistik', { params: { jahr } }).then(r => r.data), + }) +} diff --git a/frontend/src/pages/GutachtenStatistikPage.tsx b/frontend/src/pages/GutachtenStatistikPage.tsx index d51e9aa..9bdf33c 100644 --- a/frontend/src/pages/GutachtenStatistikPage.tsx +++ b/frontend/src/pages/GutachtenStatistikPage.tsx @@ -1,17 +1,308 @@ -import { BarChart3 } from 'lucide-react' -import { Card, CardContent } from '@/components/ui/card' +import { useMemo, useState } from 'react' +import { + BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, + PieChart, Pie, Cell, ResponsiveContainer, +} from 'recharts' +import { Stethoscope, CheckCircle, ArrowLeftRight, Clock, Info } from 'lucide-react' +import { useGutachtenStatistik } from '@/hooks/useDashboard' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { + Select, SelectContent, SelectItem, SelectTrigger, SelectValue, +} from '@/components/ui/select' +import { Skeleton } from '@/components/ui/skeleton' +import { + Tooltip as UiTooltip, TooltipContent, TooltipTrigger, +} from '@/components/ui/tooltip' + +const CHART_COLORS = [ + 'var(--chart-1)', + 'var(--chart-2)', + 'var(--chart-3)', + 'var(--chart-4)', + 'var(--chart-5)', +] export function GutachtenStatistikPage() { + const currentYear = new Date().getFullYear() + const years = Array.from({ length: 5 }, (_, i) => currentYear - i) + const [jahr, setJahr] = useState(currentYear) + const { data, isLoading } = useGutachtenStatistik(jahr) + + // Filter out weeks with no data for cleaner charts + const gutachtenWeekly = useMemo(() => { + if (!data?.gutachten_weekly) return [] + return data.gutachten_weekly.filter((w) => w.bestaetigung > 0 || w.alternative > 0) + }, [data]) + + const therapieWeekly = useMemo(() => { + if (!data?.therapie_weekly) return [] + return data.therapie_weekly.filter((w) => w.ta_ja > 0 || w.ta_nein > 0) + }, [data]) + + // Donut data for type distribution + const typDonutData = useMemo(() => { + if (!data?.kpis) return [] + const items = [] + if (data.kpis.bestaetigung > 0) items.push({ name: 'Bestätigung', value: data.kpis.bestaetigung }) + if (data.kpis.alternative > 0) items.push({ name: 'Alternative', value: data.kpis.alternative }) + if (data.kpis.uncodiert > 0) items.push({ name: 'Uncodiert', value: data.kpis.uncodiert }) + return items + }, [data]) + + // Horizontal bar data for therapy change reasons + const reasonsData = useMemo(() => { + if (!data?.kpis) return [] + return [ + { name: 'Diagnosekorrektur', value: data.kpis.diagnosekorrektur }, + { name: 'Unterversorgung', value: data.kpis.unterversorgung }, + { name: 'Übertherapie', value: data.kpis.uebertherapie }, + ].filter((r) => r.value > 0) + }, [data]) + + const reasonsMax = reasonsData.length > 0 ? Math.max(...reasonsData.map((r) => r.value)) : 1 + + const tooltipStyle = { + backgroundColor: 'var(--popover)', + border: '1px solid var(--border)', + borderRadius: '8px', + color: 'var(--popover-foreground)', + } + return ( -
-

Gutachten-Statistik

- - - -

Kommt bald

-

Diese Seite wird in Kürze mit detaillierten Gutachten-Statistiken verfügbar sein.

-
-
+
+ {/* Header */} +
+

Gutachten-Statistik

+ +
+ + {/* KPI Cards */} + {isLoading ? ( +
+ {[1, 2, 3, 4].map((i) => ( + + + + + ))} +
+ ) : data ? ( +
+ } + /> + } + /> + } + /> + } + /> +
+ ) : ( +

Keine Daten verfügbar.

+ )} + + {/* Row 1: Gutachten type per KW + Donut */} + {data && ( +
+ {/* Stacked bar: Bestätigung vs Alternative per KW */} + + + Gutachten-Typ pro Kalenderwoche +

Bestätigung und Alternative pro KW

+
+ + {gutachtenWeekly.length > 0 ? ( + + + + `${v}`} className="text-xs" interval="preserveStartEnd" /> + + `KW ${v}`} contentStyle={tooltipStyle} /> + + + + + + ) : ( +

Noch keine codierten Gutachten vorhanden.

+ )} +
+
+ + {/* Donut: Type distribution */} + + + Typ-Verteilung +

Bestätigung, Alternative, Uncodiert

+
+ + {typDonutData.length > 0 ? ( + + + value} + > + {typDonutData.map((_, idx) => ( + + ))} + + + ( + {value} + )} + /> + + + ) : ( +

Keine Daten.

+ )} +
+
+
+ )} + + {/* Row 2: Therapy changes per KW + Reasons */} + {data && ( +
+ {/* Grouped bar: TA Ja vs Nein per KW */} + + + Therapieänderungen pro Kalenderwoche +

Fälle mit und ohne Therapieänderung pro KW

+
+ + {therapieWeekly.length > 0 ? ( + + + + `${v}`} className="text-xs" interval="preserveStartEnd" /> + + `KW ${v}`} contentStyle={tooltipStyle} /> + + + + + + ) : ( +

Noch keine Therapieänderungen codiert.

+ )} +
+
+ + {/* Reasons for therapy change */} + + + Gründe für Therapieänderung +

Gesamtanzahl im Jahr {jahr}

+
+ + {reasonsData.length > 0 ? ( +
+ {reasonsData.map((item, idx) => ( +
+
+ {item.name} + {item.value} +
+
+
+
+
+ ))} + + {/* Summary */} + {data && ( +
+
+ Therapieänderung Ja + {data.kpis.ta_ja} +
+
+ Therapieänderung Nein + {data.kpis.ta_nein} +
+
+ )} +
+ ) : ( +

Keine Therapieänderungen erfasst.

+ )} + + +
+ )}
) } + +function KpiCard({ title, tooltip, value, icon }: { title: string; tooltip?: string; value: number; icon: React.ReactNode }) { + return ( + + + + {tooltip ? ( + + + + {title} + + + + {tooltip} + + ) : title} + + {icon} + + +
{value.toLocaleString('de-DE')}
+
+
+ ) +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index bae73ad..4aeca73 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -133,6 +133,40 @@ export interface TopIcdResponse { items: TopIcdItem[] } +export interface GutachtenStatistikKPIs { + total_gutachten: number + bestaetigung: number + alternative: number + uncodiert: number + ta_ja: number + ta_nein: number + diagnosekorrektur: number + unterversorgung: number + uebertherapie: number +} + +export interface GutachtenWeeklyPoint { + kw: number + bestaetigung: number + alternative: number +} + +export interface TherapieWeeklyPoint { + kw: number + gutachten: number + ta_ja: number + ta_nein: number + diagnosekorrektur: number + unterversorgung: number + uebertherapie: number +} + +export interface GutachtenStatistikResponse { + kpis: GutachtenStatistikKPIs + gutachten_weekly: GutachtenWeeklyPoint[] + therapie_weekly: TherapieWeeklyPoint[] +} + export interface Notification { id: number notification_type: string