mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 20:43:41 +00:00
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:
parent
3216dd6d53
commit
73b0d6761c
5 changed files with 59 additions and 6 deletions
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue