feat: Phase B — interactive dashboard, breadcrumbs, clickable navigation

- 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
This commit is contained in:
m
2026-03-25 19:29:13 +01:00
parent 9ad58e1ba3
commit 84b178edbf
13 changed files with 357 additions and 87 deletions

View File

@@ -10,7 +10,14 @@ import { toast } from "sonner";
import { useState, useMemo } from "react";
import { EmptyState } from "@/components/ui/EmptyState";
type StatusFilter = "all" | "pending" | "completed" | "overdue";
type StatusFilter = "all" | "pending" | "completed" | "overdue" | "this_week" | "ok";
function mapUrlStatus(status?: string): StatusFilter {
if (status === "overdue") return "overdue";
if (status === "this_week") return "this_week";
if (status === "ok") return "ok";
return "all";
}
function getUrgency(deadline: Deadline): "red" | "amber" | "green" {
if (deadline.status === "completed") return "green";
@@ -47,9 +54,15 @@ const urgencyConfig = {
const selectClass =
"rounded-md border border-neutral-200 bg-white px-2.5 py-1 text-sm text-neutral-700 transition-colors focus:border-neutral-400 focus:ring-1 focus:ring-neutral-400 outline-none";
export function DeadlineList() {
interface Props {
initialStatus?: string;
}
export function DeadlineList({ initialStatus }: Props) {
const queryClient = useQueryClient();
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all");
const [statusFilter, setStatusFilter] = useState<StatusFilter>(
mapUrlStatus(initialStatus),
);
const [caseFilter, setCaseFilter] = useState<string>("all");
const { data: deadlines, isLoading } = useQuery({
@@ -90,6 +103,18 @@ export function DeadlineList() {
if (d.status === "completed") return false;
if (!isPast(parseISO(d.due_date))) return false;
}
if (statusFilter === "this_week") {
if (d.status === "completed") return false;
const due = parseISO(d.due_date);
if (isPast(due)) return false;
if (!isThisWeek(due, { weekStartsOn: 1 })) return false;
}
if (statusFilter === "ok") {
if (d.status === "completed") return false;
const due = parseISO(d.due_date);
if (isPast(due)) return false;
if (isThisWeek(due, { weekStartsOn: 1 })) return false;
}
if (caseFilter !== "all" && d.case_id !== caseFilter) return false;
return true;
});
@@ -144,10 +169,10 @@ export function DeadlineList() {
</button>
<button
onClick={() =>
setStatusFilter(statusFilter === "pending" ? "all" : "pending")
setStatusFilter(statusFilter === "this_week" ? "all" : "this_week")
}
className={`rounded-lg border p-3 text-left transition-all ${
statusFilter === "pending"
statusFilter === "this_week"
? "border-amber-300 bg-amber-50 ring-1 ring-amber-200"
: "border-neutral-200 bg-white hover:bg-neutral-50"
}`}
@@ -158,9 +183,11 @@ export function DeadlineList() {
<div className="text-xs text-neutral-500">Diese Woche</div>
</button>
<button
onClick={() => setStatusFilter("all")}
onClick={() =>
setStatusFilter(statusFilter === "ok" ? "all" : "ok")
}
className={`rounded-lg border p-3 text-left transition-all ${
statusFilter === "all"
statusFilter === "ok"
? "border-green-300 bg-green-50 ring-1 ring-green-200"
: "border-neutral-200 bg-white hover:bg-neutral-50"
}`}
@@ -187,6 +214,8 @@ export function DeadlineList() {
<option value="pending">Offen</option>
<option value="completed">Erledigt</option>
<option value="overdue">Überfällig</option>
<option value="this_week">Diese Woche</option>
<option value="ok">Im Zeitplan</option>
</select>
{Array.isArray(cases) && cases.length > 0 && (
<select