fix(boss-dashboard): guard fetch races, surface errors, tighten response type

This commit is contained in:
Dave 2026-08-28 00:19:12 +02:00
parent f09f9163c0
commit 16efafc111
2 changed files with 43 additions and 27 deletions

View file

@ -6,6 +6,7 @@ import { RefreshCw } from "lucide-react";
import getPocketBase from "@/lib/pocketbase";
import type {
BossDashboardData,
BossDashboardResponse,
BossWindow,
} from "@/lib/server/boss-dashboard-service";
@ -17,42 +18,43 @@ export default function BossDashboardView() {
const [window, setWindow] = useState<BossWindow>("today");
const [loading, setLoading] = useState(true);
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
const [error, setError] = useState<string | null>(null);
const reqIdRef = useRef(0);
const fetchData = useCallback(async () => {
// Monotonic request id — discards late responses when a newer fetch
// has started (e.g. tab switch). Survives unmount because refs outlive
// the component instance.
const myId = ++reqIdRef.current;
setLoading(true);
setError(null);
try {
const pb = getPocketBase();
const pbToken = pb.authStore?.token;
const headers: Record<string, string> = {};
if (pbToken) headers.Authorization = pbToken;
const token = pb.authStore?.token ?? "";
const res = await fetch(`/api/v1/boss-dashboard?window=${window}`, {
headers,
headers: { Authorization: token },
cache: "no-store",
});
if (res.status === 401) {
document.location.href = "/login";
return;
}
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as { error?: string };
throw new Error(body.error ?? `HTTP ${res.status}`);
if (res.status === 401) {
document.location.href = "/login";
return;
}
throw new Error(`HTTP ${res.status}`);
}
const next = (await res.json()) as BossDashboardData & { viewerName?: string };
setData({
window: next.window,
rangeStart: next.rangeStart,
rangeEnd: next.rangeEnd,
orderTotals: next.orderTotals,
reps: next.reps,
drivers: next.drivers,
});
setViewerName(next.viewerName ?? null);
const body = (await res.json()) as BossDashboardResponse;
if (myId !== reqIdRef.current) return; // stale — newer fetch in flight
setData(body);
setViewerName(body.viewerName);
setLastRefreshed(new Date());
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to load";
toast.error(message);
if (myId !== reqIdRef.current) return;
const message =
err instanceof Error ? err.message : "Failed to load dashboard";
toast.error("Couldn't refresh", { description: message });
setError(message);
} finally {
setLoading(false);
if (myId === reqIdRef.current) setLoading(false);
}
}, [window]);
@ -73,9 +75,13 @@ export default function BossDashboardView() {
Good {greeting()}, <span className="text-cyan-400">{firstName}</span>.
</h1>
<p className="text-xs text-muted-foreground mt-0.5">
{lastRefreshed
? `Updated ${timeAgo(lastRefreshed)}`
: "Loading…"}
{error
? "Couldn't refresh"
: loading
? "Loading…"
: lastRefreshed
? `Updated ${timeAgo(lastRefreshed)}`
: "Loading…"}
</p>
</div>
<button

View file

@ -79,6 +79,16 @@ export interface BossDashboardData {
drivers: BossDriverRow[];
}
/**
* Shape of the JSON the `/api/v1/boss-dashboard` route returns to the
* client. The service returns `BossDashboardData`; the route merges in
* `viewerName` from the auth-refresh response. Using a named alias here
* keeps the client cast honest instead of an inline intersection.
*/
export type BossDashboardResponse = BossDashboardData & {
viewerName: string | null;
};
/** Quote statuses that count as "the rep finished this one". */
const DONE_STATUSES = ["emailed", "approved", "sent"];