fix(dashboard2): show error inline instead of getting stuck on Loading

- Track explicit `error` state; render an inline error panel with a Try-again button when the fetch fails and we have no data to display (previously the toast fired but the page sat on the Loading… screen forever)
- Add `content-type: application/json` header (matches /dashboard convention)
- Switch gate from `loading || !data` to `loading && !data` so we don't hide data once it's loaded
- Defer the initial fetch via setTimeout(0) and only start the realtime subscriptions AFTER the first successful load, so a slow first fetch doesn't race with realtime events
This commit is contained in:
Dave 2026-09-01 01:58:38 +02:00
parent b3965b1454
commit f84d14fe4d

View file

@ -11,6 +11,7 @@ import {
TrendingUp,
AlertCircle,
DollarSign,
RefreshCw,
} from "lucide-react";
import { toast } from "sonner";
import getPocketBase from "@/lib/pocketbase";
@ -43,12 +44,13 @@ export default function RepDashboardPage() {
const router = useRouter();
const [data, setData] = useState<RepDashboardData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
const pb = getPocketBase();
const pbToken = pb.authStore?.token;
const headers: Record<string, string> = {};
const headers: Record<string, string> = { "content-type": "application/json" };
if (pbToken) headers.Authorization = pbToken;
const response = await fetch("/api/v1/rep-dashboard", {
@ -60,23 +62,36 @@ export default function RepDashboardPage() {
throw new Error(body.error || `Rep dashboard failed (HTTP ${response.status})`);
}
setData((await response.json()) as RepDashboardData);
setError(null);
} catch (err: unknown) {
toast.error(err instanceof Error ? err.message : "Failed to load rep dashboard");
const message = err instanceof Error ? err.message : "Failed to load rep dashboard";
setError(message);
toast.error(message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void fetchData();
const handle = window.setTimeout(() => {
void fetchData();
}, 0);
return () => window.clearTimeout(handle);
}, [fetchData]);
// Realtime subscription with a single trailing-edge debounce. A burst of
// changes collapses to a single refetch — and we only subscribe AFTER
// the initial load completes, so a slow first fetch doesn't compete
// with the realtime subscription firing.
useEffect(() => {
if (!data) return;
const pb = getPocketBase();
let handle = 0;
const debounceRef = { handle: 0 as number };
const trigger = () => {
if (handle) window.clearTimeout(handle);
handle = window.setTimeout(() => void fetchData(), REALTIME_DEBOUNCE_MS);
if (debounceRef.handle) window.clearTimeout(debounceRef.handle);
debounceRef.handle = window.setTimeout(() => {
void fetchData();
}, REALTIME_DEBOUNCE_MS);
};
const subscribe = async () => {
await pb.collection("quotes").subscribe("*", trigger);
@ -85,14 +100,44 @@ export default function RepDashboardPage() {
};
void subscribe();
return () => {
if (handle) window.clearTimeout(handle);
if (debounceRef.handle) window.clearTimeout(debounceRef.handle);
void pb.collection("quotes").unsubscribe("*");
void pb.collection("orders").unsubscribe("*");
void pb.collection("order_items").unsubscribe("*");
};
}, [fetchData]);
}, [fetchData, data]);
if (loading || !data) {
if (error && !data) {
return (
<div className="space-y-4">
<div>
<h1 className="text-2xl font-bold tracking-tight">My Dashboard</h1>
<p className="text-muted-foreground text-sm">
Your quotes, outstanding work, and this month&rsquo;s numbers.
</p>
</div>
<div
className="rounded-lg border p-6 text-center"
style={{ borderColor: "var(--border)" }}
>
<AlertCircle className="h-7 w-7 mx-auto mb-2" style={{ color: CHART.red }} />
<p className="text-sm font-medium mb-1">Couldn&rsquo;t load your dashboard</p>
<p className="text-xs text-muted-foreground mb-4">{error}</p>
<button
type="button"
onClick={() => void fetchData()}
className="inline-flex items-center gap-1.5 rounded-md border px-3 py-1.5 text-xs font-medium hover:bg-muted/40"
style={{ borderColor: "var(--border)" }}
>
<RefreshCw className="h-3.5 w-3.5" />
Try again
</button>
</div>
</div>
);
}
if (loading && !data) {
return (
<div className="py-16 text-center text-muted-foreground">
Loading your dashboard…
@ -100,6 +145,8 @@ export default function RepDashboardPage() {
);
}
if (!data) return null;
const { quotesLoaded, quoteVolumeTrend, outstandingQuotes, outstandingOrders, monthRevenue } = data;
return (