fix(boss-dashboard): filter to active reps only + fix PB date-filter bug
The reps list seeded a row for every `users` record (admin, test accounts, drivers, duplicate entries), not just the 6 real sales reps. Also, window filters built with toISOString() used a "T"-separated date PB's filter engine doesn't match against `created`, so every window silently returned zero rows — the whole dashboard read 0 regardless of real activity. Same fix pattern already used in order-delivery-note-link-service.ts / order-order-duplicate-link-service.ts. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
40993ce8f8
commit
f05fc5d9cf
3 changed files with 66 additions and 9 deletions
|
|
@ -7,7 +7,7 @@ vi.mock("@/lib/server/boss-dashboard-service", () => ({
|
|||
getBossDashboardData: (...args: unknown[]) => getBossDashboardData(...args),
|
||||
}));
|
||||
|
||||
import { GET } from "./route";
|
||||
import { ACTIVE_REP_IDS, GET } from "./route";
|
||||
|
||||
// --- Helpers --------------------------------------------------------------
|
||||
|
||||
|
|
@ -120,8 +120,8 @@ describe("GET /api/v1/boss-dashboard", () => {
|
|||
orderTotals: { total: 7, delivered: 3, invoiced: 1 },
|
||||
reps: [
|
||||
{
|
||||
id: "u1",
|
||||
name: "Alice",
|
||||
id: "krpyec9lblra0qz", // Gary — one of the active reps
|
||||
name: "Gary",
|
||||
isUnknown: false,
|
||||
quotesLoaded: 4,
|
||||
quotesDoneFor: 2,
|
||||
|
|
@ -144,7 +144,7 @@ describe("GET /api/v1/boss-dashboard", () => {
|
|||
expect(body.rangeEnd).toBe("2026-08-29T00:00:00.000Z");
|
||||
expect(body.orderTotals).toEqual({ total: 7, delivered: 3, invoiced: 1 });
|
||||
expect(body.reps).toHaveLength(1);
|
||||
expect(body.reps[0].name).toBe("Alice");
|
||||
expect(body.reps[0].name).toBe("Gary");
|
||||
|
||||
// viewerName merged from the auth-refresh response.
|
||||
expect(body.viewerName).toBe("Dave");
|
||||
|
|
@ -204,6 +204,31 @@ describe("GET /api/v1/boss-dashboard", () => {
|
|||
expect((await res.json()).error).toBe("PB down");
|
||||
});
|
||||
|
||||
it("filters reps down to the active allow-list, dropping everyone else", async () => {
|
||||
mockAuthRefreshOk();
|
||||
const [firstActiveId] = ACTIVE_REP_IDS;
|
||||
getBossDashboardData.mockResolvedValueOnce({
|
||||
window: "today",
|
||||
rangeStart: "2026-08-28T00:00:00.000Z",
|
||||
rangeEnd: "2026-08-29T00:00:00.000Z",
|
||||
orderTotals: { total: 0, delivered: 0, invoiced: 0 },
|
||||
reps: [
|
||||
{ id: firstActiveId, name: "Active Rep", isUnknown: false, quotesLoaded: 1, quotesDoneFor: 0, quotesDoneBy: 0, ordersPlaced: 0 },
|
||||
{ id: "not-in-allow-list", name: "Admin", isUnknown: false, quotesLoaded: 5, quotesDoneFor: 0, quotesDoneBy: 0, ordersPlaced: 0 },
|
||||
{ id: "", name: "(unknown)", isUnknown: true, quotesLoaded: 2, quotesDoneFor: 0, quotesDoneBy: 0, ordersPlaced: 0 },
|
||||
],
|
||||
drivers: [],
|
||||
});
|
||||
|
||||
const res = await GET(
|
||||
authedRequest("http://localhost/api/v1/boss-dashboard?window=today"),
|
||||
);
|
||||
const body = await res.json();
|
||||
|
||||
expect(body.reps).toHaveLength(1);
|
||||
expect(body.reps[0].id).toBe(firstActiveId);
|
||||
});
|
||||
|
||||
it("forwards the 'week' window to the service", async () => {
|
||||
mockAuthRefreshOk();
|
||||
getBossDashboardData.mockResolvedValueOnce({
|
||||
|
|
|
|||
|
|
@ -13,6 +13,23 @@ export const dynamic = "force-dynamic";
|
|||
|
||||
const VALID_WINDOWS: BossWindow[] = ["today", "week", "month"];
|
||||
|
||||
/**
|
||||
* The boss only wants his active sales reps on this view. `users` also
|
||||
* holds admin/test/driver/picker accounts, which the service seeds as
|
||||
* zero-activity rep rows same as everyone else — filter them out here
|
||||
* rather than in the service, so the tested aggregation math stays
|
||||
* generic. Keyed by id (not name) so a display-name edit in PB admin
|
||||
* doesn't silently drop someone off the list.
|
||||
*/
|
||||
export const ACTIVE_REP_IDS: ReadonlySet<string> = new Set([
|
||||
"krpyec9lblra0qz", // Gary
|
||||
"54uj39i4juqkh9x", // Tiaan
|
||||
"wvfr6q8ppjnkkrs", // Chris
|
||||
"j3sgntvcqpc7gt5", // Conar
|
||||
"63zd0ys47pvs9w2", // Karin
|
||||
"hlu4yn214ceu11b", // Pieter
|
||||
]);
|
||||
|
||||
async function assertBrowserSession(req: NextRequest) {
|
||||
const auth = req.headers.get("authorization");
|
||||
if (!auth) {
|
||||
|
|
@ -51,7 +68,12 @@ export async function GET(req: NextRequest) {
|
|||
}
|
||||
|
||||
const data = await getBossDashboardData({ window: raw as BossWindow });
|
||||
return NextResponse.json({ ...data, viewerName: user?.record?.name ?? null });
|
||||
const reps = data.reps.filter((r) => ACTIVE_REP_IDS.has(r.id));
|
||||
return NextResponse.json({
|
||||
...data,
|
||||
reps,
|
||||
viewerName: user?.record?.name ?? null,
|
||||
});
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : "Failed to load dashboard";
|
||||
|
|
|
|||
|
|
@ -48,6 +48,16 @@ export function windowRange(window: BossWindow, now: Date = new Date()): WindowR
|
|||
return { rangeStart: start.toISOString(), rangeEnd: end.toISOString() };
|
||||
}
|
||||
|
||||
/**
|
||||
* PB filter clauses compare `created` (stored as "YYYY-MM-DD HH:mm:ss.sssZ")
|
||||
* against the literal string given — a "T"-separated ISO string like
|
||||
* `toISOString()` produces does not match and silently returns zero rows.
|
||||
* Same fix as `order-delivery-note-link-service.ts` / `order-order-duplicate-link-service.ts`.
|
||||
*/
|
||||
function toPbFilterDate(iso: string): string {
|
||||
return iso.replace("T", " ");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Boss dashboard aggregation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
@ -106,8 +116,8 @@ export async function getBossDashboardData({
|
|||
const todayRange = windowRange("today", now);
|
||||
const pb = await getAdminPocketBase();
|
||||
|
||||
const rangeFilter = `created >= "${rangeStart}" && created < "${rangeEnd}"`;
|
||||
const todayRangeFilter = `created >= "${todayRange.rangeStart}" && created < "${todayRange.rangeEnd}"`;
|
||||
const rangeFilter = `created >= "${toPbFilterDate(rangeStart)}" && created < "${toPbFilterDate(rangeEnd)}"`;
|
||||
const todayRangeFilter = `created >= "${toPbFilterDate(todayRange.rangeStart)}" && created < "${toPbFilterDate(todayRange.rangeEnd)}"`;
|
||||
|
||||
const [users, customers, orders, quotes, todayItems, orderItemsInWindow] = await Promise.all([
|
||||
pb.collection("users").getFullList({ fields: "id,name" }),
|
||||
|
|
@ -368,7 +378,7 @@ export async function getBossRepActivity({
|
|||
if (kind === "quotes") {
|
||||
// Quotes owned by this rep = claimed_by = rep.id
|
||||
const result = await pb.collection("quotes").getList(page, PAGE_SIZE, {
|
||||
filter: `claimed_by = "${rep}" && created >= "${rangeStart}" && created < "${rangeEnd}"`,
|
||||
filter: `claimed_by = "${rep}" && created >= "${toPbFilterDate(rangeStart)}" && created < "${toPbFilterDate(rangeEnd)}"`,
|
||||
sort: "-created",
|
||||
fields: "id,quote_number,customer,status,created",
|
||||
expand: "customer",
|
||||
|
|
@ -391,7 +401,7 @@ export async function getBossRepActivity({
|
|||
|
||||
// kind === "orders"
|
||||
const result = await pb.collection("orders").getList(page, PAGE_SIZE, {
|
||||
filter: `rep = "${rep}" && created >= "${rangeStart}" && created < "${rangeEnd}"`,
|
||||
filter: `rep = "${rep}" && created >= "${toPbFilterDate(rangeStart)}" && created < "${toPbFilterDate(rangeEnd)}"`,
|
||||
sort: "-created",
|
||||
fields: "id,order_number,customer,item_status,created",
|
||||
expand: "customer",
|
||||
|
|
|
|||
Loading…
Reference in a new issue