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:
parent
b3965b1454
commit
f84d14fe4d
1 changed files with 56 additions and 9 deletions
|
|
@ -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’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’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 (
|
||||
|
|
|
|||
Loading…
Reference in a new issue