feat(boss-dashboard): add /m page + dashboard-view skeleton

This commit is contained in:
Dave 2026-08-28 00:16:33 +02:00
parent fcbc085723
commit f09f9163c0
2 changed files with 139 additions and 0 deletions

View file

@ -0,0 +1,132 @@
"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { toast } from "sonner";
import { RefreshCw } from "lucide-react";
import getPocketBase from "@/lib/pocketbase";
import type {
BossDashboardData,
BossWindow,
} from "@/lib/server/boss-dashboard-service";
const REALTIME_DEBOUNCE_MS = 1_000;
export default function BossDashboardView() {
const [data, setData] = useState<BossDashboardData | null>(null);
const [viewerName, setViewerName] = useState<string | null>(null);
const [window, setWindow] = useState<BossWindow>("today");
const [loading, setLoading] = useState(true);
const [lastRefreshed, setLastRefreshed] = useState<Date | null>(null);
const fetchData = useCallback(async () => {
try {
const pb = getPocketBase();
const pbToken = pb.authStore?.token;
const headers: Record<string, string> = {};
if (pbToken) headers.Authorization = pbToken;
const res = await fetch(`/api/v1/boss-dashboard?window=${window}`, {
headers,
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}`);
}
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);
setLastRefreshed(new Date());
} catch (err) {
const message = err instanceof Error ? err.message : "Failed to load";
toast.error(message);
} finally {
setLoading(false);
}
}, [window]);
useEffect(() => {
void fetchData();
}, [fetchData]);
// Realtime subscription (added in Task 14).
const firstName = (viewerName ?? "").split(" ")[0] || "there";
return (
<div className="mx-auto max-w-md px-4 py-4 space-y-4">
{/* Header strip */}
<header className="flex items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-semibold tracking-tight">
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…"}
</p>
</div>
<button
type="button"
onClick={() => void fetchData()}
disabled={loading}
aria-label="Refresh"
className="grid h-9 w-9 place-items-center rounded-full border border-cyan-500/30 text-cyan-400 disabled:opacity-50"
>
<RefreshCw className={`h-4 w-4 ${loading ? "animate-spin" : ""}`} />
</button>
</header>
{/* Placeholder for tabs (Task 10) */}
<div className="rounded-xl bg-muted/30 p-3 text-xs text-muted-foreground">
Tabs land here in Task 10.
</div>
{/* Placeholder for order totals (Task 11) */}
<div className="rounded-xl bg-muted/30 p-3 text-xs text-muted-foreground">
Order totals land here in Task 11.
</div>
{/* Placeholder for reps (Task 12) */}
<div className="rounded-xl bg-muted/30 p-3 text-xs text-muted-foreground">
Reps list lands here in Task 12.
</div>
{/* Placeholder for drivers (Task 13) */}
<div className="rounded-xl bg-muted/30 p-3 text-xs text-muted-foreground">
Driver stops land here in Task 13.
</div>
</div>
);
}
function greeting(): string {
const h = new Date().getHours();
if (h < 12) return "morning";
if (h < 17) return "afternoon";
return "evening";
}
function timeAgo(date: Date): string {
const diff = Math.max(0, Date.now() - date.getTime());
const sec = Math.floor(diff / 1000);
if (sec < 5) return "just now";
if (sec < 60) return `${sec}s ago`;
const min = Math.floor(sec / 60);
if (min < 60) return `${min}m ago`;
const hr = Math.floor(min / 60);
if (hr < 24) return `${hr}h ago`;
return date.toLocaleDateString("en-ZA", { day: "numeric", month: "short" });
}

View file

@ -0,0 +1,7 @@
"use client";
import BossDashboardView from "./dashboard-view";
export default function BossDashboardPage() {
return <BossDashboardView />;
}