- 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
110 lines
3.3 KiB
TypeScript
110 lines
3.3 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import Link from "next/link";
|
|
import { AlertTriangle, Clock, CheckCircle } from "lucide-react";
|
|
import type { DeadlineSummary } from "@/lib/types";
|
|
|
|
function AnimatedCount({ value }: { value: number }) {
|
|
const ref = useRef<HTMLSpanElement>(null);
|
|
const prevValue = useRef(value);
|
|
|
|
useEffect(() => {
|
|
const el = ref.current;
|
|
if (!el || prevValue.current === value) return;
|
|
|
|
el.classList.remove("animate-count-up");
|
|
void el.offsetWidth;
|
|
el.classList.add("animate-count-up");
|
|
prevValue.current = value;
|
|
}, [value]);
|
|
|
|
return (
|
|
<span ref={ref} className="inline-block tabular-nums">
|
|
{value}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
interface Props {
|
|
data: DeadlineSummary;
|
|
}
|
|
|
|
export function DeadlineTrafficLights({ data }: Props) {
|
|
const safe = data ?? { overdue_count: 0, due_this_week: 0, due_next_week: 0, ok_count: 0 };
|
|
const cards = [
|
|
{
|
|
key: "overdue" as const,
|
|
label: "Überfällig",
|
|
count: safe.overdue_count ?? 0,
|
|
icon: AlertTriangle,
|
|
href: "/fristen?status=overdue",
|
|
bg: "bg-red-50",
|
|
border: "border-red-200",
|
|
iconColor: "text-red-500",
|
|
countColor: "text-red-700",
|
|
labelColor: "text-red-600",
|
|
ring: (safe.overdue_count ?? 0) > 0 ? "ring-2 ring-red-300 ring-offset-1" : "",
|
|
pulse: (safe.overdue_count ?? 0) > 0,
|
|
},
|
|
{
|
|
key: "this_week" as const,
|
|
label: "Diese Woche",
|
|
count: safe.due_this_week ?? 0,
|
|
icon: Clock,
|
|
href: "/fristen?status=this_week",
|
|
bg: "bg-amber-50",
|
|
border: "border-amber-200",
|
|
iconColor: "text-amber-500",
|
|
countColor: "text-amber-700",
|
|
labelColor: "text-amber-600",
|
|
ring: "",
|
|
pulse: false,
|
|
},
|
|
{
|
|
key: "ok" as const,
|
|
label: "Im Zeitplan",
|
|
count: (safe.ok_count ?? 0) + (safe.due_next_week ?? 0),
|
|
icon: CheckCircle,
|
|
href: "/fristen?status=ok",
|
|
bg: "bg-emerald-50",
|
|
border: "border-emerald-200",
|
|
iconColor: "text-emerald-500",
|
|
countColor: "text-emerald-700",
|
|
labelColor: "text-emerald-600",
|
|
ring: "",
|
|
pulse: false,
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
{cards.map((card) => (
|
|
<Link
|
|
key={card.key}
|
|
href={card.href}
|
|
className={`group relative overflow-hidden rounded-xl border ${card.border} ${card.bg} ${card.ring} p-6 text-left transition-all hover:shadow-md active:scale-[0.98]`}
|
|
>
|
|
{card.pulse && (
|
|
<span className="absolute right-4 top-4 flex h-3 w-3">
|
|
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-red-400 opacity-75" />
|
|
<span className="relative inline-flex h-3 w-3 rounded-full bg-red-500" />
|
|
</span>
|
|
)}
|
|
<div className="flex items-center gap-3">
|
|
<div className={`rounded-lg p-2 ${card.bg}`}>
|
|
<card.icon className={`h-5 w-5 ${card.iconColor}`} />
|
|
</div>
|
|
<span className={`text-sm font-medium ${card.labelColor}`}>
|
|
{card.label}
|
|
</span>
|
|
</div>
|
|
<div className={`mt-4 text-4xl font-bold tracking-tight ${card.countColor}`}>
|
|
<AnimatedCount value={card.count} />
|
|
</div>
|
|
</Link>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|