Files
KanzlAI-mGMT/frontend/src/components/reports/WorkloadTab.tsx
m fdef5af32e feat: reporting dashboard — case stats, deadline compliance, workload, billing (P1)
Backend:
- ReportingService with aggregation queries (CTEs, FILTER clauses)
- 4 API endpoints: /api/reports/{cases,deadlines,workload,billing}
- Date range filtering via ?from=&to= query params

Frontend:
- /berichte page with 4 tabs: Akten, Fristen, Auslastung, Abrechnung
- recharts: bar/pie/line charts for all report types
- Date range picker, CSV export, print-friendly view
- Sidebar nav entry with BarChart3 icon

Also resolves merge conflicts between role-based, notification, and
audit trail branches, and adds missing TS types (AuditLogResponse,
Notification, NotificationPreferences).
2026-03-30 11:24:45 +02:00

188 lines
6.6 KiB
TypeScript

"use client";
import type { WorkloadReport } from "@/lib/types";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Legend,
} from "recharts";
import { Users, AlertTriangle, CheckCircle } from "lucide-react";
export function WorkloadTab({ data }: { data: WorkloadReport }) {
const chartData = data.users.map((u, i) => ({
name: `Nutzer ${i + 1}`,
user_id: u.user_id,
active_cases: u.active_cases,
deadlines: u.deadlines,
overdue: u.overdue,
completed: u.completed,
}));
const totalCases = data.users.reduce((sum, u) => sum + u.active_cases, 0);
const totalOverdue = data.users.reduce((sum, u) => sum + u.overdue, 0);
const totalCompleted = data.users.reduce((sum, u) => sum + u.completed, 0);
return (
<div className="space-y-6">
{/* Summary cards */}
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
<div className="rounded-xl border border-neutral-200 bg-white p-5">
<div className="flex items-center gap-2 text-sm text-neutral-500">
<Users className="h-4 w-4" />
Mitarbeiter
</div>
<p className="mt-2 text-2xl font-semibold text-neutral-900">
{data.users.length}
</p>
<p className="mt-1 text-xs text-neutral-500">
{totalCases} aktive Akten gesamt
</p>
</div>
<div className="rounded-xl border border-neutral-200 bg-white p-5">
<div className="flex items-center gap-2 text-sm text-neutral-500">
<AlertTriangle className="h-4 w-4" />
Überfällige Fristen
</div>
<p className="mt-2 text-2xl font-semibold text-red-600">
{totalOverdue}
</p>
</div>
<div className="rounded-xl border border-neutral-200 bg-white p-5">
<div className="flex items-center gap-2 text-sm text-neutral-500">
<CheckCircle className="h-4 w-4" />
Erledigte Fristen
</div>
<p className="mt-2 text-2xl font-semibold text-emerald-600">
{totalCompleted}
</p>
</div>
</div>
{/* Stacked bar chart */}
<div className="rounded-xl border border-neutral-200 bg-white p-5">
<h3 className="mb-4 text-sm font-medium text-neutral-900">
Auslastung pro Mitarbeiter
</h3>
{chartData.length === 0 ? (
<p className="py-8 text-center text-sm text-neutral-400">
Keine Daten im gewählten Zeitraum
</p>
) : (
<ResponsiveContainer width="100%" height={300}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e5e5" />
<XAxis dataKey="name" tick={{ fontSize: 12 }} stroke="#a3a3a3" />
<YAxis
allowDecimals={false}
tick={{ fontSize: 12 }}
stroke="#a3a3a3"
/>
<Tooltip
contentStyle={{
border: "1px solid #e5e5e5",
borderRadius: 8,
fontSize: 13,
}}
/>
<Legend wrapperStyle={{ fontSize: 13 }} />
<Bar
dataKey="active_cases"
name="Aktive Akten"
stackId="work"
fill="#171717"
radius={[0, 0, 0, 0]}
/>
<Bar
dataKey="completed"
name="Erledigt"
stackId="deadlines"
fill="#a3a3a3"
radius={[0, 0, 0, 0]}
/>
<Bar
dataKey="overdue"
name="Überfällig"
stackId="deadlines"
fill="#dc2626"
radius={[4, 4, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
)}
</div>
{/* Table */}
<div className="rounded-xl border border-neutral-200 bg-white">
<div className="border-b border-neutral-100 px-5 py-4">
<h3 className="text-sm font-medium text-neutral-900">
Übersicht pro Mitarbeiter
</h3>
</div>
{data.users.length === 0 ? (
<p className="px-5 py-8 text-center text-sm text-neutral-400">
Keine Mitarbeiter mit zugewiesenen Akten
</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-neutral-100 text-left text-neutral-500">
<th className="px-5 py-3 font-medium">Mitarbeiter</th>
<th className="px-5 py-3 font-medium text-right">
Aktive Akten
</th>
<th className="px-5 py-3 font-medium text-right">Fristen</th>
<th className="px-5 py-3 font-medium text-right">
Überfällig
</th>
<th className="px-5 py-3 font-medium text-right">
Erledigt
</th>
</tr>
</thead>
<tbody>
{data.users.map((u, i) => (
<tr
key={u.user_id}
className="border-b border-neutral-50 last:border-b-0"
>
<td className="px-5 py-3 text-neutral-900">
Nutzer {i + 1}
<span className="ml-2 text-xs text-neutral-400">
{u.user_id.slice(0, 8)}...
</span>
</td>
<td className="px-5 py-3 text-right font-medium text-neutral-900">
{u.active_cases}
</td>
<td className="px-5 py-3 text-right text-neutral-600">
{u.deadlines}
</td>
<td className="px-5 py-3 text-right">
{u.overdue > 0 ? (
<span className="inline-flex items-center rounded-full bg-red-50 px-2 py-0.5 text-xs font-medium text-red-700">
{u.overdue}
</span>
) : (
<span className="text-neutral-400">0</span>
)}
</td>
<td className="px-5 py-3 text-right text-emerald-600">
{u.completed}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
}