diff --git a/src/lib/item-status-ui.test.ts b/src/lib/item-status-ui.test.ts new file mode 100644 index 0000000..18e79b9 --- /dev/null +++ b/src/lib/item-status-ui.test.ts @@ -0,0 +1,66 @@ +import { describe, it, expect } from "vitest"; +import { + ITEM_STATUS_VISUAL, + TAB_GROUP_ITEM_STATUSES, + itemStatusToTabGroup, + tabGroupFilter, + getItemStatusVisual, +} from "./item-status-ui"; + +describe("ITEM_STATUS_VISUAL — ready_delivery", () => { + it("is labelled 'Ready for Collection' (NOT 'Collected')", () => { + // ready_delivery means "supplier confirmed ready for pickup" — + // it sits BEFORE the collection-pool flow. Calling it + // "Collected" makes the operator think the driver already + // picked the goods up. + expect(ITEM_STATUS_VISUAL.ready_delivery.label).toBe("Ready for Collection"); + }); + + it("is visually distinct from the actual `collected` status", () => { + // Different label and different hex so the per-item pill is + // unambiguous in the order row. + expect(ITEM_STATUS_VISUAL.ready_delivery.label).not.toBe( + ITEM_STATUS_VISUAL.collected.label, + ); + expect(ITEM_STATUS_VISUAL.ready_delivery.hex).not.toBe( + ITEM_STATUS_VISUAL.collected.hex, + ); + }); +}); + +describe("TAB_GROUP_ITEM_STATUSES — ready_delivery belongs to collection_pool", () => { + it("is in the collection_pool tab group, not the collected tab group", () => { + expect(TAB_GROUP_ITEM_STATUSES.collection_pool).toContain("ready_delivery"); + expect(TAB_GROUP_ITEM_STATUSES.collected).not.toContain("ready_delivery"); + }); + + it("ready_delivery maps to the collection_pool tab group", () => { + expect(itemStatusToTabGroup("ready_delivery")).toBe("collection_pool"); + }); +}); + +describe("tabGroupFilter", () => { + it("emits a PB-friendly filter for the collection_pool tab", () => { + // ready_delivery must show up under the collection_pool filter + // so an order containing a marked-ready item appears on that tab. + const filter = tabGroupFilter("collection_pool"); + expect(filter).toContain(`item_status = "ready_delivery"`); + expect(filter).toContain(`item_status = "collection_pool"`); + expect(filter).toContain(`item_status = "assigned"`); + }); + + it("does NOT include ready_delivery under the collected filter", () => { + expect(tabGroupFilter("collected")).not.toContain("ready_delivery"); + }); +}); + +describe("getItemStatusVisual", () => { + it("returns the 'Ready for Collection' visual for ready_delivery", () => { + expect(getItemStatusVisual("ready_delivery").label).toBe("Ready for Collection"); + }); + + it("falls back to a sane visual for unknown statuses", () => { + const v = getItemStatusVisual("definitely-not-a-real-status"); + expect(v.label).toBe("Unknown"); + }); +}); \ No newline at end of file diff --git a/src/lib/item-status-ui.ts b/src/lib/item-status-ui.ts index 71115d7..0f84a67 100644 --- a/src/lib/item-status-ui.ts +++ b/src/lib/item-status-ui.ts @@ -43,12 +43,17 @@ export const ITEM_STATUS_VISUAL: Record = { pending: { hex: "#2563eb", label: "New" }, processed: { hex: "#b45309", label: "Processing" }, po_generated: { hex: "#b45309", label: "Processing" }, + // Pre-pickup collection flow. ready_delivery ("supplier confirmed + // ready for pickup") sits BEFORE the pool — it shares the purple + // hue with collection_pool/assigned so the operator sees the + // family, but gets its own label so they don't confuse it with + // the actual `collected` status (driver has picked up). collection_pool: { hex: "#7c3aed", label: "Collection Pool" }, assigned: { hex: "#7c3aed", label: "Collection Pool" }, + ready_delivery: { hex: "#7c3aed", label: "Ready for Collection" }, collected: { hex: "#0f766e", label: "Collected" }, stock_verified: { hex: "#0f766e", label: "Collected" }, picking: { hex: "#0f766e", label: "Collected" }, - ready_delivery: { hex: "#0f766e", label: "Collected" }, delivered: { hex: "#047857", label: "Delivered" }, cancelled: { hex: "#e11d48", label: "Cancelled" }, ready_invoice: { hex: "#1d4ed8", label: "Ready for Invoice" }, @@ -116,8 +121,13 @@ export const TAB_GROUP_ORDER: TabGroup[] = [ export const TAB_GROUP_ITEM_STATUSES: Record = { new: ["pending"], processing: ["processed", "po_generated"], - collection_pool: ["collection_pool", "assigned"], - collected: ["collected", "stock_verified", "picking", "ready_delivery"], + // Pre-pickup flow — collection_pool, assigned, AND ready_delivery + // (supplier confirmed ready for pickup) all live here. ready_delivery + // used to be lumped under "collected" which made it look like the + // item had already been picked up by the driver, but it sits + // *before* the pool, not after. + collection_pool: ["collection_pool", "assigned", "ready_delivery"], + collected: ["collected", "stock_verified", "picking"], delivered: ["delivered"], cancelled: ["cancelled"], }; diff --git a/src/lib/server/buyer-tasks-service.test.ts b/src/lib/server/buyer-tasks-service.test.ts index d296ffc..a2a3c51 100644 --- a/src/lib/server/buyer-tasks-service.test.ts +++ b/src/lib/server/buyer-tasks-service.test.ts @@ -220,7 +220,14 @@ describe("listBuyerTasks", () => { // so they don't pollute the wire payload. const filter = (getFullList.mock.calls[0]?.[0] as { filter: string } | undefined)?.filter; expect(filter).toContain(`purchase_order != ""`); + // Marking ready (from the buyer-tasks MarkReadyDialog) flips + // the item into the collection pool with ready_for_collection: + // true. That must drop the item out of the buyer-tasks po_sent + // tab so the operator doesn't see the same card twice (once on + // /buyer-tasks, once on /collection-pool). expect(filter).toContain(`item_status != "ready_delivery"`); + expect(filter).toContain(`item_status != "collection_pool"`); + expect(filter).toContain(`item_status != "assigned"`); expect(filter).toContain(`item_status != "invoiced"`); expect(filter).toContain(`item_status != "delivered"`); expect(filter).toContain(`item_status != "cancelled"`); @@ -279,16 +286,16 @@ describe("listBuyerTasks", () => { expect(groups[0].supplierEmail).toBeNull(); }); - it("does not include a separate ready_delivery exclusion in po_sent post-filter", async () => { - // The ready_delivery exclusion moved to the PB filter (covered by - // the filter-content assertions above). Verify the service still - // accepts a PO-sent item in a non-ready_delivery status, which - // means we don't accidentally re-introduce a redundant - // post-filter that would silently drop legitimate groups. + it("accepts a PO-sent item in a non-excluded status on po_sent", async () => { + // The item_status exclusions for the po_sent tab all live in the + // PB filter (covered by the filter-content assertions above) — + // verify the service still surfaces a legitimate PO-sent item + // so we don't accidentally re-introduce a redundant post-filter + // that would silently drop legitimate groups. const { admin } = makeAdmin([ baseItem({ purchase_order: "po-1", - item_status: "collection_pool", + item_status: "po_generated", expand: { order: baseOrder, supplier: baseSupplier, @@ -374,7 +381,7 @@ describe("generatePoForGroup", () => { }); describe("markGroupReady", () => { - it("updates all items to ready_delivery with the note written to notes", async () => { + it("moves items into the collection pool with ready_for_collection=true and the note", async () => { const getOne = vi.fn().mockResolvedValue({ order: "order-1" }); const updateItem = vi.fn().mockResolvedValue({}); const admin = { @@ -392,12 +399,17 @@ describe("markGroupReady", () => { expect(result.updated).toBe(2); expect(result.failed).toEqual([]); + // Single write must flip both the status (into the collection + // pool) and the ready flag (so driver assignment is unblocked) + // — same shape the in-pool MarkReadyDialog uses internally. expect(updateItem).toHaveBeenCalledWith("item-1", { - item_status: "ready_delivery", + item_status: "collection_pool", + ready_for_collection: true, notes: "Spoke to David — pickup tomorrow 09:00", }); expect(updateItem).toHaveBeenCalledWith("item-2", { - item_status: "ready_delivery", + item_status: "collection_pool", + ready_for_collection: true, notes: "Spoke to David — pickup tomorrow 09:00", }); }); @@ -414,7 +426,8 @@ describe("markGroupReady", () => { await markGroupReady("order-1", "sup-1", [{ id: "item-1" }], " trimmed "); expect(updateItem).toHaveBeenCalledWith("item-1", { - item_status: "ready_delivery", + item_status: "collection_pool", + ready_for_collection: true, notes: "trimmed", }); }); diff --git a/src/lib/server/buyer-tasks-service.ts b/src/lib/server/buyer-tasks-service.ts index 6e6c530..02afafd 100644 --- a/src/lib/server/buyer-tasks-service.ts +++ b/src/lib/server/buyer-tasks-service.ts @@ -50,7 +50,7 @@ export async function listBuyerTasks(tab: BuyerTaskTab): Promise