From f09f9163c01a70a6b04aea91fd72ada81c97439d Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 28 Aug 2026 00:16:33 +0200 Subject: [PATCH] feat(boss-dashboard): add /m page + dashboard-view skeleton --- src/app/(mobile)/m/dashboard-view.tsx | 132 ++++++++++++++++++++++++++ src/app/(mobile)/m/page.tsx | 7 ++ 2 files changed, 139 insertions(+) create mode 100644 src/app/(mobile)/m/dashboard-view.tsx create mode 100644 src/app/(mobile)/m/page.tsx diff --git a/src/app/(mobile)/m/dashboard-view.tsx b/src/app/(mobile)/m/dashboard-view.tsx new file mode 100644 index 0000000..28f652b --- /dev/null +++ b/src/app/(mobile)/m/dashboard-view.tsx @@ -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(null); + const [viewerName, setViewerName] = useState(null); + const [window, setWindow] = useState("today"); + const [loading, setLoading] = useState(true); + const [lastRefreshed, setLastRefreshed] = useState(null); + + const fetchData = useCallback(async () => { + try { + const pb = getPocketBase(); + const pbToken = pb.authStore?.token; + const headers: Record = {}; + 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 ( +
+ {/* Header strip */} +
+
+

+ Good {greeting()}, {firstName}. +

+

+ {lastRefreshed + ? `Updated ${timeAgo(lastRefreshed)}` + : "Loading…"} +

+
+ +
+ + {/* Placeholder for tabs (Task 10) */} +
+ Tabs land here in Task 10. +
+ + {/* Placeholder for order totals (Task 11) */} +
+ Order totals land here in Task 11. +
+ + {/* Placeholder for reps (Task 12) */} +
+ Reps list lands here in Task 12. +
+ + {/* Placeholder for drivers (Task 13) */} +
+ Driver stops land here in Task 13. +
+
+ ); +} + +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" }); +} diff --git a/src/app/(mobile)/m/page.tsx b/src/app/(mobile)/m/page.tsx new file mode 100644 index 0000000..9e020a9 --- /dev/null +++ b/src/app/(mobile)/m/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import BossDashboardView from "./dashboard-view"; + +export default function BossDashboardPage() { + return ; +}