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).
224 lines
6.9 KiB
TypeScript
224 lines
6.9 KiB
TypeScript
"use client";
|
|
|
|
import type { CaseReport } from "@/lib/types";
|
|
import {
|
|
BarChart,
|
|
Bar,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
Legend,
|
|
PieChart,
|
|
Pie,
|
|
Cell,
|
|
} from "recharts";
|
|
import { FolderOpen, TrendingUp, TrendingDown } from "lucide-react";
|
|
|
|
const COLORS = [
|
|
"#171717",
|
|
"#525252",
|
|
"#a3a3a3",
|
|
"#d4d4d4",
|
|
"#737373",
|
|
"#404040",
|
|
"#e5e5e5",
|
|
"#262626",
|
|
];
|
|
|
|
function formatMonth(period: string): string {
|
|
const [year, month] = period.split("-");
|
|
const months = [
|
|
"Jan",
|
|
"Feb",
|
|
"Mär",
|
|
"Apr",
|
|
"Mai",
|
|
"Jun",
|
|
"Jul",
|
|
"Aug",
|
|
"Sep",
|
|
"Okt",
|
|
"Nov",
|
|
"Dez",
|
|
];
|
|
return `${months[parseInt(month, 10) - 1]} ${year.slice(2)}`;
|
|
}
|
|
|
|
export function CasesTab({ data }: { data: CaseReport }) {
|
|
const chartData = data.monthly.map((m) => ({
|
|
...m,
|
|
name: formatMonth(m.period),
|
|
}));
|
|
|
|
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">
|
|
<FolderOpen className="h-4 w-4" />
|
|
Eröffnet
|
|
</div>
|
|
<p className="mt-2 text-2xl font-semibold text-neutral-900">
|
|
{data.total.opened}
|
|
</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">
|
|
<TrendingDown className="h-4 w-4" />
|
|
Geschlossen
|
|
</div>
|
|
<p className="mt-2 text-2xl font-semibold text-neutral-900">
|
|
{data.total.closed}
|
|
</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">
|
|
<TrendingUp className="h-4 w-4" />
|
|
Aktiv
|
|
</div>
|
|
<p className="mt-2 text-2xl font-semibold text-emerald-600">
|
|
{data.total.active}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Bar chart: opened/closed per month */}
|
|
<div className="rounded-xl border border-neutral-200 bg-white p-5">
|
|
<h3 className="mb-4 text-sm font-medium text-neutral-900">
|
|
Akten pro Monat
|
|
</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="opened"
|
|
name="Eröffnet"
|
|
fill="#171717"
|
|
radius={[4, 4, 0, 0]}
|
|
/>
|
|
<Bar
|
|
dataKey="closed"
|
|
name="Geschlossen"
|
|
fill="#a3a3a3"
|
|
radius={[4, 4, 0, 0]}
|
|
/>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</div>
|
|
|
|
{/* Pie charts row */}
|
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
|
{/* By type */}
|
|
<div className="rounded-xl border border-neutral-200 bg-white p-5">
|
|
<h3 className="mb-4 text-sm font-medium text-neutral-900">
|
|
Nach Verfahrensart
|
|
</h3>
|
|
{data.by_type.length === 0 ? (
|
|
<p className="py-8 text-center text-sm text-neutral-400">
|
|
Keine Daten
|
|
</p>
|
|
) : (
|
|
<div className="flex items-center gap-4">
|
|
<ResponsiveContainer width="50%" height={200}>
|
|
<PieChart>
|
|
<Pie
|
|
data={data.by_type}
|
|
dataKey="count"
|
|
nameKey="case_type"
|
|
cx="50%"
|
|
cy="50%"
|
|
innerRadius={40}
|
|
outerRadius={80}
|
|
>
|
|
{data.by_type.map((_, i) => (
|
|
<Cell
|
|
key={i}
|
|
fill={COLORS[i % COLORS.length]}
|
|
/>
|
|
))}
|
|
</Pie>
|
|
<Tooltip />
|
|
</PieChart>
|
|
</ResponsiveContainer>
|
|
<div className="flex-1 space-y-2">
|
|
{data.by_type.map((item, i) => (
|
|
<div key={i} className="flex items-center gap-2 text-sm">
|
|
<div
|
|
className="h-3 w-3 rounded-full"
|
|
style={{
|
|
backgroundColor: COLORS[i % COLORS.length],
|
|
}}
|
|
/>
|
|
<span className="text-neutral-600">{item.case_type}</span>
|
|
<span className="ml-auto font-medium text-neutral-900">
|
|
{item.count}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* By court */}
|
|
<div className="rounded-xl border border-neutral-200 bg-white p-5">
|
|
<h3 className="mb-4 text-sm font-medium text-neutral-900">
|
|
Nach Gericht
|
|
</h3>
|
|
{data.by_court.length === 0 ? (
|
|
<p className="py-8 text-center text-sm text-neutral-400">
|
|
Keine Daten
|
|
</p>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{data.by_court.map((item, i) => {
|
|
const maxCount = Math.max(...data.by_court.map((c) => c.count));
|
|
const pct = maxCount > 0 ? (item.count / maxCount) * 100 : 0;
|
|
return (
|
|
<div key={i}>
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="text-neutral-600">{item.court}</span>
|
|
<span className="font-medium text-neutral-900">
|
|
{item.count}
|
|
</span>
|
|
</div>
|
|
<div className="mt-1 h-2 rounded-full bg-neutral-100">
|
|
<div
|
|
className="h-2 rounded-full bg-neutral-900 transition-all"
|
|
style={{ width: `${pct}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|