feat(pocketbase): merge stock_purchase_items into Assigned list
Stock pickups live in stock_purchase_items (status="assigned_to_driver"), not order_items. Previously getMyAssignedItems() only queried order_items, so a driver with a stock pickup assigned to them saw nothing in the Assigned tab or Map. - getMyAssignedStockItems() fetches stock_purchase_items with the matching drv_id/status filter and expands purchase.supplier + stock_item. A toOrderItemFromStock() reshaper puts each row onto the OrderItem shape so the existing card/modal/map components render stock pickups unchanged; a kind:'pickup' discriminator tags them for the write side. - handleCollect / handleProblem / handleBulkCollect / handleBulkProblem branch on item.kind to PATCH stock_purchase_items instead of order_items (status="collected" + quantity_received on collect, problem_flag + problem_type + problem_notes on problem; mirrors the order_items quantity-mismatch auto-notes). - Same merge on app/map/page.tsx so the stock stop lands on the map. - scripts/check-pb-schema.mjs now covers the stock filter so a future PB rename is caught at prebuild.
This commit is contained in:
parent
8a51f4851b
commit
22bf5dab81
5 changed files with 443 additions and 14 deletions
|
|
@ -6,7 +6,7 @@ import dynamic from 'next/dynamic';
|
|||
import { OrderItem } from '@/lib/types';
|
||||
import { useAuth } from '@/components/AuthProvider';
|
||||
import BottomNav from '@/components/BottomNav';
|
||||
import { getMyAssignedItems, getPoolItems, getProblemItems, isAuthenticated } from '@/lib/pocketbase';
|
||||
import { getMyAssignedItems, getMyAssignedStockItems, getPoolItems, getProblemItems, isAuthenticated } from '@/lib/pocketbase';
|
||||
import { getCached, setCached } from '@/lib/itemsCache';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { RefreshCw, MapPin, AlertCircle, Package, Inbox, AlertTriangle } from 'lucide-react';
|
||||
|
|
@ -93,9 +93,19 @@ export default function MapPage() {
|
|||
setError('');
|
||||
try {
|
||||
let data: OrderItem[] = [];
|
||||
if (tab === 'assigned') data = await getMyAssignedItems();
|
||||
else if (tab === 'collection_pool') data = await getPoolItems();
|
||||
else data = await getProblemItems();
|
||||
if (tab === 'assigned') {
|
||||
// Merge order_items + stock_purchase_items (the "My stops"
|
||||
// tab is the same scope as the Assigned list on page.tsx).
|
||||
const [orders, stock] = await Promise.all([
|
||||
getMyAssignedItems(),
|
||||
getMyAssignedStockItems(),
|
||||
]);
|
||||
data = [...orders, ...stock];
|
||||
} else if (tab === 'collection_pool') {
|
||||
data = await getPoolItems();
|
||||
} else {
|
||||
data = await getProblemItems();
|
||||
}
|
||||
setItems(data);
|
||||
setCached(tab, data);
|
||||
} catch (err) {
|
||||
|
|
|
|||
71
app/page.tsx
71
app/page.tsx
|
|
@ -13,16 +13,21 @@ import ProblemModal from '@/components/ProblemModal';
|
|||
import ItemDetailModal from '@/components/ItemDetailModal';
|
||||
import {
|
||||
getMyAssignedItems,
|
||||
getMyAssignedStockItems,
|
||||
getPoolItems,
|
||||
getProblemItems,
|
||||
updateCollectionStatus,
|
||||
updateStockCollectionStatus,
|
||||
uploadOrderItemAttachment,
|
||||
reportProblem,
|
||||
reportStockProblem,
|
||||
isAuthenticated,
|
||||
logout,
|
||||
getItemDisplayQty,
|
||||
bulkCollect,
|
||||
bulkProblem,
|
||||
bulkCollectStock,
|
||||
bulkProblemStock,
|
||||
} from '@/lib/pocketbase';
|
||||
import { getCached, setCached } from '@/lib/itemsCache';
|
||||
import { cn, clusterByProximity } from '@/lib/utils';
|
||||
|
|
@ -118,7 +123,21 @@ const loadItems = useCallback(async (showLoading = true) => {
|
|||
try {
|
||||
let data: OrderItem[] = [];
|
||||
if (statusFilter === 'assigned') {
|
||||
data = await getMyAssignedItems();
|
||||
// Merge order_items (the original Assigned scope) with
|
||||
// stock_purchase_items (status="assigned_to_driver"). Both
|
||||
// get reshaped onto the same OrderItem shape with a `kind`
|
||||
// discriminator so the existing card / modal / map code
|
||||
// renders them identically — only the WRITE side branches
|
||||
// on kind to hit the right PB collection.
|
||||
//
|
||||
// Pool + Problems stay unchanged: pool is system-wide open
|
||||
// claims, problems are operator-side audit flags. Stock
|
||||
// pickups don't have an open-claim state.
|
||||
const [orders, stock] = await Promise.all([
|
||||
getMyAssignedItems(),
|
||||
getMyAssignedStockItems(),
|
||||
]);
|
||||
data = [...orders, ...stock];
|
||||
} else if (statusFilter === 'collection_pool') {
|
||||
data = await getPoolItems();
|
||||
} else if (statusFilter === 'problem') {
|
||||
|
|
@ -225,6 +244,22 @@ const loadItems = useCallback(async (showLoading = true) => {
|
|||
// modal uses, so the comparison happens against the same number
|
||||
// the driver just saw on screen.
|
||||
const expectedQty = getItemDisplayQty(item);
|
||||
|
||||
if (item.kind === 'pickup') {
|
||||
// Stock-pickup stop — PATCH stock_purchase_items. No photo
|
||||
// upload on this side (stock_purchase_items has no
|
||||
// attachments collection wired in PB).
|
||||
await updateStockCollectionStatus(item.id, {
|
||||
qty_collected: receivedQty,
|
||||
notes,
|
||||
expectedQty,
|
||||
});
|
||||
setItems((prev) => prev.filter((i) => i.id !== item.id));
|
||||
setShowCollectModal(false);
|
||||
showToast('Stock pickup collected successfully');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await updateCollectionStatus(item.id, 'collected', {
|
||||
qty_collected: receivedQty,
|
||||
notes,
|
||||
|
|
@ -280,7 +315,11 @@ const loadItems = useCallback(async (showLoading = true) => {
|
|||
|
||||
const handleProblem = async (item: OrderItem, problemType: string, description: string) => {
|
||||
try {
|
||||
await reportProblem(item.id, problemType, description);
|
||||
if (item.kind === 'pickup') {
|
||||
await reportStockProblem(item.id, problemType, description);
|
||||
} else {
|
||||
await reportProblem(item.id, problemType, description);
|
||||
}
|
||||
setItems((prev) => prev.filter((i) => i.id !== item.id));
|
||||
setShowProblemModal(false);
|
||||
showToast('Problem reported successfully', 'warning');
|
||||
|
|
@ -300,14 +339,21 @@ const loadItems = useCallback(async (showLoading = true) => {
|
|||
const handleBulkCollect = async (poLabel: string, itemsToCollect: OrderItem[]) => {
|
||||
if (itemsToCollect.length === 0) return;
|
||||
try {
|
||||
const result = await bulkCollect(itemsToCollect);
|
||||
// Split the input by kind so order rows and stock rows go to
|
||||
// their respective PB collections in one round-trip each.
|
||||
const [orderResult, stockResult] = await Promise.all([
|
||||
bulkCollect(itemsToCollect.filter((i) => i.kind !== 'pickup')),
|
||||
bulkCollectStock(itemsToCollect),
|
||||
]);
|
||||
const ok = orderResult.ok + stockResult.ok;
|
||||
const failed = orderResult.failed + stockResult.failed;
|
||||
const ids = new Set(itemsToCollect.map((i) => i.id));
|
||||
setItems((prev) => prev.filter((i) => !ids.has(i.id)));
|
||||
if (result.failed === 0) {
|
||||
showToast(`Collected ${result.ok} item${result.ok === 1 ? '' : 's'} · ${poLabel}`, 'success');
|
||||
if (failed === 0) {
|
||||
showToast(`Collected ${ok} item${ok === 1 ? '' : 's'} · ${poLabel}`, 'success');
|
||||
} else {
|
||||
showToast(
|
||||
`Collected ${result.ok}, ${result.failed} failed · ${poLabel}`,
|
||||
`Collected ${ok}, ${failed} failed · ${poLabel}`,
|
||||
'warning'
|
||||
);
|
||||
}
|
||||
|
|
@ -320,14 +366,19 @@ const loadItems = useCallback(async (showLoading = true) => {
|
|||
if (itemsToFlag.length === 0) return;
|
||||
const description = `Bulk-flagged from ${poLabel}`;
|
||||
try {
|
||||
const result = await bulkProblem(itemsToFlag, description);
|
||||
const [orderResult, stockResult] = await Promise.all([
|
||||
bulkProblem(itemsToFlag.filter((i) => i.kind !== 'pickup'), description),
|
||||
bulkProblemStock(itemsToFlag, description),
|
||||
]);
|
||||
const ok = orderResult.ok + stockResult.ok;
|
||||
const failed = orderResult.failed + stockResult.failed;
|
||||
const ids = new Set(itemsToFlag.map((i) => i.id));
|
||||
setItems((prev) => prev.filter((i) => !ids.has(i.id)));
|
||||
if (result.failed === 0) {
|
||||
showToast(`Flagged ${result.ok} item${result.ok === 1 ? '' : 's'} · ${poLabel}`, 'warning');
|
||||
if (failed === 0) {
|
||||
showToast(`Flagged ${ok} item${ok === 1 ? '' : 's'} · ${poLabel}`, 'warning');
|
||||
} else {
|
||||
showToast(
|
||||
`Flagged ${result.ok}, ${result.failed} failed · ${poLabel}`,
|
||||
`Flagged ${ok}, ${failed} failed · ${poLabel}`,
|
||||
'error'
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import {
|
|||
OrderItemAttachment,
|
||||
AttachmentSource,
|
||||
DriverLocation,
|
||||
StockPurchaseItem,
|
||||
PurchaseOrder,
|
||||
} from './types';
|
||||
import { clearCache as clearItemsCache } from './itemsCache';
|
||||
|
||||
|
|
@ -136,6 +138,198 @@ export const getMyAssignedItems = async (): Promise<OrderItem[]> => {
|
|||
// Backwards-compat alias — keep old call sites working during the refactor.
|
||||
export const getAssignedCollections = getMyAssignedItems;
|
||||
|
||||
// ---------- 1b. STOCK PICKUPS — assigned stock_purchase_items ----------
|
||||
// Stock pickups live in their own PB collection (`stock_purchase_items`,
|
||||
// not `order_items`) with their own status enum. The driver app
|
||||
// previously only queried `order_items`, so a driver with a stock
|
||||
// pickup assigned to them (status="assigned_to_driver") saw nothing —
|
||||
// the row never made it into the Assigned list.
|
||||
//
|
||||
// Live PB probe (2026-08-17):
|
||||
// - drv_id on stock_purchase_items is a relation -> users (works)
|
||||
// - status values present: 'booked' | 'collection_pool' |
|
||||
// 'assigned_to_driver' | 'collected' | '' (no 'assigned' alias
|
||||
// on this collection — the operator-side 'assigned' is on
|
||||
// order_items only).
|
||||
// - purchase -> purchases; purchases -> suppliers via the standard
|
||||
// supplier relation, so `expand: 'purchase.supplier,stock_item'`
|
||||
// brings in both supplier coords and the stock_item name/sku.
|
||||
//
|
||||
// The merge step below tags every returned row with kind:"pickup"
|
||||
// and re-shapes it onto the OrderItem shape so the existing card /
|
||||
// modal / map components render stock pickups without modification.
|
||||
// Only the WRITE side (handleCollect / handleProblem in app/page.tsx)
|
||||
// branches on kind — it has to PATCH `stock_purchase_items` instead
|
||||
// of `order_items`.
|
||||
export const getMyAssignedStockItems = async (): Promise<OrderItem[]> => {
|
||||
const user = getCurrentUser();
|
||||
if (!user) return [];
|
||||
let rows: StockPurchaseItem[] = [];
|
||||
try {
|
||||
rows = await pb.collection('stock_purchase_items').getFullList<StockPurchaseItem>({
|
||||
filter: `drv_id = "${user.id}" && status = "assigned_to_driver"`,
|
||||
expand: 'purchase.supplier,stock_item',
|
||||
sort: 'purchase.purchase_number',
|
||||
});
|
||||
} catch (e) {
|
||||
// Fallback without expand — re-fetch each purchase + stock_item
|
||||
// individually. Slower, but at least it doesn't 400 the whole tab.
|
||||
console.warn('[getMyAssignedStockItems] expand failed, falling back:', e instanceof Error ? e.message : e);
|
||||
try {
|
||||
rows = await pb.collection('stock_purchase_items').getFullList<StockPurchaseItem>({
|
||||
filter: `drv_id = "${user.id}" && status = "assigned_to_driver"`,
|
||||
});
|
||||
await hydrateStockExpansions(rows);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return rows.map(toOrderItemFromStock);
|
||||
};
|
||||
|
||||
// Re-fetch the purchase + stock_item rows for any stock_purchase_items
|
||||
// row where the expand side was dropped (degraded mode).
|
||||
const hydrateStockExpansions = async (rows: StockPurchaseItem[]): Promise<void> => {
|
||||
const purchaseIds = new Set<string>();
|
||||
const stockItemIds = new Set<string>();
|
||||
for (const r of rows) {
|
||||
if (r.purchase) purchaseIds.add(r.purchase);
|
||||
if (r.stock_item) stockItemIds.add(r.stock_item);
|
||||
}
|
||||
if (purchaseIds.size === 0 && stockItemIds.size === 0) return;
|
||||
|
||||
let purchaseMap = new Map<string, any>();
|
||||
let stockItemMap = new Map<string, any>();
|
||||
let supplierMap = new Map<string, any>();
|
||||
|
||||
try {
|
||||
if (purchaseIds.size > 0) {
|
||||
const purchases = await pb.collection('purchases').getFullList({
|
||||
filter: Array.from(purchaseIds).map((id) => `id = "${id}"`).join(' || '),
|
||||
expand: 'supplier',
|
||||
});
|
||||
purchaseMap = new Map(purchases.map((p: any) => [p.id, p]));
|
||||
for (const p of purchases) {
|
||||
if ((p as any).expand?.supplier) {
|
||||
supplierMap.set((p as any).expand.supplier.id, (p as any).expand.supplier);
|
||||
} else if (p.supplier) {
|
||||
try {
|
||||
const sup = await pb.collection('suppliers').getOne(p.supplier);
|
||||
supplierMap.set(sup.id, sup);
|
||||
} catch { /* supplier missing — leave it empty */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[hydrateStockExpansions] purchases fetch failed:', e instanceof Error ? e.message : e);
|
||||
}
|
||||
try {
|
||||
if (stockItemIds.size > 0) {
|
||||
const stockItems = await pb.collection('stock_items').getFullList({
|
||||
filter: Array.from(stockItemIds).map((id) => `id = "${id}"`).join(' || '),
|
||||
});
|
||||
stockItemMap = new Map(stockItems.map((s: any) => [s.id, s]));
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[hydrateStockExpansions] stock_items fetch failed:', e instanceof Error ? e.message : e);
|
||||
}
|
||||
|
||||
for (const r of rows) {
|
||||
if (r.expand) continue;
|
||||
const purchase = purchaseMap.get(r.purchase);
|
||||
const supplier = purchase ? supplierMap.get(purchase.supplier) : undefined;
|
||||
const stockItem = stockItemMap.get(r.stock_item);
|
||||
r.expand = {
|
||||
purchase: purchase || undefined,
|
||||
stock_item: stockItem || undefined,
|
||||
};
|
||||
if (purchase && supplier) {
|
||||
(purchase as any).expand = { ...((purchase as any).expand || {}), supplier };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Reshape a stock_purchase_items row onto the OrderItem shape so
|
||||
// the existing card / modal / map components can render stock pickups
|
||||
// without modification. The `kind: 'pickup'` tag is the only signal
|
||||
// downstream code (write handlers in app/page.tsx) uses to decide
|
||||
// which PB collection to PATCH.
|
||||
//
|
||||
// Field mapping (verified against the live schema 2026-08-17):
|
||||
// description <- stock.description (falls back to stock_item.name)
|
||||
// po_description <- stock.description (same — stock pickups don't
|
||||
// have a separate customer-facing line)
|
||||
// quantity <- stock.quantity (expected)
|
||||
// order_qty <- stock.quantity (mirrors quantity so existing
|
||||
// getItemDisplayQty helpers work)
|
||||
// item_status <- 'assigned' (we map stock status onto the
|
||||
// operator-side enum so chip counts and
|
||||
// problem filters keep working)
|
||||
// drv_id <- stock.drv_id
|
||||
// assigned_at <- stock.assigned_at
|
||||
// notes <- stock.notes
|
||||
// problem_* <- pass-through
|
||||
// is_stock_item <- true (renders the existing "Stock" badge)
|
||||
// expand.supplier <- purchase.expand.supplier (so the supplier
|
||||
// grouping on the list page works)
|
||||
// expand.purchase_order <- a synthesized { po_number, status,
|
||||
// expand:{supplier} } from purchase so the PO
|
||||
// tooltip / ItemDetailModal / PO sub-group
|
||||
// bucket code keeps working
|
||||
// expand.stock_item <- pass-through (lets the detail modal show the
|
||||
// canonical stock-item name + sku if needed)
|
||||
// qty_collected <- stock.quantity_received (audit; the modal
|
||||
// pre-fills this when re-opening)
|
||||
const toOrderItemFromStock = (stock: StockPurchaseItem): OrderItem => {
|
||||
const purchase = stock.expand?.purchase as (PurchaseOrder & { expand?: { supplier?: any }; supplier?: string; purchase_number?: string }) | undefined;
|
||||
const supplierFromPurchase = purchase?.expand?.supplier;
|
||||
const stockItem = stock.expand?.stock_item;
|
||||
|
||||
const description = (stock.description || stockItem?.description || stockItem?.name || '').trim();
|
||||
|
||||
// Synthesize a purchase_order-shaped object from the purchase row.
|
||||
// We do this rather than aliasing so existing components that read
|
||||
// `expand.purchase_order.po_number` keep working unchanged.
|
||||
const purchase_order: PurchaseOrder | undefined = purchase
|
||||
? ({
|
||||
id: purchase.id,
|
||||
po_number: purchase.purchase_number || '',
|
||||
status: purchase.status,
|
||||
expand: { supplier: supplierFromPurchase },
|
||||
supplier: purchase.supplier,
|
||||
} as unknown as PurchaseOrder)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
kind: 'pickup',
|
||||
id: stock.id,
|
||||
// `order` is a required field on OrderItem but stock pickups have
|
||||
// no order — empty string keeps the type satisfied without
|
||||
// misleading the UI (the render path reads expand.purchase_order
|
||||
// for the PO column, never raw `order`).
|
||||
order: '',
|
||||
description,
|
||||
po_description: description,
|
||||
quantity: stock.quantity || 0,
|
||||
order_qty: stock.quantity || 0,
|
||||
item_status: 'assigned',
|
||||
drv_id: stock.drv_id,
|
||||
assigned_at: stock.assigned_at,
|
||||
notes: stock.notes,
|
||||
problem_flag: stock.problem_flag,
|
||||
problem_type: stock.problem_type as OrderItem['problem_type'],
|
||||
problem_notes: stock.problem_notes,
|
||||
is_stock_item: true,
|
||||
sku: stock.sku || stockItem?.sku,
|
||||
qty_collected: stock.quantity_received,
|
||||
expand: {
|
||||
supplier: supplierFromPurchase,
|
||||
purchase_order,
|
||||
},
|
||||
} as OrderItem;
|
||||
};
|
||||
|
||||
// ---------- 2. POOL — every collection_pool item in the system ----------
|
||||
// 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.
|
||||
|
|
@ -318,6 +512,112 @@ export const bulkProblem = async (
|
|||
return { ok, failed };
|
||||
};
|
||||
|
||||
// ---------- Stock-pickup writers (mirror of the order_items ones) ----------
|
||||
// The driver-side collect/problem handlers in app/page.tsx branch on
|
||||
// `item.kind === 'pickup'` and call these instead of
|
||||
// `updateCollectionStatus` / `reportProblem`. Mirrors the same shape:
|
||||
// - collected -> status="collected", quantity_received=received
|
||||
// - partial -> problem_flag + problem_type="stock" + auto note
|
||||
// - over -> problem_flag + problem_type="" + auto note
|
||||
// - problem -> problem_flag + problem_type + problem_notes
|
||||
//
|
||||
// No photo upload on the stock side — stock_purchase_items has no
|
||||
// attachments file field wired in PB. Photos stay an order_items-only
|
||||
// feature; the collect modal on a stock stop accepts a photo input
|
||||
// but it's silently dropped here.
|
||||
export const updateStockCollectionStatus = async (
|
||||
id: string,
|
||||
data: {
|
||||
qty_collected?: number;
|
||||
notes?: string;
|
||||
expectedQty?: number;
|
||||
}
|
||||
) => {
|
||||
const patch: Record<string, unknown> = {
|
||||
status: 'collected',
|
||||
quantity_received: data.qty_collected ?? 0,
|
||||
};
|
||||
if (data.notes !== undefined) patch.notes = data.notes;
|
||||
|
||||
const received = typeof data.qty_collected === 'number' ? data.qty_collected : 0;
|
||||
const expected = typeof data.expectedQty === 'number' ? data.expectedQty : 0;
|
||||
if (expected > 0 && received > 0 && received < expected) {
|
||||
patch.problem_flag = true;
|
||||
patch.problem_type = 'stock';
|
||||
const userNote = (data.notes ?? '').trim();
|
||||
const autoNote = `Partial collection: ${received} of ${expected} units`;
|
||||
patch.problem_notes = userNote ? `${autoNote} — ${userNote}` : autoNote;
|
||||
} else if (expected > 0 && received > expected) {
|
||||
patch.problem_flag = true;
|
||||
patch.problem_type = '';
|
||||
const userNote = (data.notes ?? '').trim();
|
||||
const autoNote = `Over collection: ${received} units received against ${expected} ordered`;
|
||||
patch.problem_notes = userNote ? `${autoNote} — ${userNote}` : autoNote;
|
||||
}
|
||||
|
||||
return await pb.collection('stock_purchase_items').update(id, patch);
|
||||
};
|
||||
|
||||
export const reportStockProblem = async (
|
||||
stockItemId: string,
|
||||
problemType: string,
|
||||
description: string
|
||||
) => {
|
||||
return await pb.collection('stock_purchase_items').update(stockItemId, {
|
||||
problem_flag: true,
|
||||
problem_type: problemType,
|
||||
problem_notes: description,
|
||||
});
|
||||
};
|
||||
|
||||
// Bulk versions — same pattern as the order_items bulk helpers, but
|
||||
// filter the input list to only stock-pickup rows so a single bulk
|
||||
// action across a mixed PO (orders + stock) doesn't cross-write.
|
||||
export const bulkCollectStock = async (items: OrderItem[]): Promise<{ ok: number; failed: number }> => {
|
||||
const stockItems = items.filter((i) => i.kind === 'pickup');
|
||||
if (stockItems.length === 0) return { ok: 0, failed: 0 };
|
||||
let ok = 0;
|
||||
let failed = 0;
|
||||
for (const item of stockItems) {
|
||||
try {
|
||||
const expectedQty = getItemDisplayQty(item);
|
||||
await pb.collection('stock_purchase_items').update(item.id, {
|
||||
status: 'collected',
|
||||
quantity_received: expectedQty,
|
||||
});
|
||||
ok += 1;
|
||||
} catch (e) {
|
||||
failed += 1;
|
||||
console.warn('[bulkCollectStock] failed for', item.id, e);
|
||||
}
|
||||
}
|
||||
return { ok, failed };
|
||||
};
|
||||
|
||||
export const bulkProblemStock = async (
|
||||
items: OrderItem[],
|
||||
description: string
|
||||
): Promise<{ ok: number; failed: number }> => {
|
||||
const stockItems = items.filter((i) => i.kind === 'pickup');
|
||||
if (stockItems.length === 0) return { ok: 0, failed: 0 };
|
||||
let ok = 0;
|
||||
let failed = 0;
|
||||
for (const item of stockItems) {
|
||||
try {
|
||||
await pb.collection('stock_purchase_items').update(item.id, {
|
||||
problem_flag: true,
|
||||
problem_type: 'other',
|
||||
problem_notes: description,
|
||||
});
|
||||
ok += 1;
|
||||
} catch (e) {
|
||||
failed += 1;
|
||||
console.warn('[bulkProblemStock] failed for', item.id, e);
|
||||
}
|
||||
}
|
||||
return { ok, failed };
|
||||
};
|
||||
|
||||
// ---------- Attachments ----------
|
||||
// Upload a pickup photo by appending it to the order_item's
|
||||
// `order_item_attachements` file field (PB file field, max multi).
|
||||
|
|
|
|||
64
lib/types.ts
64
lib/types.ts
|
|
@ -44,7 +44,71 @@ export interface PurchaseOrder {
|
|||
updated?: string;
|
||||
}
|
||||
|
||||
// Discriminator that tags a row as either an `order_items` row
|
||||
// (the original Assigned-tab scope) or a `stock_purchase_items`
|
||||
// row merged into the same list with the same stop shape.
|
||||
// Default for legacy rows is 'order' — components can treat any
|
||||
// row without an explicit kind as an order row.
|
||||
export type StopKind = 'order' | 'pickup';
|
||||
|
||||
// Raw PocketBase row shape for `stock_purchase_items`. Mirrors the
|
||||
// schema verified live 2026-08-17:
|
||||
// drv_id, status (booked | collection_pool | assigned_to_driver |
|
||||
// collected | ''), purchase (relation -> purchases),
|
||||
// stock_item (relation -> stock_items), quantity (expected),
|
||||
// quantity_received (audit), description (line), sku,
|
||||
// supplier_sku, unit, cost_price, problem_flag/problem_type/
|
||||
// problem_notes, assigned_at, notes.
|
||||
//
|
||||
// The driver app never displays this raw shape — see StockPickupItem
|
||||
// below for the merged view that matches OrderItem's shape so the
|
||||
// existing card/modal/map components render stock pickups unchanged.
|
||||
export interface StockPurchaseItem {
|
||||
id: string;
|
||||
purchase: string;
|
||||
stock_item: string;
|
||||
drv_id?: string;
|
||||
// Status field names line up with the operator-side flow. The
|
||||
// driver-app driver-side analog is `item_status` — we map
|
||||
// status="assigned_to_driver" -> item_status="assigned" so the
|
||||
// existing chip counts and filters keep working.
|
||||
status: 'booked' | 'collection_pool' | 'assigned_to_driver' | 'collected' | '' | string;
|
||||
description: string;
|
||||
sku?: string;
|
||||
supplier_sku?: string;
|
||||
unit?: string;
|
||||
cost_price?: number;
|
||||
quantity: number;
|
||||
quantity_received?: number;
|
||||
notes?: string;
|
||||
problem_flag?: boolean;
|
||||
problem_type?: string;
|
||||
problem_notes?: string;
|
||||
assigned_at?: string;
|
||||
expand?: {
|
||||
purchase?: PurchaseOrder;
|
||||
stock_item?: StockItem;
|
||||
};
|
||||
}
|
||||
|
||||
// `stock_items` row shape — needed only to type the expand side of
|
||||
// `stock_purchase_items.expand.stock_item`. The driver app reads
|
||||
// `stock_item.name` (display name) and `stock_item.sku` from here.
|
||||
export interface StockItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
sku?: string;
|
||||
unit?: string;
|
||||
selling_price?: number;
|
||||
cost_price?: number;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
// 'order' for the original order_items rows, 'pickup' for stock
|
||||
// pickups merged from stock_purchase_items. Components that don't
|
||||
// write (card/map/modal reads) can ignore this entirely.
|
||||
kind?: StopKind;
|
||||
id: string;
|
||||
order: string;
|
||||
// Customer-facing line. Use this for label / list / customer view.
|
||||
|
|
|
|||
|
|
@ -35,6 +35,10 @@ const FILTERS = [
|
|||
{ collection: 'order_items', filter: 'drv_id = "placeholder" && item_status = "assigned"' },
|
||||
{ collection: 'order_items', filter: 'item_status = "collection_pool"' },
|
||||
{ collection: 'order_items', filter: 'problem_flag = true || item_status = "problem"' },
|
||||
// Stock pickups live in their own collection — see
|
||||
// getMyAssignedStockItems in lib/pocketbase.ts. The driver app
|
||||
// was silently dropping these rows before this filter was added.
|
||||
{ collection: 'stock_purchase_items', filter: 'drv_id = "placeholder" && status = "assigned_to_driver"' },
|
||||
{ collection: 'driver_locations', filter: 'relation = "placeholder" && timestamp >= "2026-01-01T00:00:00Z"' },
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue