- Breadcrumb component: reusable nav with items array (label+href)
- DeadlineTrafficLights: buttons → Links to /fristen?status={filter}
- CaseOverviewGrid: static metrics → clickable Links to /cases?status={filter}
- UpcomingTimeline: items → clickable Links to /fristen/{id} or /termine/{id}
with case number links and hover chevron
- QuickActions: swap CalDAV Sync for "Neuer Termin" → /termine/neu,
fix "Frist eintragen" → /fristen/neu
- AISummaryCard: add RefreshCw button with spinning animation
- RecentActivityList: new component showing recent case events
- DeadlineList: accept initialStatus prop, add this_week/ok filters
- fristen/page.tsx: read searchParams.status for initial filter
- Add breadcrumbs to dashboard, fristen, cases, termine pages
- Add RecentActivity type, update DashboardData type
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { Sparkles, RefreshCw } from "lucide-react";
|
|
import type { DashboardData } from "@/lib/types";
|
|
|
|
interface Props {
|
|
data: DashboardData;
|
|
onRefresh?: () => void;
|
|
}
|
|
|
|
function generateSummary(data: DashboardData): string {
|
|
const parts: string[] = [];
|
|
const ds = data.deadline_summary ?? { overdue_count: 0, due_this_week: 0, due_next_week: 0, ok_count: 0 };
|
|
const cs = data.case_summary ?? { active_count: 0, new_this_month: 0, closed_count: 0 };
|
|
const ud = Array.isArray(data.upcoming_deadlines) ? data.upcoming_deadlines : [];
|
|
|
|
// Deadline urgency
|
|
if (ds.overdue_count > 0) {
|
|
parts.push(
|
|
`${ds.overdue_count} Frist${ds.overdue_count > 1 ? "en" : ""} ${ds.overdue_count > 1 ? "sind" : "ist"} überfällig und ${ds.overdue_count > 1 ? "erfordern" : "erfordert"} sofortige Aufmerksamkeit.`,
|
|
);
|
|
}
|
|
|
|
if (ds.due_this_week > 0) {
|
|
parts.push(
|
|
`${ds.due_this_week} Frist${ds.due_this_week > 1 ? "en laufen" : " läuft"} diese Woche ab.`,
|
|
);
|
|
}
|
|
|
|
// Highlight most critical upcoming deadline
|
|
if (ud.length > 0) {
|
|
const next = ud[0];
|
|
parts.push(
|
|
`Die nächste Frist ist "${next.title}" in Akte ${next.case_number}.`,
|
|
);
|
|
}
|
|
|
|
// Case activity
|
|
if (cs.new_this_month > 0) {
|
|
parts.push(
|
|
`${cs.new_this_month} neue Akte${cs.new_this_month > 1 ? "n" : ""} diesen Monat bei ${cs.active_count} aktiven Verfahren.`,
|
|
);
|
|
} else {
|
|
parts.push(`${cs.active_count} aktive Verfahren.`);
|
|
}
|
|
|
|
// All good
|
|
if (ds.overdue_count === 0 && ds.due_this_week === 0) {
|
|
parts.unshift("Alle Fristen sind im Zeitplan.");
|
|
}
|
|
|
|
return parts.join(" ");
|
|
}
|
|
|
|
export function AISummaryCard({ data, onRefresh }: Props) {
|
|
const [spinning, setSpinning] = useState(false);
|
|
const summary = generateSummary(data);
|
|
|
|
function handleRefresh() {
|
|
if (!onRefresh) return;
|
|
setSpinning(true);
|
|
onRefresh();
|
|
setTimeout(() => setSpinning(false), 1000);
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-xl border border-neutral-200 bg-white p-5">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div className="rounded-md bg-violet-50 p-1.5">
|
|
<Sparkles className="h-4 w-4 text-violet-500" />
|
|
</div>
|
|
<h2 className="text-sm font-semibold text-neutral-900">
|
|
KI-Zusammenfassung
|
|
</h2>
|
|
</div>
|
|
{onRefresh && (
|
|
<button
|
|
onClick={handleRefresh}
|
|
title="Aktualisieren"
|
|
className="rounded-md p-1.5 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600"
|
|
>
|
|
<RefreshCw
|
|
className={`h-4 w-4 ${spinning ? "animate-spin" : ""}`}
|
|
/>
|
|
</button>
|
|
)}
|
|
</div>
|
|
<p className="mt-3 text-sm leading-relaxed text-neutral-700">
|
|
{summary}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|