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.
This commit is contained in:
Dave 2026-09-08 22:04:37 +02:00
parent 2e3ec03927
commit 04d56fa1fb
2 changed files with 117 additions and 16 deletions

View file

@ -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}`,

View file

@ -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<OrderItem[]> => {
let orderPool: OrderItem[] = [];
let stockPool: OrderItem[] = [];
try {
const items = await pb.collection('order_items').getFullList<OrderItem>({
orderPool = await pb.collection('order_items').getFullList<OrderItem>({
filter: `item_status = "collection_pool"`,
expand: 'supplier,order,purchase_order',
sort: 'supplier.suppliername',
});
return items;
} catch {
const items = await pb.collection('order_items').getFullList<OrderItem>({
filter: `item_status = "collection_pool"`,
});
return hydrateSuppliers(items);
orderPool = await hydrateSuppliers(
await pb.collection('order_items').getFullList<OrderItem>({
filter: `item_status = "collection_pool"`,
})
);
}
try {
const stockRows = await pb.collection('stock_purchase_items').getFullList<StockPurchaseItem>({
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<OrderItem[]> => {
let orderProblems: OrderItem[] = [];
let stockProblems: OrderItem[] = [];
try {
const items = await pb.collection('order_items').getFullList<OrderItem>({
orderProblems = await pb.collection('order_items').getFullList<OrderItem>({
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<OrderItem>({
filter: `problem_flag = true || item_status = "problem"`,
});
return hydrateSuppliers(items);
orderProblems = await hydrateSuppliers(
await pb.collection('order_items').getFullList<OrderItem>({
filter: `problem_flag = true || item_status = "problem"`,
})
);
}
try {
const stockRows = await pb.collection('stock_purchase_items').getFullList<StockPurchaseItem>({
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,