From 91173edbcd6fb9bc32291bb566a33bebf072f081 Mon Sep 17 00:00:00 2001 From: Dave Date: Fri, 30 Oct 2026 22:08:19 +0200 Subject: [PATCH] feat(collection-pool): stock lines can go back to pending and be marked ready Back to pending on a stock line used to clear only the driver, so it stayed in the pool as ready. Stock lines now carry a pool_pending flag (the inverse of ready, so every existing line stays ready) and behave like customer stops: pending badge, Mark Ready (new stock-ready route), no driver assignment while pending. The back-to-pending route refuses to claim success when PocketBase has no pool_pending field yet. Needs the manual live step npm run pb:migrate:pool-pending (already run). Co-Authored-By: Claude Sonnet 5.5 --- package.json | 1 + .../2026-10-30-add-stock-pool-pending.ts | 47 +++++++++++++++ src/app/(app)/collection-pool/components.tsx | 37 ++++++------ src/app/(app)/collection-pool/page.tsx | 23 ++++++++ .../(app)/collection-pool/view-model.test.ts | 27 +++------ src/app/(app)/collection-pool/view-model.ts | 3 +- .../items/[id]/back-to-pending/route.test.ts | 17 +++++- .../items/[id]/back-to-pending/route.ts | 24 ++++++-- .../items/[id]/stock-ready/route.test.ts | 53 +++++++++++++++++ .../items/[id]/stock-ready/route.ts | 57 +++++++++++++++++++ 10 files changed, 242 insertions(+), 47 deletions(-) create mode 100644 scripts/migrations/2026-10-30-add-stock-pool-pending.ts create mode 100644 src/app/api/v1/collection-map/items/[id]/stock-ready/route.test.ts create mode 100644 src/app/api/v1/collection-map/items/[id]/stock-ready/route.ts diff --git a/package.json b/package.json index 546fb64..25c5fb4 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "pb:migrate:rep-trip-legs": "npx tsx scripts/migrations/2026-09-28-create-rep-trip-legs.ts", "pb:migrate:quote-assistant": "npx tsx scripts/migrations/2026-10-16-add-quote-assistant-fields.ts", "pb:migrate:collection-ai": "npx tsx scripts/migrations/2026-10-17-add-collection-ai.ts", + "pb:migrate:pool-pending": "npx tsx scripts/migrations/2026-10-30-add-stock-pool-pending.ts", "pb:migrate:collection-auto-push": "npx tsx scripts/migrations/2026-10-28-add-collection-auto-push.ts", "pb:migrate:order-assistant": "npx tsx scripts/migrations/2026-10-24-add-order-assistant-fields.ts", "pb:migrate:po-number-unique": "npx tsx scripts/migrations/2026-10-19-po-number-unique-index.ts", diff --git a/scripts/migrations/2026-10-30-add-stock-pool-pending.ts b/scripts/migrations/2026-10-30-add-stock-pool-pending.ts new file mode 100644 index 0000000..27c8157 --- /dev/null +++ b/scripts/migrations/2026-10-30-add-stock-pool-pending.ts @@ -0,0 +1,47 @@ +/** + * One-shot migration: let a stock line wait in the collection pool as + * "pending", like a customer stop that is not ready yet. + * + * stock_purchase_items.pool_pending bool (false = ready, the old behaviour) + * + * Inverted on purpose: a new bool is false on every existing row, so every + * existing stock line stays ready and nothing changes until someone presses + * Back to pending. + * + * Run with: npm run pb:migrate:pool-pending + * + * Idempotent. Manual step on the live PB, before deploying. + */ +import "./_env-loader"; +import PocketBase from "pocketbase"; + +const PB_URL = process.env.NEXT_PUBLIC_POCKETBASE_URL || process.env.PB_URL || "http://127.0.0.1:8090"; +const ADMIN_EMAIL = process.env.PB_ADMIN_EMAIL; +const ADMIN_PASSWORD = process.env.PB_ADMIN_PASSWORD; +const TAG = "[pool-pending]"; + +async function main() { + if (!ADMIN_EMAIL || !ADMIN_PASSWORD) { + throw new Error("Set PB_ADMIN_EMAIL and PB_ADMIN_PASSWORD (e.g. from .env.local)."); + } + const pb = new PocketBase(PB_URL); + await pb.collection("_superusers").authWithPassword(ADMIN_EMAIL, ADMIN_PASSWORD); + console.log(`${TAG} Authenticated against ${PB_URL}`); + const collection = await pb.collections.getOne("stock_purchase_items"); + if (collection.fields.some((f: { name: string }) => f.name === "pool_pending")) { + console.log(`${TAG} stock_purchase_items.pool_pending already exists — skipping.`); + return; + } + await pb.collections.update(collection.id, { + fields: [ + ...collection.fields, + { name: "pool_pending", type: "bool", required: false, presentable: false }, + ], + }); + console.log(`${TAG} stock_purchase_items.pool_pending added.`); +} + +main().catch((err) => { + console.error(`${TAG} Migration failed:`, err); + process.exit(1); +}); diff --git a/src/app/(app)/collection-pool/components.tsx b/src/app/(app)/collection-pool/components.tsx index edc6bf1..228d39c 100644 --- a/src/app/(app)/collection-pool/components.tsx +++ b/src/app/(app)/collection-pool/components.tsx @@ -166,7 +166,7 @@ export function PoolRow({ // to clear. Hiding it when there is nothing to undo keeps the row's // actions honest instead of offering a button that silently does // nothing. - const showBackToPending = isStock ? isAssigned : Boolean(row.ready_for_collection) || isAssigned; + const showBackToPending = Boolean(row.ready_for_collection) || isAssigned; // `collection_date` on a pending stop is a FORECAST from the supplier; // on a ready stop it's the confirmed collection day. Either way the // row goes blue so a dated stop stands out in the list, but the badge @@ -379,20 +379,17 @@ export function PoolRow({ · {collectionDate.relative} )} - {/* Ready/pending badge is customer-only — stock lines - don't have a `ready_for_collection` concept. */} - {!isStock && ( - - {readyLabel} - - )} + {/* Ready/pending badge: stock lines carry it too (`pool_pending`). */} + + {readyLabel} + {!isStock && noteLines.length > 0 && ( Notes @@ -422,9 +419,9 @@ export function PoolRow({ size="sm" variant={isAssigned ? "outline" : "default"} onClick={() => onOpenAssign(row)} - disabled={busy || (!isStock && !row.ready_for_collection)} + disabled={busy || !row.ready_for_collection} title={ - !isStock && !row.ready_for_collection + !row.ready_for_collection ? "Mark this stop as ready before assigning" : undefined } @@ -435,7 +432,7 @@ export function PoolRow({ is still pending. The reverse direction ("back to pending") is its own action below, because it also clears a driver assignment and always demands a reason. */} - {!isStock && !row.ready_for_collection && ( + {!row.ready_for_collection && (