feat(boss-dashboard): add /m page + dashboard-view skeleton
This commit is contained in:
parent
fcbc085723
commit
f09f9163c0
2 changed files with 139 additions and 0 deletions
132
src/app/(mobile)/m/dashboard-view.tsx
Normal file
132
src/app/(mobile)/m/dashboard-view.tsx
Normal 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" });
|
||||
}
|
||||
7
src/app/(mobile)/m/page.tsx
Normal file
7
src/app/(mobile)/m/page.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import BossDashboardView from "./dashboard-view";
|
||||
|
||||
export default function BossDashboardPage() {
|
||||
return <BossDashboardView />;
|
||||
}
|
||||
Loading…
Reference in a new issue