From 04d56fa1fb3dbd891d5b82f69363ea401c12a5df Mon Sep 17 00:00:00 2001 From: Dave Date: Tue, 8 Sep 2026 22:04:37 +0200 Subject: [PATCH] Send reported problems back to the collection pool Previously, reportProblem parked an order item in item_status='problem' and reportStockProblem only toggled problem_flag on a stock_purchase_items row that stayed in status='assigned_to_driver'. Both ended up as a dead-end: the row fell off the driver's Assigned tab but never landed anywhere the driver (or any other driver) could re-attempt it, and the stock-pickup variant looked like 'tapped Problem and nothing happened' because the assigned-status filter didn't exclude the flagged row. Now both writers release the row back to collection_pool, clear drv_id and assigned_at, and keep problem_flag=true for operator-side audit. getPoolItems and getProblemItems are extended to surface stock_purchase_items alongside order_items so flagged / pooled stock pickups actually appear in both tabs. Toast wording on the driver side updated to reflect the pool handoff. --- app/page.tsx | 11 ++++- lib/pocketbase.ts | 122 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 117 insertions(+), 16 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 70c7cba..3b0f3a8 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -314,7 +314,11 @@ const loadItems = useCallback(async (showLoading = true) => { } setItems((prev) => prev.filter((i) => i.id !== item.id)); setShowProblemModal(false); - showToast('Problem reported successfully', 'warning'); + // Item is now in status='collection_pool' on PB, so it dropped + // off the Assigned tab server-side as well. Toast signposts the + // pool handoff so the driver knows the item will be re-attempted + // rather than just disappearing. + showToast('Problem reported — item sent back to the pool', 'warning'); } catch (err) { showToast('Failed to report problem', 'error'); } @@ -367,7 +371,10 @@ const loadItems = useCallback(async (showLoading = true) => { const ids = new Set(itemsToFlag.map((i) => i.id)); setItems((prev) => prev.filter((i) => !ids.has(i.id))); if (failed === 0) { - showToast(`Flagged ${ok} item${ok === 1 ? '' : 's'} · ${poLabel}`, 'warning'); + showToast( + `Flagged ${ok} item${ok === 1 ? '' : 's'} · sent back to the pool · ${poLabel}`, + 'warning' + ); } else { showToast( `Flagged ${ok}, ${failed} failed · ${poLabel}`, diff --git a/lib/pocketbase.ts b/lib/pocketbase.ts index 754ef33..19e56bd 100644 --- a/lib/pocketbase.ts +++ b/lib/pocketbase.ts @@ -371,39 +371,80 @@ const toOrderItemFromStock = (stock: StockPurchaseItem): OrderItem => { // Pool means "open for any driver to claim". The driver app shows ALL // pool rows here so the driver can see what's available before claiming. // No driver filter on purpose. +// +// Stock pickups live in their own collection (`stock_purchase_items`) +// with the pool rows surfaced as `status = "collection_pool"` there +// too. Without the second fetch, a stock pickup that's been reported +// as a problem (or any stock row that gets returned to the pool) sits +// in limbo — gone from Assigned, but invisible from Pool as well. +// Merge both collections into the same OrderItem shape so the same +// card / modal code renders them without branching on the source. export const getPoolItems = async (): Promise => { + let orderPool: OrderItem[] = []; + let stockPool: OrderItem[] = []; try { - const items = await pb.collection('order_items').getFullList({ + orderPool = await pb.collection('order_items').getFullList({ filter: `item_status = "collection_pool"`, expand: 'supplier,order,purchase_order', sort: 'supplier.suppliername', }); - return items; } catch { - const items = await pb.collection('order_items').getFullList({ - filter: `item_status = "collection_pool"`, - }); - return hydrateSuppliers(items); + orderPool = await hydrateSuppliers( + await pb.collection('order_items').getFullList({ + filter: `item_status = "collection_pool"`, + }) + ); } + try { + const stockRows = await pb.collection('stock_purchase_items').getFullList({ + filter: `status = "collection_pool"`, + expand: 'purchase.supplier,stock_item', + sort: 'purchase.purchase_number', + }); + stockPool = stockRows.map(toOrderItemFromStock); + } catch (e) { + console.warn('[getPoolItems] stock pool fetch failed:', e instanceof Error ? e.message : e); + } + return [...orderPool, ...stockPool]; }; // ---------- 3. PROBLEMS — every flagged item in the system ---------- // Items with problem_flag=true OR item_status='problem'. No driver // filter — problems are operational visibility, not personal lists. +// +// Stock pickups (stock_purchase_items) get a parallel query: same +// problem_flag / status enum, mirrored onto OrderItem shape via +// toOrderItemFromStock so the Problems card UI renders them without +// a separate code path. Without this, flagged stock pickups only +// ever showed up in the Pool tab (and only after the getPoolItems +// change) — no driver ever saw a "Problems" card for a stock row. export const getProblemItems = async (): Promise => { + let orderProblems: OrderItem[] = []; + let stockProblems: OrderItem[] = []; try { - const items = await pb.collection('order_items').getFullList({ + orderProblems = await pb.collection('order_items').getFullList({ filter: `problem_flag = true || item_status = "problem"`, expand: 'supplier,order,purchase_order', sort: '-updated', }); - return items; } catch { - const items = await pb.collection('order_items').getFullList({ - filter: `problem_flag = true || item_status = "problem"`, - }); - return hydrateSuppliers(items); + orderProblems = await hydrateSuppliers( + await pb.collection('order_items').getFullList({ + filter: `problem_flag = true || item_status = "problem"`, + }) + ); } + try { + const stockRows = await pb.collection('stock_purchase_items').getFullList({ + filter: `problem_flag = true`, + expand: 'purchase.supplier,stock_item', + sort: '-updated', + }); + stockProblems = stockRows.map(toOrderItemFromStock); + } catch (e) { + console.warn('[getProblemItems] stock fetch failed:', e instanceof Error ? e.message : e); + } + return [...orderProblems, ...stockProblems]; }; // Update order item status when collected. This is the canonical @@ -526,6 +567,11 @@ export const bulkCollect = async (items: OrderItem[]): Promise<{ ok: number; fai // Flag every row as a generic problem — used by the "Flag all" action // on a PO sub-group. Each row gets the supplied note so the operator // can see what was reported in bulk (no per-item modal flow). +// +// Mirrors reportProblem — releases the row back into the open +// collection_pool (so it can be re-attempted) instead of parking it +// in a dead-end 'problem' status. drv_id is cleared so the row is +// unowned. export const bulkProblem = async ( items: OrderItem[], description: string @@ -535,7 +581,10 @@ export const bulkProblem = async ( for (const item of items) { try { await pb.collection('order_items').update(item.id, { - item_status: 'problem', + item_status: 'collection_pool', + drv_id: null, + assigned_at: null, + drv_status: 'unassigned', problem_type: 'other', problem_flag: true, problem_notes: description, @@ -595,12 +644,29 @@ export const updateStockCollectionStatus = async ( return await pb.collection('stock_purchase_items').update(id, patch); }; +// Report a problem on a stock pickup (stock_purchase_items row). +// +// This used to set only the problem flags and leave the row in +// status='assigned_to_driver'. Combined with the Assigned-tab +// filter ignoring problem_flag, that meant a stock pickup with a +// reported problem kept reappearing in the driver's assigned list +// after every refresh — the visible "I tapped Problem and nothing +// happened" bug. Now we route it back to status='collection_pool' +// instead, mirroring the order_items flow above. +// * Drops the row off the driver's Assigned list immediately. +// * Puts it in the Pool tab (via getPoolItems, which now unions +// stock pickups too), so another driver can pick it up. +// * Keeps problem_flag=true so the operator audit trail and the +// Pool-card's red "Problem" badge both still surface the flag. export const reportStockProblem = async ( stockItemId: string, problemType: string, description: string ) => { return await pb.collection('stock_purchase_items').update(stockItemId, { + status: 'collection_pool', + drv_id: null, + assigned_at: null, problem_flag: true, problem_type: problemType, problem_notes: description, @@ -631,6 +697,10 @@ export const bulkCollectStock = async (items: OrderItem[]): Promise<{ ok: number return { ok, failed }; }; +// Mirror of bulkProblem for the stock collection. Releases the row +// to status='collection_pool' (same convention as +// reportStockProblem) so flagged stock pickups show up in the Pool +// tab for re-attempt instead of vanishing from the driver app. export const bulkProblemStock = async ( items: OrderItem[], description: string @@ -642,6 +712,9 @@ export const bulkProblemStock = async ( for (const item of stockItems) { try { await pb.collection('stock_purchase_items').update(item.id, { + status: 'collection_pool', + drv_id: null, + assigned_at: null, problem_flag: true, problem_type: 'other', problem_notes: description, @@ -750,13 +823,34 @@ export const claimItemForDriver = async (itemId: string) => { }; // Report a problem on an order item +// +// Pools the row back into the open collection_pool instead of +// parking it in 'problem'. Rationale: +// * "Problem" was a dead-end status: the row disappeared from +// the driver's Assigned list but never appeared in a list +// where anyone could re-attempt it, so reported items just +// rotted on the operator-side Problems dashboard. +// * Setting item_status='collection_pool' puts the row straight +// into the Pool tab, open for any driver (or the same one) +// to claim via claimItemForDriver. `problem_flag` is kept true +// so the operator-side Problems view still sees it (the +// Problems filter is OR-based) and so the Pool tab renders +// the row with a red "Problem" badge — clearly flagged to +// whoever picks it up. +// * drv_id is cleared so the row no longer belongs to the +// reporter; without that, the next claim by another driver +// would silently overwrite drv_id but the row would still be +// associated with the old driver in any drv_id-only audits. export const reportProblem = async ( orderItemId: string, problemType: string, description: string ) => { return await pb.collection('order_items').update(orderItemId, { - item_status: 'problem', + item_status: 'collection_pool', + drv_id: null, + assigned_at: null, + drv_status: 'unassigned', problem_type: problemType, problem_flag: true, problem_notes: description,