diff --git a/backend/app/api/reports.py b/backend/app/api/reports.py index 0042c6d..c6e122e 100644 --- a/backend/app/api/reports.py +++ b/backend/app/api/reports.py @@ -56,6 +56,44 @@ def dashboard( raise HTTPException(501, "Report service not yet available") +@router.get("/dashboard/yearly-comparison") +def yearly_comparison( + db: Session = Depends(get_db), + user: User = Depends(get_current_user), +): + """Return per-KW case counts for each year from 2022 onwards. + + Used for the multi-year comparison bar chart on the dashboard. + Accessible to both admin and dak_mitarbeiter users. + """ + from app.services.report_service import calculate_yearly_kw_comparison + + data = calculate_yearly_kw_comparison(db) + return {"data": data} + + +@router.get("/dashboard/top-icd") +def top_icd( + jahr: int | None = Query(None), + db: Session = Depends(get_db), + user: User = Depends(get_current_user), +): + """Return the top 10 most common ICD codes for the given year. + + 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_top_icd + + items = calculate_top_icd(db, jahr) + return {"items": items} + + @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 befba11..69fdc2f 100644 --- a/backend/app/services/report_service.py +++ b/backend/app/services/report_service.py @@ -606,6 +606,92 @@ def calculate_dashboard_kpis(db: Session, jahr: int) -> dict: } +# --------------------------------------------------------------------------- +# Dashboard: Yearly KW comparison (multi-year) +# --------------------------------------------------------------------------- + +def calculate_yearly_kw_comparison(db: Session) -> list[dict]: + """Return per-KW total case counts for each year from 2022 onwards. + + Returns a list of dicts like:: + + [ + {"kw": 1, "2022": 5, "2023": 8, "2024": 12, ...}, + {"kw": 2, "2022": 3, "2023": 7, "2024": 9, ...}, + ... + ] + """ + v_filter = Case.versicherung == settings.VERSICHERUNG_FILTER + + rows = ( + db.query( + Case.jahr, + Case.kw, + func.count(Case.id).label("cnt"), + ) + .filter(v_filter, Case.jahr >= 2022) + .group_by(Case.jahr, Case.kw) + .all() + ) + + # Build lookup: {kw: {year: count}} + kw_data: dict[int, dict[int, int]] = {} + all_years: set[int] = set() + for row in rows: + yr = int(row.jahr) + kw = int(row.kw) + all_years.add(yr) + if kw not in kw_data: + kw_data[kw] = {} + kw_data[kw][yr] = int(row.cnt) + + sorted_years = sorted(all_years) + result = [] + for kw in range(1, 53): + entry: dict[str, Any] = {"kw": kw} + for yr in sorted_years: + entry[str(yr)] = kw_data.get(kw, {}).get(yr, 0) + result.append(entry) + + return result + + +# --------------------------------------------------------------------------- +# Dashboard: Top ICD codes +# --------------------------------------------------------------------------- + +def calculate_top_icd(db: Session, jahr: int, limit: int = 10) -> list[dict]: + """Return the most common ICD codes across all cases for the given year. + + Uses the ``icd`` text field on cases (not the junction table) so it + covers all Fallgruppen, not just onko. + + Returns:: + + [{"icd": "C50.9", "count": 42}, {"icd": "I25.1", "count": 31}, ...] + """ + v_filter = Case.versicherung == settings.VERSICHERUNG_FILTER + + rows = ( + db.query( + func.upper(func.trim(Case.icd)).label("icd_code"), + func.count(Case.id).label("cnt"), + ) + .filter( + v_filter, + Case.jahr == jahr, + Case.icd != None, # noqa: E711 + Case.icd != "", + ) + .group_by(func.upper(func.trim(Case.icd))) + .order_by(func.count(Case.id).desc()) + .limit(limit) + .all() + ) + + return [{"icd": row.icd_code, "count": int(row.cnt)} for row in rows] + + # --------------------------------------------------------------------------- # Full report generation (all 5 sheets) # --------------------------------------------------------------------------- diff --git a/frontend/src/hooks/useDashboard.ts b/frontend/src/hooks/useDashboard.ts index 559275c..d9c100f 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 } from '@/types' +import type { DashboardResponse, YearlyComparisonResponse, TopIcdResponse } from '@/types' export function useDashboard(jahr: number) { return useQuery({ @@ -8,3 +8,18 @@ export function useDashboard(jahr: number) { queryFn: () => api.get('/reports/dashboard', { params: { jahr } }).then(r => r.data), }) } + +export function useYearlyComparison() { + return useQuery({ + queryKey: ['dashboard', 'yearly-comparison'], + queryFn: () => api.get('/reports/dashboard/yearly-comparison').then(r => r.data), + staleTime: 60_000, + }) +} + +export function useTopIcd(jahr: number) { + return useQuery({ + queryKey: ['dashboard', 'top-icd', jahr], + queryFn: () => api.get('/reports/dashboard/top-icd', { params: { jahr } }).then(r => r.data), + }) +} diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 8ab4f4d..af516e0 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, @@ -6,7 +6,7 @@ import { } from 'recharts' import { FileText, Clock, Code, Stethoscope, Info } from 'lucide-react' import { useAuth } from '@/context/AuthContext' -import { useDashboard } from '@/hooks/useDashboard' +import { useDashboard, useYearlyComparison, useTopIcd } from '@/hooks/useDashboard' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, @@ -32,12 +32,41 @@ const CHART_COLORS = [ 'var(--chart-5)', ] +const YEAR_COLORS: Record = { + '2022': 'var(--chart-4)', + '2023': 'var(--chart-5)', + '2024': 'var(--chart-2)', + '2025': 'var(--chart-3)', + '2026': 'var(--chart-1)', +} + export function DashboardPage() { const currentYear = new Date().getFullYear() const [jahr, setJahr] = useState(currentYear) const { data, isLoading: loading } = useDashboard(jahr) + const { data: comparisonData } = useYearlyComparison() + const { data: topIcdData } = useTopIcd(jahr) const { isAdmin } = useAuth() + // Extract year keys from comparison data (e.g. ["2022", "2023", ...]) + const comparisonYears = useMemo(() => { + if (!comparisonData?.data?.[0]) return [] + return Object.keys(comparisonData.data[0]) + .filter((k) => k !== 'kw') + .sort() + }, [comparisonData]) + + // Filter out KWs where all years are 0 + const filteredComparison = useMemo(() => { + if (!comparisonData?.data) return [] + return comparisonData.data.filter((row) => + comparisonYears.some((yr) => (row[yr] ?? 0) > 0) + ) + }, [comparisonData, comparisonYears]) + + // Top ICD with max for progress bars + const topIcdMax = topIcdData?.items?.[0]?.count ?? 1 + const fallgruppenData = data ? Object.entries(data.kpis.fallgruppen) .filter(([, value]) => value > 0) @@ -217,6 +246,83 @@ export function DashboardPage() { )} + + {/* Row 2: Yearly KW comparison + Top 10 ICD */} +
+ {/* Yearly KW comparison bar chart */} + {filteredComparison.length > 0 && ( + + + Jahresvergleich nach Kalenderwoche +

Anzahl Fälle pro KW im Vergleich ab 2022

+
+ + + + + `${v}`} + className="text-xs" + interval="preserveStartEnd" + /> + + `KW ${v}`} + contentStyle={{ + backgroundColor: 'var(--popover)', + border: '1px solid var(--border)', + borderRadius: '8px', + color: 'var(--popover-foreground)', + }} + /> + + {comparisonYears.map((yr) => ( + + ))} + + + +
+ )} + + {/* Top 10 ICD codes */} + {topIcdData && topIcdData.items.length > 0 && ( + + + Top 10 ICD-Codes +

Häufigste Diagnosecodes in {jahr}

+
+ +
+ {topIcdData.items.map((item, idx) => ( +
+
+ {item.icd} + {item.count} +
+
+
+
+
+ ))} +
+ + + )} +
) } diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 51b6e6f..bae73ad 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -115,6 +115,24 @@ export interface DashboardResponse { weekly: WeeklyDataPoint[] } +export interface YearlyComparisonPoint { + kw: number + [year: string]: number +} + +export interface YearlyComparisonResponse { + data: YearlyComparisonPoint[] +} + +export interface TopIcdItem { + icd: string + count: number +} + +export interface TopIcdResponse { + items: TopIcdItem[] +} + export interface Notification { id: number notification_type: string