From 70807e34fe368c33139348569488e7bfe38cb65a Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 28 Aug 2026 00:05:20 +0200 Subject: [PATCH] feat(boss-dashboard): add /api/v1/boss-dashboard/rep-activity route handler --- .../v1/boss-dashboard/rep-activity/route.ts | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 src/app/api/v1/boss-dashboard/rep-activity/route.ts diff --git a/src/app/api/v1/boss-dashboard/rep-activity/route.ts b/src/app/api/v1/boss-dashboard/rep-activity/route.ts new file mode 100644 index 0000000..ed3f674 --- /dev/null +++ b/src/app/api/v1/boss-dashboard/rep-activity/route.ts @@ -0,0 +1,88 @@ +import { NextRequest, NextResponse } from "next/server"; +import { + getBossRepActivity, + type BossWindow, +} from "@/lib/server/boss-dashboard-service"; + +/** + * GET /api/v1/boss-dashboard/rep-activity?rep=&window=today|week|month&kind=quotes|orders&page=1 + * + * Paginated list of quotes or orders for one rep within the active + * window. Drives the drill-down bottom sheet on the boss's phone + * dashboard at `/m`. Returns up to 25 items per page plus a `hasMore` + * flag. Auth via the PB browser session. + */ +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +const VALID_WINDOWS: BossWindow[] = ["today", "week", "month"]; +const VALID_KINDS = ["quotes", "orders"] as const; +type ActivityKind = (typeof VALID_KINDS)[number]; + +async function assertBrowserSession(req: NextRequest) { + const auth = req.headers.get("authorization"); + if (!auth) { + throw Object.assign(new Error("Not authenticated."), { status: 401 }); + } + const pbUrl = + process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090"; + const token = auth.startsWith("Bearer ") ? auth.slice(7) : auth; + const res = await fetch( + `${pbUrl.replace(/\/$/, "")}/api/collections/users/auth-refresh`, + { + method: "POST", + headers: { Authorization: token }, + signal: AbortSignal.timeout(5000), + }, + ); + if (!res.ok) { + throw Object.assign(new Error("Invalid or expired session."), { + status: 401, + }); + } + return res.json(); +} + +export async function GET(req: NextRequest) { + try { + await assertBrowserSession(req); + + const url = new URL(req.url); + const rep = url.searchParams.get("rep"); + if (!rep) { + return NextResponse.json({ error: "Missing rep." }, { status: 400 }); + } + + const window = url.searchParams.get("window") ?? "today"; + if (!VALID_WINDOWS.includes(window as BossWindow)) { + return NextResponse.json( + { error: `Invalid window: ${window}` }, + { status: 400 }, + ); + } + + const kind = url.searchParams.get("kind") ?? "quotes"; + if (!VALID_KINDS.includes(kind as ActivityKind)) { + return NextResponse.json( + { error: `Invalid kind: ${kind}` }, + { status: 400 }, + ); + } + + const pageParam = url.searchParams.get("page"); + const page = pageParam ? Math.max(1, parseInt(pageParam, 10) || 1) : 1; + + const data = await getBossRepActivity({ + rep, + window: window as BossWindow, + kind: kind as ActivityKind, + page, + }); + return NextResponse.json(data); + } catch (error) { + const message = + error instanceof Error ? error.message : "Failed to load activity"; + const status = (error as Error & { status?: number }).status || 500; + return NextResponse.json({ error: message }, { status }); + } +} \ No newline at end of file