fix(boss-dashboard): guard fetch races, surface errors, tighten response type
This commit is contained in:
parent
f09f9163c0
commit
16efafc111
2 changed files with 43 additions and 27 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue