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));
|
||||
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}`,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue