From 9101871fef9336319b0a831d755bae62a2e4a2ee Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 28 Aug 2026 00:24:46 +0200 Subject: [PATCH] feat(boss-dashboard): add rep-activity bottom sheet --- src/app/(mobile)/m/dashboard-view.tsx | 189 ++++++++++++++++++++++++-- 1 file changed, 181 insertions(+), 8 deletions(-) diff --git a/src/app/(mobile)/m/dashboard-view.tsx b/src/app/(mobile)/m/dashboard-view.tsx index 94b73e7..6d17e43 100644 --- a/src/app/(mobile)/m/dashboard-view.tsx +++ b/src/app/(mobile)/m/dashboard-view.tsx @@ -7,7 +7,9 @@ import getPocketBase from "@/lib/pocketbase"; import type { BossDashboardData, BossDashboardResponse, + BossRepRow, BossWindow, + RepActivityItem, } from "@/lib/server/boss-dashboard-service"; const REALTIME_DEBOUNCE_MS = 1_000; @@ -15,12 +17,21 @@ const REALTIME_DEBOUNCE_MS = 1_000; export default function BossDashboardView() { const [data, setData] = useState(null); const [viewerName, setViewerName] = useState(null); - const [window, setWindow] = useState("today"); + const [activeWindow, setActiveWindow] = useState("today"); const [loading, setLoading] = useState(true); const [lastRefreshed, setLastRefreshed] = useState(null); const [error, setError] = useState(null); const reqIdRef = useRef(0); + // Sheet (drill-down) state — populated when a rep card is tapped. + const [sheetRep, setSheetRep] = useState(null); + const [sheetKind, setSheetKind] = useState<"quotes" | "orders">("quotes"); + const [sheetItems, setSheetItems] = useState([]); + const [sheetPage, setSheetPage] = useState(1); + const [sheetHasMore, setSheetHasMore] = useState(false); + const [sheetLoading, setSheetLoading] = useState(false); + const dialogRef = useRef(null); + 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 @@ -31,7 +42,7 @@ export default function BossDashboardView() { try { const pb = getPocketBase(); const token = pb.authStore?.token ?? ""; - const res = await fetch(`/api/v1/boss-dashboard?window=${window}`, { + const res = await fetch(`/api/v1/boss-dashboard?window=${activeWindow}`, { headers: { Authorization: token }, cache: "no-store", }); @@ -56,7 +67,7 @@ export default function BossDashboardView() { } finally { if (myId === reqIdRef.current) setLoading(false); } - }, [window]); + }, [activeWindow]); useEffect(() => { void fetchData(); @@ -66,6 +77,53 @@ export default function BossDashboardView() { const firstName = (viewerName ?? "").split(" ")[0] || "there"; + // --- Rep-activity bottom sheet ------------------------------------------ + async function openSheet(rep: BossRepRow) { + if (rep.isUnknown) return; + setSheetRep(rep); + setSheetKind("quotes"); + setSheetPage(1); + setSheetItems([]); + dialogRef.current?.showModal(); + await loadSheetPage(rep.id, "quotes", 1, true); + } + + async function loadSheetPage( + repId: string, + kind: "quotes" | "orders", + page: number, + replace: boolean, + ) { + setSheetLoading(true); + try { + const pb = getPocketBase(); + const headers: Record = {}; + if (pb.authStore?.token) headers.Authorization = pb.authStore.token; + + const res = await fetch( + `/api/v1/boss-dashboard/rep-activity?rep=${encodeURIComponent(repId)}&window=${activeWindow}&kind=${kind}&page=${page}`, + { headers, cache: "no-store" }, + ); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const data = (await res.json()) as { + items: RepActivityItem[]; + hasMore: boolean; + }; + setSheetItems((prev) => (replace ? data.items : [...prev, ...data.items])); + setSheetHasMore(data.hasMore); + } catch (err) { + toast.error(err instanceof Error ? err.message : "Failed to load"); + } finally { + setSheetLoading(false); + } + } + + function closeSheet() { + dialogRef.current?.close(); + setSheetRep(null); + setSheetItems([]); + } + return (
{/* Header strip */} @@ -102,10 +160,10 @@ export default function BossDashboardView() { key={w} type="button" role="tab" - aria-selected={window === w} - onClick={() => setWindow(w)} + aria-selected={activeWindow === w} + onClick={() => setActiveWindow(w)} className={`flex-1 rounded-lg px-3 py-2 text-sm font-medium capitalize transition-colors ${ - window === w + activeWindow === w ? "bg-cyan-500/15 text-cyan-300" : "text-muted-foreground hover:text-foreground" }`} @@ -241,6 +299,114 @@ export default function BossDashboardView() { )} + + {/* Rep-activity bottom sheet */} + setSheetRep(null)} + > + {sheetRep && ( +
+
+
+
{sheetRep.name}
+
+ {sheetRep.quotesLoaded} loaded · {sheetRep.quotesDoneBy} done + · {sheetRep.ordersPlaced} orders +
+
+ +
+ + {/* Tab bar */} +
+ {(["quotes", "orders"] as const).map((k) => ( + + ))} +
+ + {/* List */} +
    + {sheetItems.map((item) => ( +
  • + +
  • + ))} +
+ + {sheetHasMore && ( + + )} + + {sheetItems.length === 0 && !sheetLoading && ( +
+ No {sheetKind} in this window. +
+ )} +
+ )} +
); } @@ -306,6 +472,13 @@ function Metric({ ); } -function openSheet(_rep: BossDashboardData["reps"][number]) { - // Wired in Task 14. +function formatAge(ms: number): string { + const sec = Math.floor(ms / 1000); + if (sec < 60) return `${sec}s`; + 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`; + const days = Math.floor(hr / 24); + return `${days}d ago`; }