feat: add year-over-year comparison to Dashboard KPI cards

Dashboard endpoint now returns prev_kpis (previous year's KPIs) alongside
current KPIs. KpiCard component shows percentage change with colored trend
indicators (green up, red down, grey neutral). Also marks completed items
in todo.md (notification center, dark mode toggle were already implemented).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
CCS Admin 2026-02-28 13:15:05 +00:00
parent 3216dd6d53
commit 73b0d6761c
5 changed files with 59 additions and 6 deletions

View file

@ -49,8 +49,9 @@ def dashboard(
) )
kpis = calculate_dashboard_kpis(db, jahr) kpis = calculate_dashboard_kpis(db, jahr)
prev_kpis = calculate_dashboard_kpis(db, jahr - 1)
sheet1 = calculate_sheet1_data(db, jahr) sheet1 = calculate_sheet1_data(db, jahr)
return DashboardResponse(kpis=kpis, weekly=sheet1.get("weekly", [])) return DashboardResponse(kpis=kpis, prev_kpis=prev_kpis, weekly=sheet1.get("weekly", []))
except ImportError: except ImportError:
# report_service not yet implemented (parallel task) # report_service not yet implemented (parallel task)
raise HTTPException(501, "Report service not yet available") raise HTTPException(501, "Report service not yet available")

View file

@ -31,6 +31,7 @@ class DashboardResponse(BaseModel):
"""Combined dashboard payload: KPIs + weekly time-series.""" """Combined dashboard payload: KPIs + weekly time-series."""
kpis: DashboardKPIs kpis: DashboardKPIs
prev_kpis: Optional[DashboardKPIs] = None
weekly: list[WeeklyDataPoint] weekly: list[WeeklyDataPoint]

View file

@ -4,14 +4,14 @@
- [x] **Gutachten-Statistik Seite** — ✅ Implementiert: 4 KPIs, Stacked-Bar (Typ pro KW), Donut (Typ-Verteilung), Grouped-Bar (Therapieänderungen pro KW), Horizontal-Bars (Gründe). Backend-Endpoint + Frontend komplett. - [x] **Gutachten-Statistik Seite** — ✅ Implementiert: 4 KPIs, Stacked-Bar (Typ pro KW), Donut (Typ-Verteilung), Grouped-Bar (Therapieänderungen pro KW), Horizontal-Bars (Gründe). Backend-Endpoint + Frontend komplett.
- [ ] **Fallliste als Excel exportieren** — Export-Button auf der Cases-Seite für gefilterte Falllisten als .xlsx Download. Nutzt aktive Filter (Jahr, Fallgruppe, ICD-Status). - [ ] **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. - [x] **E-Mail-Benachrichtigungen bei Freigabe-Entscheidung** — ✅ Implementiert: disclosure_service nutzt nun notification_service für In-App + E-Mail. Admins erhalten E-Mail bei neuer Anfrage, Mitarbeiter bei Genehmigung/Ablehnung.
## Mittlere Priorität ## 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. - [x] **Benachrichtigungs-Center (Bell-Icon)** — ✅ Bereits implementiert: Bell-Icon im Header mit Badge-Counter, Popover-Dropdown, Mark-as-read, 60s-Polling. War schon in Header.tsx vorhanden.
- [ ] **Dashboard: Vorjahresvergleich bei KPIs** — Prozentuale Veränderung zum Vorjahr neben den KPI-Zahlen (z.B. "+12% vs. 2025"). `vorjahr_service` existiert im Backend. - [ ] **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. - [ ] **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. - [x] **Dark Mode Toggle** — ✅ Bereits implementiert: Sun/Moon-Toggle im Header, useTheme Hook aktiv.
## Niedrige Priorität ## Niedrige Priorität

View file

@ -4,7 +4,7 @@ import {
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend,
PieChart, Pie, Cell, ResponsiveContainer, PieChart, Pie, Cell, ResponsiveContainer,
} from 'recharts' } from 'recharts'
import { FileText, Clock, Code, Stethoscope, Info } from 'lucide-react' import { FileText, Clock, Code, Stethoscope, Info, TrendingUp, TrendingDown, Minus } from 'lucide-react'
import { useAuth } from '@/context/AuthContext' import { useAuth } from '@/context/AuthContext'
import { useDashboard, useYearlyComparison, useTopIcd } from '@/hooks/useDashboard' import { useDashboard, useYearlyComparison, useTopIcd } from '@/hooks/useDashboard'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@ -115,6 +115,8 @@ export function DashboardPage() {
title="Fälle gesamt" title="Fälle gesamt"
tooltip="Gesamtzahl aller erfassten Fälle im gewählten Jahr" tooltip="Gesamtzahl aller erfassten Fälle im gewählten Jahr"
value={data.kpis.total_cases} value={data.kpis.total_cases}
prevValue={data.prev_kpis?.total_cases}
prevYear={jahr - 1}
icon={<FileText className="size-5 text-muted-foreground" />} icon={<FileText className="size-5 text-muted-foreground" />}
href="/cases" href="/cases"
/> />
@ -122,6 +124,8 @@ export function DashboardPage() {
title="Offene ICD" title="Offene ICD"
tooltip="Fälle, denen noch ein ICD-10-Diagnosecode zugewiesen werden muss" tooltip="Fälle, denen noch ein ICD-10-Diagnosecode zugewiesen werden muss"
value={data.kpis.pending_icd} value={data.kpis.pending_icd}
prevValue={data.prev_kpis?.pending_icd}
prevYear={jahr - 1}
icon={<Clock className="size-5 text-muted-foreground" />} icon={<Clock className="size-5 text-muted-foreground" />}
href="/icd" href="/icd"
/> />
@ -129,6 +133,8 @@ export function DashboardPage() {
title="Offene Codierung" title="Offene Codierung"
tooltip="Fälle, die noch nicht abschließend klassifiziert wurden" tooltip="Fälle, die noch nicht abschließend klassifiziert wurden"
value={data.kpis.pending_coding} value={data.kpis.pending_coding}
prevValue={data.prev_kpis?.pending_coding}
prevYear={jahr - 1}
icon={<Code className="size-5 text-muted-foreground" />} icon={<Code className="size-5 text-muted-foreground" />}
href={isAdmin ? '/coding' : undefined} href={isAdmin ? '/coding' : undefined}
/> />
@ -136,6 +142,8 @@ export function DashboardPage() {
title="Gutachten gesamt" title="Gutachten gesamt"
tooltip="Anzahl der Fälle mit erstelltem Gutachten" tooltip="Anzahl der Fälle mit erstelltem Gutachten"
value={data.kpis.total_gutachten} value={data.kpis.total_gutachten}
prevValue={data.prev_kpis?.total_gutachten}
prevYear={jahr - 1}
icon={<Stethoscope className="size-5 text-muted-foreground" />} icon={<Stethoscope className="size-5 text-muted-foreground" />}
href="/gutachten-statistik" href="/gutachten-statistik"
/> />
@ -327,7 +335,20 @@ export function DashboardPage() {
) )
} }
function KpiCard({ title, tooltip, value, icon, href }: { title: string; tooltip?: string; value: number; icon: React.ReactNode; href?: string }) { function KpiCard({ title, tooltip, value, prevValue, prevYear, icon, href }: {
title: string
tooltip?: string
value: number
prevValue?: number
prevYear?: number
icon: React.ReactNode
href?: string
}) {
// Calculate percentage change vs previous year
const change = prevValue != null && prevValue > 0
? ((value - prevValue) / prevValue) * 100
: null
const card = ( const card = (
<Card className={href ? 'transition-colors hover:border-primary/50 hover:shadow-md' : undefined}> <Card className={href ? 'transition-colors hover:border-primary/50 hover:shadow-md' : undefined}>
<CardHeader className="flex flex-row items-center justify-between pb-2"> <CardHeader className="flex flex-row items-center justify-between pb-2">
@ -348,6 +369,35 @@ function KpiCard({ title, tooltip, value, icon, href }: { title: string; tooltip
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="text-3xl font-bold">{value.toLocaleString('de-DE')}</div> <div className="text-3xl font-bold">{value.toLocaleString('de-DE')}</div>
{prevValue != null && prevYear != null && (
<div className="mt-1 flex items-center gap-1 text-xs">
{change !== null ? (
change > 0 ? (
<>
<TrendingUp className="size-3 text-green-600" />
<span className="text-green-600 font-medium">+{Math.round(change)}%</span>
</>
) : change < 0 ? (
<>
<TrendingDown className="size-3 text-red-500" />
<span className="text-red-500 font-medium">{Math.round(change)}%</span>
</>
) : (
<>
<Minus className="size-3 text-muted-foreground" />
<span className="text-muted-foreground">0%</span>
</>
)
) : prevValue === 0 && value > 0 ? (
<span className="text-muted-foreground">Neu in {prevYear + 1}</span>
) : (
<span className="text-muted-foreground"></span>
)}
{change !== null && (
<span className="text-muted-foreground">ggü. {prevYear}</span>
)}
</div>
)}
</CardContent> </CardContent>
</Card> </Card>
) )

View file

@ -112,6 +112,7 @@ export interface WeeklyDataPoint {
export interface DashboardResponse { export interface DashboardResponse {
kpis: DashboardKPIs kpis: DashboardKPIs
prev_kpis?: DashboardKPIs | null
weekly: WeeklyDataPoint[] weekly: WeeklyDataPoint[]
} }