Prevents "M.forEach is not a function" crashes when API returns error objects or unexpected shapes instead of arrays. Guards all useQuery consumers with Array.isArray checks and safe defaults for object props. Files fixed: DeadlineList, AppointmentList, TenantSwitcher, DeadlineTrafficLights, UpcomingTimeline, CaseOverviewGrid, AISummaryCard, TeamSettings, and all page-level components (dashboard, cases, fristen, termine, ai/extract).
101 lines
3.5 KiB
TypeScript
101 lines
3.5 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 { 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>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="animate-fade-in mx-auto max-w-6xl space-y-6">
|
|
<div>
|
|
<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} />
|
|
<QuickActions />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|