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:
parent
2e3ec03927
commit
04d56fa1fb
2 changed files with 117 additions and 16 deletions
11
app/page.tsx
11
app/page.tsx
|
|
@ -314,7 +314,11 @@ const loadItems = useCallback(async (showLoading = true) => {
|
||||||
}
|
}
|
||||||
setItems((prev) => prev.filter((i) => i.id !== item.id));
|
setItems((prev) => prev.filter((i) => i.id !== item.id));
|
||||||
setShowProblemModal(false);
|
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) {
|
} catch (err) {
|
||||||
showToast('Failed to report problem', 'error');
|
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));
|
const ids = new Set(itemsToFlag.map((i) => i.id));
|
||||||
setItems((prev) => prev.filter((i) => !ids.has(i.id)));
|
setItems((prev) => prev.filter((i) => !ids.has(i.id)));
|
||||||
if (failed === 0) {
|
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 {
|
} else {
|
||||||
showToast(
|
showToast(
|
||||||
`Flagged ${ok}, ${failed} failed · ${poLabel}`,
|
`Flagged ${ok}, ${failed} failed · ${poLabel}`,
|
||||||
|
|
|
||||||
|
|
@ -371,39 +371,80 @@ const toOrderItemFromStock = (stock: StockPurchaseItem): OrderItem => {
|
||||||
// Pool means "open for any driver to claim". The driver app shows ALL
|
// 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.
|
// pool rows here so the driver can see what's available before claiming.
|
||||||
// No driver filter on purpose.
|
// 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[]> => {
|
export const getPoolItems = async (): Promise<OrderItem[]> => {
|
||||||
|
let orderPool: OrderItem[] = [];
|
||||||
|
let stockPool: OrderItem[] = [];
|
||||||
try {
|
try {
|
||||||
const items = await pb.collection('order_items').getFullList<OrderItem>({
|
orderPool = await pb.collection('order_items').getFullList<OrderItem>({
|
||||||
filter: `item_status = "collection_pool"`,
|
filter: `item_status = "collection_pool"`,
|
||||||
expand: 'supplier,order,purchase_order',
|
expand: 'supplier,order,purchase_order',
|
||||||
sort: 'supplier.suppliername',
|
sort: 'supplier.suppliername',
|
||||||
});
|
});
|
||||||
return items;
|
|
||||||
} catch {
|
} catch {
|
||||||
const items = await pb.collection('order_items').getFullList<OrderItem>({
|
orderPool = await hydrateSuppliers(
|
||||||
filter: `item_status = "collection_pool"`,
|
await pb.collection('order_items').getFullList<OrderItem>({
|
||||||
});
|
filter: `item_status = "collection_pool"`,
|
||||||
return hydrateSuppliers(items);
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
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 ----------
|
// ---------- 3. PROBLEMS — every flagged item in the system ----------
|
||||||
// Items with problem_flag=true OR item_status='problem'. No driver
|
// Items with problem_flag=true OR item_status='problem'. No driver
|
||||||
// filter — problems are operational visibility, not personal lists.
|
// 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[]> => {
|
export const getProblemItems = async (): Promise<OrderItem[]> => {
|
||||||
|
let orderProblems: OrderItem[] = [];
|
||||||
|
let stockProblems: OrderItem[] = [];
|
||||||
try {
|
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"`,
|
filter: `problem_flag = true || item_status = "problem"`,
|
||||||
expand: 'supplier,order,purchase_order',
|
expand: 'supplier,order,purchase_order',
|
||||||
sort: '-updated',
|
sort: '-updated',
|
||||||
});
|
});
|
||||||
return items;
|
|
||||||
} catch {
|
} catch {
|
||||||
const items = await pb.collection('order_items').getFullList<OrderItem>({
|
orderProblems = await hydrateSuppliers(
|
||||||
filter: `problem_flag = true || item_status = "problem"`,
|
await pb.collection('order_items').getFullList<OrderItem>({
|
||||||
});
|
filter: `problem_flag = true || item_status = "problem"`,
|
||||||
return hydrateSuppliers(items);
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
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
|
// 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
|
// 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
|
// 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).
|
// 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 (
|
export const bulkProblem = async (
|
||||||
items: OrderItem[],
|
items: OrderItem[],
|
||||||
description: string
|
description: string
|
||||||
|
|
@ -535,7 +581,10 @@ export const bulkProblem = async (
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
try {
|
try {
|
||||||
await pb.collection('order_items').update(item.id, {
|
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_type: 'other',
|
||||||
problem_flag: true,
|
problem_flag: true,
|
||||||
problem_notes: description,
|
problem_notes: description,
|
||||||
|
|
@ -595,12 +644,29 @@ export const updateStockCollectionStatus = async (
|
||||||
return await pb.collection('stock_purchase_items').update(id, patch);
|
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 (
|
export const reportStockProblem = async (
|
||||||
stockItemId: string,
|
stockItemId: string,
|
||||||
problemType: string,
|
problemType: string,
|
||||||
description: string
|
description: string
|
||||||
) => {
|
) => {
|
||||||
return await pb.collection('stock_purchase_items').update(stockItemId, {
|
return await pb.collection('stock_purchase_items').update(stockItemId, {
|
||||||
|
status: 'collection_pool',
|
||||||
|
drv_id: null,
|
||||||
|
assigned_at: null,
|
||||||
problem_flag: true,
|
problem_flag: true,
|
||||||
problem_type: problemType,
|
problem_type: problemType,
|
||||||
problem_notes: description,
|
problem_notes: description,
|
||||||
|
|
@ -631,6 +697,10 @@ export const bulkCollectStock = async (items: OrderItem[]): Promise<{ ok: number
|
||||||
return { ok, failed };
|
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 (
|
export const bulkProblemStock = async (
|
||||||
items: OrderItem[],
|
items: OrderItem[],
|
||||||
description: string
|
description: string
|
||||||
|
|
@ -642,6 +712,9 @@ export const bulkProblemStock = async (
|
||||||
for (const item of stockItems) {
|
for (const item of stockItems) {
|
||||||
try {
|
try {
|
||||||
await pb.collection('stock_purchase_items').update(item.id, {
|
await pb.collection('stock_purchase_items').update(item.id, {
|
||||||
|
status: 'collection_pool',
|
||||||
|
drv_id: null,
|
||||||
|
assigned_at: null,
|
||||||
problem_flag: true,
|
problem_flag: true,
|
||||||
problem_type: 'other',
|
problem_type: 'other',
|
||||||
problem_notes: description,
|
problem_notes: description,
|
||||||
|
|
@ -750,13 +823,34 @@ export const claimItemForDriver = async (itemId: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Report a problem on an order item
|
// 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 (
|
export const reportProblem = async (
|
||||||
orderItemId: string,
|
orderItemId: string,
|
||||||
problemType: string,
|
problemType: string,
|
||||||
description: string
|
description: string
|
||||||
) => {
|
) => {
|
||||||
return await pb.collection('order_items').update(orderItemId, {
|
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_type: problemType,
|
||||||
problem_flag: true,
|
problem_flag: true,
|
||||||
problem_notes: description,
|
problem_notes: description,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue