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).
205 lines
6.7 KiB
TypeScript
205 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import type { DeadlineReport } from "@/lib/types";
|
|
import Link from "next/link";
|
|
import {
|
|
LineChart,
|
|
Line,
|
|
XAxis,
|
|
YAxis,
|
|
CartesianGrid,
|
|
Tooltip,
|
|
ResponsiveContainer,
|
|
Legend,
|
|
} from "recharts";
|
|
import { CheckCircle, XCircle, Clock, AlertTriangle } from "lucide-react";
|
|
|
|
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)}`;
|
|
}
|
|
|
|
function formatDate(dateStr: string): string {
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleDateString("de-DE", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
});
|
|
}
|
|
|
|
export function DeadlinesTab({ data }: { data: DeadlineReport }) {
|
|
const chartData = data.monthly.map((m) => ({
|
|
...m,
|
|
name: formatMonth(m.period),
|
|
compliance_rate: Math.round(m.compliance_rate * 10) / 10,
|
|
}));
|
|
|
|
const complianceColor =
|
|
data.total.compliance_rate >= 90
|
|
? "text-emerald-600"
|
|
: data.total.compliance_rate >= 70
|
|
? "text-amber-600"
|
|
: "text-red-600";
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Summary cards */}
|
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-4">
|
|
<div className="rounded-xl border border-neutral-200 bg-white p-5">
|
|
<div className="flex items-center gap-2 text-sm text-neutral-500">
|
|
<Clock className="h-4 w-4" />
|
|
Gesamt
|
|
</div>
|
|
<p className="mt-2 text-2xl font-semibold text-neutral-900">
|
|
{data.total.total}
|
|
</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" />
|
|
Eingehalten
|
|
</div>
|
|
<p className="mt-2 text-2xl font-semibold text-emerald-600">
|
|
{data.total.met}
|
|
</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">
|
|
<XCircle className="h-4 w-4" />
|
|
Versäumt
|
|
</div>
|
|
<p className="mt-2 text-2xl font-semibold text-red-600">
|
|
{data.total.missed}
|
|
</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" />
|
|
Einhaltungsquote
|
|
</div>
|
|
<p className={`mt-2 text-2xl font-semibold ${complianceColor}`}>
|
|
{data.total.compliance_rate.toFixed(1)}%
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Compliance rate over time */}
|
|
<div className="rounded-xl border border-neutral-200 bg-white p-5">
|
|
<h3 className="mb-4 text-sm font-medium text-neutral-900">
|
|
Fristeneinhaltung im Zeitverlauf
|
|
</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}>
|
|
<LineChart data={chartData}>
|
|
<CartesianGrid strokeDasharray="3 3" stroke="#e5e5e5" />
|
|
<XAxis dataKey="name" tick={{ fontSize: 12 }} stroke="#a3a3a3" />
|
|
<YAxis
|
|
domain={[0, 100]}
|
|
tick={{ fontSize: 12 }}
|
|
stroke="#a3a3a3"
|
|
unit="%"
|
|
/>
|
|
<Tooltip
|
|
contentStyle={{
|
|
border: "1px solid #e5e5e5",
|
|
borderRadius: 8,
|
|
fontSize: 13,
|
|
}}
|
|
formatter={(value) => [`${value}%`, "Quote"]}
|
|
/>
|
|
<Legend wrapperStyle={{ fontSize: 13 }} />
|
|
<Line
|
|
type="monotone"
|
|
dataKey="compliance_rate"
|
|
name="Einhaltungsquote"
|
|
stroke="#171717"
|
|
strokeWidth={2}
|
|
dot={{ fill: "#171717", r: 4 }}
|
|
activeDot={{ r: 6 }}
|
|
/>
|
|
</LineChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</div>
|
|
|
|
{/* Missed deadlines 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">
|
|
Versäumte Fristen
|
|
</h3>
|
|
</div>
|
|
{data.missed.length === 0 ? (
|
|
<div className="px-5 py-8 text-center">
|
|
<CheckCircle className="mx-auto h-8 w-8 text-emerald-400" />
|
|
<p className="mt-2 text-sm text-neutral-500">
|
|
Keine versäumten Fristen im gewählten Zeitraum
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<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">Frist</th>
|
|
<th className="px-5 py-3 font-medium">Akte</th>
|
|
<th className="px-5 py-3 font-medium">Fällig am</th>
|
|
<th className="px-5 py-3 font-medium text-right">
|
|
Tage überfällig
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{data.missed.map((d) => (
|
|
<tr
|
|
key={d.id}
|
|
className="border-b border-neutral-50 last:border-b-0"
|
|
>
|
|
<td className="px-5 py-3 text-neutral-900">{d.title}</td>
|
|
<td className="px-5 py-3">
|
|
<Link
|
|
href={`/cases/${d.case_id}`}
|
|
className="text-neutral-600 hover:text-neutral-900"
|
|
>
|
|
{d.case_number} — {d.case_title}
|
|
</Link>
|
|
</td>
|
|
<td className="px-5 py-3 text-neutral-600">
|
|
{formatDate(d.due_date)}
|
|
</td>
|
|
<td className="px-5 py-3 text-right">
|
|
<span className="inline-flex items-center gap-1 rounded-full bg-red-50 px-2 py-0.5 text-xs font-medium text-red-700">
|
|
<AlertTriangle className="h-3 w-3" />
|
|
{d.days_overdue}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|