mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 14:53:41 +00:00
feat: implement Gutachten-Statistik page with KPIs and charts
Add full statistics page replacing placeholder: 4 KPI cards (total, Bestätigung, Alternative, Uncodiert), stacked bar chart for gutachten types per KW, donut chart for type distribution, grouped bar chart for therapy changes per KW, and horizontal bar chart for therapy change reasons. Includes new backend endpoint and service function combining sheet3/sheet4 data with KPI aggregation. Also adds feature roadmap todo.md. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
bfb0e4bfdf
commit
5da1e523d3
6 changed files with 486 additions and 12 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
20
docs/todo.md
Normal file
20
docs/todo.md
Normal file
|
|
@ -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.
|
||||
|
|
@ -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<TopIcdResponse>('/reports/dashboard/top-icd', { params: { jahr } }).then(r => r.data),
|
||||
})
|
||||
}
|
||||
|
||||
export function useGutachtenStatistik(jahr: number) {
|
||||
return useQuery({
|
||||
queryKey: ['gutachten-statistik', jahr],
|
||||
queryFn: () => api.get<GutachtenStatistikResponse>('/reports/gutachten-statistik', { params: { jahr } }).then(r => r.data),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="p-6 space-y-4">
|
||||
<h1 className="text-2xl font-bold">Gutachten-Statistik</h1>
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center justify-center py-16 text-muted-foreground">
|
||||
<BarChart3 className="size-12 mb-4" />
|
||||
<p className="text-lg font-medium">Kommt bald</p>
|
||||
<p className="text-sm">Diese Seite wird in Kürze mit detaillierten Gutachten-Statistiken verfügbar sein.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Gutachten-Statistik</h1>
|
||||
<Select value={String(jahr)} onValueChange={(v) => setJahr(Number(v))}>
|
||||
<SelectTrigger className="w-[140px]">
|
||||
<SelectValue placeholder="Jahr" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{years.map((y) => (
|
||||
<SelectItem key={y} value={String(y)}>{y}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* KPI Cards */}
|
||||
{isLoading ? (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{[1, 2, 3, 4].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader className="pb-2"><Skeleton className="h-4 w-24" /></CardHeader>
|
||||
<CardContent><Skeleton className="h-8 w-16" /></CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
) : data ? (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<KpiCard
|
||||
title="Gutachten gesamt"
|
||||
tooltip="Alle Fälle mit erstelltem Gutachten im gewählten Jahr"
|
||||
value={data.kpis.total_gutachten}
|
||||
icon={<Stethoscope className="size-5 text-muted-foreground" />}
|
||||
/>
|
||||
<KpiCard
|
||||
title="Bestätigung"
|
||||
tooltip="Gutachten, die die Erstmeinung bestätigen"
|
||||
value={data.kpis.bestaetigung}
|
||||
icon={<CheckCircle className="size-5 text-green-600" />}
|
||||
/>
|
||||
<KpiCard
|
||||
title="Alternative"
|
||||
tooltip="Gutachten mit abweichender Therapieempfehlung"
|
||||
value={data.kpis.alternative}
|
||||
icon={<ArrowLeftRight className="size-5 text-orange-500" />}
|
||||
/>
|
||||
<KpiCard
|
||||
title="Uncodiert"
|
||||
tooltip="Gutachten, die noch nicht klassifiziert wurden"
|
||||
value={data.kpis.uncodiert}
|
||||
icon={<Clock className="size-5 text-muted-foreground" />}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground">Keine Daten verfügbar.</p>
|
||||
)}
|
||||
|
||||
{/* Row 1: Gutachten type per KW + Donut */}
|
||||
{data && (
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Stacked bar: Bestätigung vs Alternative per KW */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Gutachten-Typ pro Kalenderwoche</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">Bestätigung und Alternative pro KW</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{gutachtenWeekly.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={350}>
|
||||
<BarChart data={gutachtenWeekly}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
|
||||
<XAxis dataKey="kw" tickFormatter={(v) => `${v}`} className="text-xs" interval="preserveStartEnd" />
|
||||
<YAxis className="text-xs" />
|
||||
<Tooltip labelFormatter={(v) => `KW ${v}`} contentStyle={tooltipStyle} />
|
||||
<Legend />
|
||||
<Bar dataKey="bestaetigung" name="Bestätigung" stackId="a" fill="var(--chart-3)" radius={[0, 0, 0, 0]} />
|
||||
<Bar dataKey="alternative" name="Alternative" stackId="a" fill="var(--chart-1)" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-center py-16">Noch keine codierten Gutachten vorhanden.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Donut: Type distribution */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Typ-Verteilung</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">Bestätigung, Alternative, Uncodiert</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{typDonutData.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={350}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={typDonutData}
|
||||
cx="50%"
|
||||
cy="45%"
|
||||
innerRadius={50}
|
||||
outerRadius={85}
|
||||
paddingAngle={3}
|
||||
dataKey="value"
|
||||
label={({ value }) => value}
|
||||
>
|
||||
{typDonutData.map((_, idx) => (
|
||||
<Cell key={idx} fill={[
|
||||
'var(--chart-3)',
|
||||
'var(--chart-1)',
|
||||
'var(--chart-4)',
|
||||
][idx % 3]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip contentStyle={tooltipStyle} />
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
iconType="circle"
|
||||
iconSize={10}
|
||||
formatter={(value: string) => (
|
||||
<span style={{ color: 'var(--foreground)', fontSize: '13px' }}>{value}</span>
|
||||
)}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-center py-16">Keine Daten.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 2: Therapy changes per KW + Reasons */}
|
||||
{data && (
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Grouped bar: TA Ja vs Nein per KW */}
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Therapieänderungen pro Kalenderwoche</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">Fälle mit und ohne Therapieänderung pro KW</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{therapieWeekly.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={350}>
|
||||
<BarChart data={therapieWeekly}>
|
||||
<CartesianGrid strokeDasharray="3 3" className="stroke-border" />
|
||||
<XAxis dataKey="kw" tickFormatter={(v) => `${v}`} className="text-xs" interval="preserveStartEnd" />
|
||||
<YAxis className="text-xs" />
|
||||
<Tooltip labelFormatter={(v) => `KW ${v}`} contentStyle={tooltipStyle} />
|
||||
<Legend />
|
||||
<Bar dataKey="ta_ja" name="Therapieänderung Ja" fill="var(--chart-1)" radius={[4, 4, 0, 0]} />
|
||||
<Bar dataKey="ta_nein" name="Therapieänderung Nein" fill="var(--chart-2)" radius={[4, 4, 0, 0]} />
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-center py-16">Noch keine Therapieänderungen codiert.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Reasons for therapy change */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Gründe für Therapieänderung</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">Gesamtanzahl im Jahr {jahr}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{reasonsData.length > 0 ? (
|
||||
<div className="space-y-4 pt-4">
|
||||
{reasonsData.map((item, idx) => (
|
||||
<div key={item.name} className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium">{item.name}</span>
|
||||
<span className="text-muted-foreground tabular-nums">{item.value}</span>
|
||||
</div>
|
||||
<div className="h-3 w-full rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${(item.value / reasonsMax) * 100}%`,
|
||||
backgroundColor: CHART_COLORS[idx % CHART_COLORS.length],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Summary */}
|
||||
{data && (
|
||||
<div className="pt-4 border-t mt-4 space-y-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Therapieänderung Ja</span>
|
||||
<span className="font-medium tabular-nums">{data.kpis.ta_ja}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Therapieänderung Nein</span>
|
||||
<span className="font-medium tabular-nums">{data.kpis.ta_nein}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-muted-foreground text-center py-16">Keine Therapieänderungen erfasst.</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function KpiCard({ title, tooltip, value, icon }: { title: string; tooltip?: string; value: number; icon: React.ReactNode }) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium">
|
||||
{tooltip ? (
|
||||
<UiTooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span className="inline-flex items-center gap-1 cursor-help">
|
||||
{title}
|
||||
<Info className="size-3 text-muted-foreground" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltip}</TooltipContent>
|
||||
</UiTooltip>
|
||||
) : title}
|
||||
</CardTitle>
|
||||
{icon}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-bold">{value.toLocaleString('de-DE')}</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue