- 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.9 KiB
TypeScript
110 lines
3.9 KiB
TypeScript
"use client";
|
|
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { api } from "@/lib/api";
|
|
import type { DashboardData } from "@/lib/types";
|
|
import { DeadlineTrafficLights } from "@/components/dashboard/DeadlineTrafficLights";
|
|
import { CaseOverviewGrid } from "@/components/dashboard/CaseOverviewGrid";
|
|
import { UpcomingTimeline } from "@/components/dashboard/UpcomingTimeline";
|
|
import { AISummaryCard } from "@/components/dashboard/AISummaryCard";
|
|
import { QuickActions } from "@/components/dashboard/QuickActions";
|
|
import { RecentActivityList } from "@/components/dashboard/RecentActivityList";
|
|
import { Breadcrumb } from "@/components/layout/Breadcrumb";
|
|
import { Skeleton, SkeletonCard } from "@/components/ui/Skeleton";
|
|
import { AlertTriangle, RefreshCw } from "lucide-react";
|
|
|
|
function DashboardSkeleton() {
|
|
return (
|
|
<div className="mx-auto max-w-6xl space-y-6">
|
|
<div>
|
|
<Skeleton className="h-5 w-28" />
|
|
<Skeleton className="mt-2 h-3.5 w-52" />
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
|
{[1, 2, 3].map((i) => (
|
|
<Skeleton key={i} className="h-28 rounded-xl" />
|
|
))}
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
<div className="lg:col-span-2">
|
|
<SkeletonCard className="min-h-[200px]" />
|
|
</div>
|
|
<div className="space-y-6">
|
|
<SkeletonCard />
|
|
<SkeletonCard />
|
|
<SkeletonCard />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function DashboardPage() {
|
|
const { data, isLoading, error, refetch } = useQuery({
|
|
queryKey: ["dashboard"],
|
|
queryFn: () => api.get<DashboardData>("/dashboard"),
|
|
refetchInterval: 60_000,
|
|
});
|
|
|
|
if (isLoading) {
|
|
return <DashboardSkeleton />;
|
|
}
|
|
|
|
if (error || !data) {
|
|
return (
|
|
<div className="mx-auto max-w-md py-16 text-center">
|
|
<div className="mx-auto mb-3 rounded-xl bg-red-50 p-3 w-fit">
|
|
<AlertTriangle className="h-6 w-6 text-red-500" />
|
|
</div>
|
|
<h2 className="text-sm font-medium text-neutral-900">
|
|
Dashboard konnte nicht geladen werden
|
|
</h2>
|
|
<p className="mt-1 text-sm text-neutral-500">
|
|
Bitte versuchen Sie es erneut oder prüfen Sie Ihre Verbindung.
|
|
</p>
|
|
<button
|
|
onClick={() => refetch()}
|
|
className="mt-4 inline-flex items-center gap-1.5 rounded-md bg-neutral-900 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-neutral-800"
|
|
>
|
|
<RefreshCw className="h-3.5 w-3.5" />
|
|
Erneut laden
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const recentActivity = Array.isArray(data.recent_activity) ? data.recent_activity : [];
|
|
|
|
return (
|
|
<div className="animate-fade-in mx-auto max-w-6xl space-y-6">
|
|
<div>
|
|
<Breadcrumb items={[{ label: "Dashboard" }]} />
|
|
<h1 className="text-lg font-semibold text-neutral-900">Dashboard</h1>
|
|
<p className="mt-0.5 text-sm text-neutral-500">
|
|
Fristenübersicht und Kanzlei-Status
|
|
</p>
|
|
</div>
|
|
|
|
<DeadlineTrafficLights data={data.deadline_summary ?? { overdue_count: 0, due_this_week: 0, due_next_week: 0, ok_count: 0 }} />
|
|
|
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
|
<div className="lg:col-span-2">
|
|
<UpcomingTimeline
|
|
deadlines={Array.isArray(data.upcoming_deadlines) ? data.upcoming_deadlines : []}
|
|
appointments={Array.isArray(data.upcoming_appointments) ? data.upcoming_appointments : []}
|
|
/>
|
|
</div>
|
|
<div className="space-y-6">
|
|
<CaseOverviewGrid data={data.case_summary ?? { active_count: 0, new_this_month: 0, closed_count: 0 }} />
|
|
<AISummaryCard data={data} onRefresh={() => refetch()} />
|
|
<QuickActions />
|
|
</div>
|
|
</div>
|
|
|
|
{recentActivity.length > 0 && (
|
|
<RecentActivityList activities={recentActivity} />
|
|
)}
|
|
</div>
|
|
);
|
|
}
|