- 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
178 lines
5.9 KiB
TypeScript
178 lines
5.9 KiB
TypeScript
"use client";
|
|
|
|
import Link from "next/link";
|
|
import { format, parseISO, isToday, isTomorrow } from "date-fns";
|
|
import { de } from "date-fns/locale";
|
|
import { Clock, Calendar, MapPin, ChevronRight } from "lucide-react";
|
|
import type { UpcomingDeadline, UpcomingAppointment } from "@/lib/types";
|
|
|
|
interface Props {
|
|
deadlines: UpcomingDeadline[];
|
|
appointments: UpcomingAppointment[];
|
|
}
|
|
|
|
type TimelineItem =
|
|
| { type: "deadline"; date: Date; data: UpcomingDeadline }
|
|
| { type: "appointment"; date: Date; data: UpcomingAppointment };
|
|
|
|
function formatDayLabel(date: Date): string {
|
|
if (isToday(date)) return "Heute";
|
|
if (isTomorrow(date)) return "Morgen";
|
|
return format(date, "EEEE, d. MMM", { locale: de });
|
|
}
|
|
|
|
export function UpcomingTimeline({ deadlines, appointments }: Props) {
|
|
const safeDeadlines = Array.isArray(deadlines) ? deadlines : [];
|
|
const safeAppointments = Array.isArray(appointments) ? appointments : [];
|
|
|
|
const items: TimelineItem[] = [
|
|
...safeDeadlines.map((d) => ({
|
|
type: "deadline" as const,
|
|
date: parseISO(d.due_date),
|
|
data: d,
|
|
})),
|
|
...safeAppointments.map((a) => ({
|
|
type: "appointment" as const,
|
|
date: parseISO(a.start_at),
|
|
data: a,
|
|
})),
|
|
].sort((a, b) => a.date.getTime() - b.date.getTime());
|
|
|
|
// Group by day
|
|
const grouped = new Map<string, TimelineItem[]>();
|
|
for (const item of items) {
|
|
const key = format(item.date, "yyyy-MM-dd");
|
|
const group = grouped.get(key) ?? [];
|
|
group.push(item);
|
|
grouped.set(key, group);
|
|
}
|
|
|
|
const empty = items.length === 0;
|
|
|
|
return (
|
|
<div className="rounded-xl border border-neutral-200 bg-white p-5">
|
|
<h2 className="text-sm font-semibold text-neutral-900">
|
|
Nächste 7 Tage
|
|
</h2>
|
|
{empty ? (
|
|
<p className="mt-6 text-center text-sm text-neutral-400">
|
|
Keine anstehenden Termine oder Fristen
|
|
</p>
|
|
) : (
|
|
<div className="mt-4 space-y-5">
|
|
{Array.from(grouped.entries()).map(([dateKey, dayItems]) => (
|
|
<div key={dateKey}>
|
|
<p className="text-xs font-medium uppercase tracking-wider text-neutral-400">
|
|
{formatDayLabel(dayItems[0].date)}
|
|
</p>
|
|
<div className="mt-2 space-y-2">
|
|
{dayItems.map((item, i) => (
|
|
<TimelineEntry key={`${item.type}-${i}`} item={item} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TimelineEntry({ item }: { item: TimelineItem }) {
|
|
if (item.type === "deadline") {
|
|
const d = item.data;
|
|
const href = `/fristen/${d.id}`;
|
|
return (
|
|
<Link
|
|
href={href}
|
|
className="group flex items-start gap-3 rounded-lg border border-neutral-100 bg-neutral-50/50 px-3 py-2.5 transition-colors hover:border-neutral-200 hover:bg-neutral-100/50"
|
|
>
|
|
<div className="mt-0.5 rounded-md bg-amber-50 p-1">
|
|
<Clock className="h-3.5 w-3.5 text-amber-500" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm font-medium text-neutral-900">
|
|
{d.title}
|
|
</p>
|
|
<p className="mt-0.5 truncate text-xs text-neutral-500">
|
|
{d.case_id ? (
|
|
<span
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="inline"
|
|
>
|
|
<Link
|
|
href={`/cases/${d.case_id}`}
|
|
className="underline decoration-neutral-300 hover:text-neutral-900 hover:decoration-neutral-500"
|
|
>
|
|
{d.case_number}
|
|
</Link>
|
|
{" · "}
|
|
</span>
|
|
) : (
|
|
<>{d.case_number} · </>
|
|
)}
|
|
{d.case_title}
|
|
</p>
|
|
</div>
|
|
<div className="flex shrink-0 items-center gap-1.5">
|
|
<span className="text-xs font-medium text-amber-600">Frist</span>
|
|
<ChevronRight className="h-3.5 w-3.5 text-neutral-300 transition-colors group-hover:text-neutral-500" />
|
|
</div>
|
|
</Link>
|
|
);
|
|
}
|
|
|
|
const a = item.data;
|
|
const href = `/termine/${a.id}`;
|
|
return (
|
|
<Link
|
|
href={href}
|
|
className="group flex items-start gap-3 rounded-lg border border-neutral-100 bg-neutral-50/50 px-3 py-2.5 transition-colors hover:border-neutral-200 hover:bg-neutral-100/50"
|
|
>
|
|
<div className="mt-0.5 rounded-md bg-blue-50 p-1">
|
|
<Calendar className="h-3.5 w-3.5 text-blue-500" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="truncate text-sm font-medium text-neutral-900">
|
|
{a.title}
|
|
</p>
|
|
<div className="mt-0.5 flex items-center gap-2 text-xs text-neutral-500">
|
|
<span>{format(item.date, "HH:mm")} Uhr</span>
|
|
{a.location && (
|
|
<>
|
|
<span className="text-neutral-300">·</span>
|
|
<span className="flex items-center gap-0.5 truncate">
|
|
<MapPin className="h-3 w-3" />
|
|
{a.location}
|
|
</span>
|
|
</>
|
|
)}
|
|
{a.case_number && a.case_id && (
|
|
<>
|
|
<span className="text-neutral-300">·</span>
|
|
<span onClick={(e) => e.stopPropagation()}>
|
|
<Link
|
|
href={`/cases/${a.case_id}`}
|
|
className="underline decoration-neutral-300 hover:text-neutral-900 hover:decoration-neutral-500"
|
|
>
|
|
{a.case_number}
|
|
</Link>
|
|
</span>
|
|
</>
|
|
)}
|
|
{a.case_number && !a.case_id && (
|
|
<>
|
|
<span className="text-neutral-300">·</span>
|
|
<span>{a.case_number}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<div className="flex shrink-0 items-center gap-1.5">
|
|
<span className="text-xs font-medium text-blue-600">Termin</span>
|
|
<ChevronRight className="h-3.5 w-3.5 text-neutral-300 transition-colors group-hover:text-neutral-500" />
|
|
</div>
|
|
</Link>
|
|
);
|
|
}
|