diff --git a/src/lib/server/boss-dashboard-service.test.ts b/src/lib/server/boss-dashboard-service.test.ts index a36e0ed..dc51e00 100644 --- a/src/lib/server/boss-dashboard-service.test.ts +++ b/src/lib/server/boss-dashboard-service.test.ts @@ -301,6 +301,52 @@ describe("getBossDashboardData", () => { expect(unknown?.quotesLoaded).toBe(1); }); + it("buckets unresolved orders.rep into (unknown) — not silently dropped", async () => { + // Regression: an order whose rep field is free-text that doesn't resolve + // to any user must surface in the (unknown) bucket, not vanish from + // ordersPlaced. Before this fix, the repOrders Map.set was skipped when + // resolveOrderRepId returned "" and the order disappeared entirely. + addRow("users", { id: "rep-1", name: "Gary Ndlovu" }); + + // rep "Old Rep Name" is set on the order but doesn't match any user id or name. + const orphanOrder = addRow("orders", { + rep: "Old Rep Name", + invoiced: false, + created: "2026-08-27T08:00:00.000Z", + }); + addRow("order_items", { order: orphanOrder, item_status: "pending", created: "2026-08-27T08:00:00.000Z" }); + + const data = await getBossDashboardData({ window: "today", now: fixedNow }); + const unknown = data.reps.find((r) => r.isUnknown); + + expect(unknown).toBeDefined(); + expect(unknown?.ordersPlaced).toBe(1); + expect(data.orderTotals.total).toBe(1); + }); + + it("matches orders.rep by case-insensitive user name (not id)", async () => { + // orders.rep has historically stored free-text names; resolveOrderRepId + // must fall back to a case-insensitive name match against users.name + // when no user with that id exists. This is the primary reason the + // helper exists. + addRow("users", { id: "rep-1", name: "Gary Ndlovu" }); + + // rep is a free-text name (with different casing) — no user with id="Gary Ndlovu". + addRow("orders", { + rep: "gary ndlovu", + invoiced: false, + created: "2026-08-27T08:00:00.000Z", + }); + + const data = await getBossDashboardData({ window: "today", now: fixedNow }); + const gary = data.reps.find((r) => r.id === "rep-1"); + + expect(gary).toBeDefined(); + expect(gary?.ordersPlaced).toBe(1); + // (unknown) must NOT be created when the name resolved. + expect(data.reps.find((r) => r.isUnknown)).toBeUndefined(); + }); + it("excludes quotes and orders outside the active window", async () => { addRow("users", { id: "rep-1", name: "Gary" }); addRow("customers", { id: "cust-a", rep: "rep-1" }); diff --git a/src/lib/server/boss-dashboard-service.ts b/src/lib/server/boss-dashboard-service.ts index cbd7a95..737e6ba 100644 --- a/src/lib/server/boss-dashboard-service.ts +++ b/src/lib/server/boss-dashboard-service.ts @@ -99,7 +99,7 @@ export async function getBossDashboardData({ const rangeFilter = `created >= "${rangeStart}" && created < "${rangeEnd}"`; const todayRangeFilter = `created >= "${todayRange.rangeStart}" && created < "${todayRange.rangeEnd}"`; - const [users, customers, orders, quotes, todayItems] = await Promise.all([ + const [users, customers, orders, quotes, todayItems, orderItemsInWindow] = await Promise.all([ pb.collection("users").getFullList({ fields: "id,name" }), pb.collection("customers").getFullList({ fields: "id,rep" }), pb.collection("orders").getFullList({ @@ -115,15 +115,15 @@ export async function getBossDashboardData({ filter: todayRangeFilter, fields: "id,drv_id,item_status,drv_status,drv_collected_at,created", }), + // Delivered + cancelled are derived from order_items.item_status (the orders + // collection doesn't carry item_status in PB). Active window only. No data + // dependency on the other five — safe to parallelise. + pb.collection("order_items").getFullList({ + filter: rangeFilter, + fields: "order,item_status", + }), ]); - // Delivered + cancelled are derived from order_items.item_status (the orders - // collection doesn't carry item_status in PB). Active window only. - const orderItemsInWindow = await pb.collection("order_items").getFullList({ - filter: rangeFilter, - fields: "order,item_status", - }); - const userById = new Map(); for (const u of users) { userById.set(u.id, { @@ -166,8 +166,14 @@ export async function getBossDashboardData({ if (rec.invoiced === true) invoiced += 1; if (isCancelled) continue; - const repId = resolveOrderRepId(rec, userById); - if (repId) repOrders.set(repId, (repOrders.get(repId) ?? 0) + 1); + const { id: repId, unresolved } = resolveOrderRepId(rec, userById); + // Unresolved orders.rep values bucket into (unknown), matching how + // claimed_by handles unresolvable values. Empty rep → silently skip. + if (unresolved) { + repOrders.set("", (repOrders.get("") ?? 0) + 1); + } else if (repId) { + repOrders.set(repId, (repOrders.get(repId) ?? 0) + 1); + } } // --- Per-rep quote stats -------------------------------------------------- @@ -218,6 +224,11 @@ export async function getBossDashboardData({ } for (const [repId, count] of repOrders) { + if (repId === "") { + // Unresolved orders.rep — bucket into (unknown). + ensureRep("", "(unknown)").ordersPlaced = count; + continue; + } const u = userById.get(repId); if (!u) continue; ensureRep(u.id, u.name).ordersPlaced = count; @@ -233,7 +244,10 @@ export async function getBossDashboardData({ a.name.localeCompare(b.name), ); const unknownRow = repRows.get(""); - if (unknownRow && (unknownRow.quotesLoaded > 0 || unknownRow.quotesDoneBy > 0)) { + if ( + unknownRow && + (unknownRow.quotesLoaded > 0 || unknownRow.quotesDoneBy > 0 || unknownRow.ordersPlaced > 0) + ) { reps.push(unknownRow); } @@ -274,16 +288,22 @@ export async function getBossDashboardData({ /** * `orders.rep` is historically either a PB user id or a free-text name. * Try the id lookup first, then a case-insensitive name match. + * + * Returns `{ id, unresolved }`: + * - `id !== ""` → matched a user; use `id`. + * - `id === ""` + `unresolved` → rep value present but didn't resolve any user; + * caller should bucket into `(unknown)`. + * - `id === ""` + !`unresolved` → rep value was empty; caller should skip. */ function resolveOrderRepId( order: Record, userById: Map, -): string { +): { id: string; unresolved: boolean } { const rep = String(order.rep ?? "").trim(); - if (!rep) return ""; - if (userById.has(rep)) return rep; + if (!rep) return { id: "", unresolved: false }; + if (userById.has(rep)) return { id: rep, unresolved: false }; for (const u of userById.values()) { - if (u.name.trim().toLowerCase() === rep.toLowerCase()) return u.id; + if (u.name.trim().toLowerCase() === rep.toLowerCase()) return { id: u.id, unresolved: false }; } - return ""; + return { id: "", unresolved: true }; }