diff --git a/backend/app/api/reports.py b/backend/app/api/reports.py index c33afa6..8a7af05 100644 --- a/backend/app/api/reports.py +++ b/backend/app/api/reports.py @@ -110,6 +110,27 @@ def top_icd( return {"items": items} +@router.get("/dashboard/top-gutachter") +def top_gutachter( + jahr: int | None = Query(None), + db: Session = Depends(get_db), + user: User = Depends(require_admin), +): + """Return the top 10 Gutachter by case count for the given year. + + Admin only. Defaults to the current ISO year if *jahr* is not provided. + """ + 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_top_gutachter + + items = calculate_top_gutachter(db, jahr) + return {"items": items} + + @router.get("/gutachten-statistik") def gutachten_statistik( jahr: int | None = Query(None), diff --git a/backend/app/schemas/report.py b/backend/app/schemas/report.py index 0e42235..b87160f 100644 --- a/backend/app/schemas/report.py +++ b/backend/app/schemas/report.py @@ -13,6 +13,8 @@ class DashboardKPIs(BaseModel): pending_icd: int pending_coding: int total_gutachten: int + total_abgerechnet: int = 0 + pending_abrechnung: int = 0 fallgruppen: dict[str, int] # e.g. {"onko": 123, "kardio": 45, ...} diff --git a/backend/app/services/report_service.py b/backend/app/services/report_service.py index b22c181..42281ce 100644 --- a/backend/app/services/report_service.py +++ b/backend/app/services/report_service.py @@ -599,11 +599,26 @@ def calculate_dashboard_kpis(db: Session, jahr: int, max_kw: int | None = None) else: gutachten_typen["uncodiert"] = _int(row.cnt) + # Abrechnung stats (only among cases with gutachten) + total_abgerechnet = ( + db.query(func.count(Case.id)) + .filter( + *base_filters, + Case.gutachten == True, # noqa: E712 + Case.abgerechnet == True, # noqa: E712 + ) + .scalar() + or 0 + ) + pending_abrechnung = total_gutachten - total_abgerechnet + return { "total_cases": total_cases, "pending_icd": pending_icd, "pending_coding": pending_coding, "total_gutachten": total_gutachten, + "total_abgerechnet": total_abgerechnet, + "pending_abrechnung": pending_abrechnung, "total_ablehnungen": total_ablehnungen, "total_unterlagen": total_unterlagen, "fallgruppen": fallgruppen, @@ -697,6 +712,40 @@ 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: Top Gutachter +# --------------------------------------------------------------------------- + +def calculate_top_gutachter(db: Session, jahr: int, limit: int = 10) -> list[dict]: + """Return the most prolific Gutachter for the given year. + + Returns:: + + [{"gutachter": "Dr. Müller", "count": 42}, ...] + """ + v_filter = Case.versicherung == settings.VERSICHERUNG_FILTER + + rows = ( + db.query( + func.trim(Case.gutachter).label("name"), + func.count(Case.id).label("cnt"), + ) + .filter( + v_filter, + Case.jahr == jahr, + Case.gutachten == True, # noqa: E712 + Case.gutachter != None, # noqa: E711 + Case.gutachter != "", + ) + .group_by(func.trim(Case.gutachter)) + .order_by(func.count(Case.id).desc()) + .limit(limit) + .all() + ) + + return [{"gutachter": row.name, "count": int(row.cnt)} for row in rows] + + # --------------------------------------------------------------------------- # Dashboard: Gutachten-Statistik (combined sheet3 + sheet4 + KPIs) # --------------------------------------------------------------------------- diff --git a/frontend/src/hooks/useDashboard.ts b/frontend/src/hooks/useDashboard.ts index e1c5929..5e62960 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, GutachtenStatistikResponse } from '@/types' +import type { DashboardResponse, YearlyComparisonResponse, TopIcdResponse, TopGutachterResponse, GutachtenStatistikResponse } from '@/types' export function useDashboard(jahr: number) { return useQuery({ @@ -24,6 +24,14 @@ export function useTopIcd(jahr: number) { }) } +export function useTopGutachter(jahr: number, enabled = true) { + return useQuery({ + queryKey: ['dashboard', 'top-gutachter', jahr], + queryFn: () => api.get('/reports/dashboard/top-gutachter', { params: { jahr } }).then(r => r.data), + enabled, + }) +} + export function useGutachtenStatistik(jahr: number) { return useQuery({ queryKey: ['gutachten-statistik', jahr], diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 92e82f3..8370c01 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -6,7 +6,7 @@ import { } from 'recharts' import { FileText, Clock, Code, Stethoscope, Info, TrendingUp, TrendingDown, Minus } from 'lucide-react' import { useAuth } from '@/context/AuthContext' -import { useDashboard, useYearlyComparison, useTopIcd } from '@/hooks/useDashboard' +import { useDashboard, useYearlyComparison, useTopIcd, useTopGutachter } from '@/hooks/useDashboard' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, @@ -47,6 +47,7 @@ export function DashboardPage() { const { data: comparisonData } = useYearlyComparison() const { data: topIcdData } = useTopIcd(jahr) const { isAdmin } = useAuth() + const { data: topGutachterData } = useTopGutachter(jahr, isAdmin) // Extract year keys from comparison data (e.g. ["2022", "2023", ...]) const comparisonYears = useMemo(() => { @@ -67,6 +68,15 @@ export function DashboardPage() { // Top ICD with max for progress bars const topIcdMax = topIcdData?.items?.[0]?.count ?? 1 + // Top Gutachter with max for progress bars (admin only) + const topGutachterMax = topGutachterData?.items?.[0]?.count ?? 1 + + // Abrechnungsstatus donut data (admin only) + const abrechnungsData = data ? [ + { name: 'Abgerechnet', value: data.kpis.total_abgerechnet }, + { name: 'Offen', value: data.kpis.pending_abrechnung }, + ].filter(d => d.value > 0) : [] + const fallgruppenData = data ? Object.entries(data.kpis.fallgruppen) .filter(([, value]) => value > 0) @@ -331,6 +341,87 @@ export function DashboardPage() { )} + + {/* Row 3: Admin-only — Abrechnungsstatus + Gutachter-Verteilung */} + {isAdmin && data && ( +
+ {/* Abrechnungsstatus Donut */} + {abrechnungsData.length > 0 && ( + + + Abrechnungsstatus +

Gutachten: abgerechnet vs. offen

+
+ + + + value} + > + + + + + ( + {value} + )} + /> + + + +
+ )} + + {/* Gutachter-Verteilung */} + {topGutachterData && topGutachterData.items.length > 0 && ( + + + Top 10 Gutachter +

Gutachten pro Gutachter in {jahr}

+
+ +
+ {topGutachterData.items.map((item, idx) => ( +
+
+ {item.gutachter} + {item.count} +
+
+
+
+
+ ))} +
+ + + )} +
+ )}
) } diff --git a/frontend/src/test/mocks/data.ts b/frontend/src/test/mocks/data.ts index 5e5032b..60e3576 100644 --- a/frontend/src/test/mocks/data.ts +++ b/frontend/src/test/mocks/data.ts @@ -158,6 +158,8 @@ export const mockDashboardKPIs: DashboardKPIs = { pending_icd: 18, pending_coding: 7, total_gutachten: 89, + total_abgerechnet: 72, + pending_abrechnung: 17, fallgruppen: { onko: 65, ortho: 42, diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index dd40817..96c87d6 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -98,6 +98,8 @@ export interface DashboardKPIs { pending_icd: number pending_coding: number total_gutachten: number + total_abgerechnet: number + pending_abrechnung: number fallgruppen: Record } @@ -134,6 +136,15 @@ export interface TopIcdResponse { items: TopIcdItem[] } +export interface TopGutachterItem { + gutachter: string + count: number +} + +export interface TopGutachterResponse { + items: TopGutachterItem[] +} + export interface GutachtenStatistikKPIs { total_gutachten: number bestaetigung: number