mirror of
https://github.com/complexcaresolutions/dak.c2s.git
synced 2026-03-17 16:03:41 +00:00
feat: add admin-only billing status donut and top gutachter charts to Dashboard
Extends DashboardKPIs with total_abgerechnet/pending_abrechnung. Adds new GET /reports/dashboard/top-gutachter endpoint (admin-only). Frontend shows Abrechnungsstatus donut + Gutachter-Verteilung progress bars in a new third row, visible only to admins. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f3846813a4
commit
42fd1808c4
7 changed files with 186 additions and 2 deletions
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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, ...}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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<TopGutachterResponse>('/reports/dashboard/top-gutachter', { params: { jahr } }).then(r => r.data),
|
||||
enabled,
|
||||
})
|
||||
}
|
||||
|
||||
export function useGutachtenStatistik(jahr: number) {
|
||||
return useQuery({
|
||||
queryKey: ['gutachten-statistik', jahr],
|
||||
|
|
|
|||
|
|
@ -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() {
|
|||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Row 3: Admin-only — Abrechnungsstatus + Gutachter-Verteilung */}
|
||||
{isAdmin && data && (
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Abrechnungsstatus Donut */}
|
||||
{abrechnungsData.length > 0 && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Abrechnungsstatus</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">Gutachten: abgerechnet vs. offen</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={abrechnungsData}
|
||||
cx="50%"
|
||||
cy="45%"
|
||||
innerRadius={50}
|
||||
outerRadius={85}
|
||||
paddingAngle={3}
|
||||
dataKey="value"
|
||||
label={({ value }) => value}
|
||||
>
|
||||
<Cell fill="var(--chart-3)" />
|
||||
<Cell fill="var(--chart-1)" />
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: 'var(--popover)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '8px',
|
||||
color: 'var(--popover-foreground)',
|
||||
}}
|
||||
/>
|
||||
<Legend
|
||||
verticalAlign="bottom"
|
||||
iconType="circle"
|
||||
iconSize={10}
|
||||
formatter={(value: string) => (
|
||||
<span style={{ color: 'var(--foreground)', fontSize: '13px' }}>{value}</span>
|
||||
)}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Gutachter-Verteilung */}
|
||||
{topGutachterData && topGutachterData.items.length > 0 && (
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Top 10 Gutachter</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">Gutachten pro Gutachter in {jahr}</p>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-3">
|
||||
{topGutachterData.items.map((item, idx) => (
|
||||
<div key={item.gutachter} className="space-y-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium truncate max-w-[200px]">{item.gutachter}</span>
|
||||
<span className="text-muted-foreground tabular-nums">{item.count}</span>
|
||||
</div>
|
||||
<div className="h-2 w-full rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${(item.count / topGutachterMax) * 100}%`,
|
||||
backgroundColor: CHART_COLORS[idx % CHART_COLORS.length],
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -98,6 +98,8 @@ export interface DashboardKPIs {
|
|||
pending_icd: number
|
||||
pending_coding: number
|
||||
total_gutachten: number
|
||||
total_abgerechnet: number
|
||||
pending_abrechnung: number
|
||||
fallgruppen: Record<string, number>
|
||||
}
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue