From a892aec52733e676338ab707b341e090152ece8b Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Fri, 25 Sep 2026 14:02:24 +0530 Subject: [PATCH] Add loading sheet edit flow --- .../LoadingEvent/LoadingSheetService.cs | 18 +- .../_components/loading-sheets-client.tsx | 5 +- .../operations/loading/new/page.tsx | 566 ++++++++++++++++-- 3 files changed, 526 insertions(+), 63 deletions(-) diff --git a/Distribution/Features/LoadingEvent/LoadingSheetService.cs b/Distribution/Features/LoadingEvent/LoadingSheetService.cs index 7384913..0afe73c 100644 --- a/Distribution/Features/LoadingEvent/LoadingSheetService.cs +++ b/Distribution/Features/LoadingEvent/LoadingSheetService.cs @@ -64,15 +64,14 @@ public class LoadingSheetService( if (sheet.Status != LoadingSheetStatus.Planned) throw new InvalidOperationException("Items can only be changed while the sheet is Planned"); - // Upsert by (item, warehouse, shop PO item): a matching row gets its quantity - // updated in place, a new combination is added. Rows for combinations not present - // in this request are left untouched, so a later save can top up the same PO - // item's remaining quantity from a different warehouse without losing what was - // already planned from the first one. + // The request is the full desired item list: a row matching (item, warehouse, + // shop PO item) gets its quantity updated in place, a new combination is added, + // and rows for combinations not present in the request are removed. await uow.BeginAsync(ct); try { var newItems = new List(); + var keptItems = new List(); foreach (var input in request.Items) { var existing = sheet.Items.FirstOrDefault(i => @@ -81,7 +80,10 @@ public class LoadingSheetService( i.FK_ShopPOItemId == input.FK_ShopPOItemId); if (existing is not null) + { existing.QuantityDispatched = input.Quantity; + keptItems.Add(existing); + } else newItems.Add(new LoadingSheetItem { @@ -92,10 +94,14 @@ public class LoadingSheetService( QuantityDispatched = input.Quantity }); } + + var removedItems = sheet.Items.Where(i => !keptItems.Contains(i)).ToList(); + if (removedItems.Count > 0) + repo.RemoveItems(removedItems); if (newItems.Count > 0) await repo.AddItemsAsync(newItems); - var shopPOItemIds = sheet.Items.Select(i => i.FK_ShopPOItemId) + var shopPOItemIds = keptItems.Select(i => i.FK_ShopPOItemId) .Concat(newItems.Select(i => i.FK_ShopPOItemId)) .Where(x => x.HasValue) .Select(x => x!.Value) diff --git a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx index e2b4320..e96a950 100644 --- a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx +++ b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheets-client.tsx @@ -201,8 +201,9 @@ export function LoadingSheetsClient() { }, { action: 'edit', - label: target.status === 'Confirmed' ? 'Add items (new sheet)' : 'Set items', - onClick: handleSetItems, + label: 'Edit sheet', + onClick: (row) => router.push(`/operations/loading/new?sheetId=${row.id}`), + visible: (target) => target.status === 'Planned', }, { action: 'confirm', diff --git a/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx b/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx index 2ff1a7c..5953b47 100644 --- a/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx +++ b/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx @@ -1,7 +1,7 @@ 'use client' -import { useEffect, useMemo, useState } from 'react' -import { useRouter } from 'next/navigation' +import { Fragment, Suspense, useEffect, useMemo, useRef, useState } from 'react' +import { useRouter, useSearchParams } from 'next/navigation' import { HugeiconsIcon } from '@hugeicons/react' import { ArrowLeft01Icon, ViewIcon } from '@hugeicons/core-free-icons' import { @@ -34,10 +34,13 @@ import { rides as ridesApi, users as usersApi, loadingSheets as loadingSheetsApi, + stock as stockApi, + items as itemsApi, getApiError, getPaginatedItems, } from '@/lib/api/index' import type { + ItemSummaryResponse, ShopPOResponse, RegionResponse, RouteResponse, @@ -50,6 +53,19 @@ import type { LoadingSheetItemInput } from '@/lib/api/operations' const ALL = '__all__' +interface ExtraAllocation { + warehouseId: string + quantity: number +} + +// An item added straight to the sheet, not tied to any Shop PO +interface ManualItem { + key: string + itemId: string + warehouseId: string + quantity: number +} + interface PickedLine { shopPOItemId: string itemId: string @@ -58,13 +74,32 @@ interface PickedLine { storeName: string shopPOId: string orderedQty: number + // already on loading sheets (or delivered) + loadedQty: number + // still open on the PO, i.e. what a sheet can still pick remainingQty: number checked: boolean + // quantity taken from the sheet's main warehouse quantity: number + // optional top-ups taken from other warehouses + extras: ExtraAllocation[] } export default function NewLoadingSheetPage() { + return ( + + + + ) +} + +function LoadingSheetForm() { const router = useRouter() + const sheetId = useSearchParams().get('sheetId') + const isEdit = !!sheetId + // shopPOItemId -> quantity from the sheet being edited, applied once its route's POs load + const prefillRef = useRef<{ warehouseId: string; byItem: Record } | null>(null) + const requestedStockRef = useRef(new Set()) const [regionsList, setRegionsList] = useState([]) const [routesList, setRoutesList] = useState([]) @@ -81,27 +116,66 @@ export default function NewLoadingSheetPage() { const [populatedByUserId, setPopulatedByUserId] = useState('') const [viewStoreId, setViewStoreId] = useState('') + const [itemsList, setItemsList] = useState([]) + const [manualItems, setManualItems] = useState([]) const [pos, setPos] = useState([]) + const [loadedByPoItem, setLoadedByPoItem] = useState>({}) const [loadingPos, setLoadingPos] = useState(false) const [submitting, setSubmitting] = useState(false) const [lines, setLines] = useState>({}) + // warehouseId -> itemId -> available quantity + const [stockByWh, setStockByWh] = useState>>({}) useEffect(() => { Promise.all([ regionsApi.list({ pageSize: 200 }), warehousesApi.list({ pageSize: 200 }), - ridesApi.list({ status: 'Open', pageSize: 200 }), + ridesApi.list({ status: isEdit ? undefined : 'Open', pageSize: 200 }), usersApi.list({ pageSize: 200 }), + itemsApi.list({ pageSize: 500 }), ]) - .then(([regionsRes, warehousesRes, ridesRes, usersRes]) => { + .then(([regionsRes, warehousesRes, ridesRes, usersRes, itemsRes]) => { + setItemsList(getPaginatedItems(itemsRes)) setRegionsList(getPaginatedItems(regionsRes)) setWarehousesList(getPaginatedItems(warehousesRes)) setRidesList(getPaginatedItems(ridesRes)) setUsersList(getPaginatedItems(usersRes)) }) .catch((err) => toast.add({ title: 'Failed to load reference data', description: getApiError(err), type: 'error' })) - }, []) + }, [isEdit]) + + useEffect(() => { + if (!sheetId) return + loadingSheetsApi.get(sheetId) + .then(async (sheet) => { + if (sheet.status !== 'Planned') { + toast.add({ title: 'Only planned loading sheets can be edited', type: 'error' }) + router.replace('/operations/loading') + return + } + setRideId(sheet.fK_RideId) + setPopulatedByUserId(sheet.populatedBy) + if (sheet.items[0]) setWarehouseId(sheet.items[0].fK_WarehouseId) + const byItem: Record = {} + const manual: ManualItem[] = [] + for (const i of sheet.items) { + if (!i.fK_ShopPOItemId) { + manual.push({ key: i.id, itemId: i.fK_ItemId, warehouseId: i.fK_WarehouseId, quantity: i.quantityDispatched }) + continue + } + ;(byItem[i.fK_ShopPOItemId] ??= []).push({ warehouseId: i.fK_WarehouseId, quantity: i.quantityDispatched }) + } + setManualItems(manual) + prefillRef.current = { warehouseId: sheet.items[0]?.fK_WarehouseId ?? '', byItem } + const firstStoreId = sheet.items.find((i) => i.storeId)?.storeId + if (firstStoreId) { + const store = await storesApi.get(firstStoreId) + setRouteId(store.routeId) + } + }) + .catch((err) => toast.add({ title: 'Failed to load loading sheet', description: getApiError(err), type: 'error' })) + }, [sheetId, router]) useEffect(() => { setRouteId('') @@ -116,7 +190,7 @@ export default function NewLoadingSheetPage() { useEffect(() => { setStoreId('') setViewStoreId('') - setLines({}) + if (!prefillRef.current) setLines({}) if (!routeId) { setStoresList([]) setPos([]) @@ -127,21 +201,108 @@ export default function NewLoadingSheetPage() { .catch((err) => toast.add({ title: 'Failed to load shops', description: getApiError(err), type: 'error' })) setLoadingPos(true) - shopPosApi.list({ routeId, confirmed: true, pageSize: 200 }) - .then((res) => { + Promise.all([ + shopPosApi.list({ routeId, confirmed: true, pageSize: 200 }), + loadingSheetsApi.list({ routeId, pageSize: 200 }), + ]) + .then(([res, sheetsRes]) => { const items = getPaginatedItems(res).filter((po) => po.status === 'Pending' || po.status === 'PartiallyFulfilled') + // Quantity of each PO item already planned/loaded on other sheets. The sheet being + // edited is excluded because its own items are re-picked on this form. + const loaded: Record = {} + for (const sheet of getPaginatedItems(sheetsRes)) { + if (sheet.id === sheetId) continue + for (const i of sheet.items) { + if (i.fK_ShopPOItemId) loaded[i.fK_ShopPOItemId] = (loaded[i.fK_ShopPOItemId] ?? 0) + i.quantityDispatched + } + } + setLoadedByPoItem(loaded) setPos(items) + const prefill = prefillRef.current + if (prefill) { + prefillRef.current = null + const picked: Record = {} + for (const po of items) { + for (const item of po.items) { + const allocs = prefill.byItem[item.id] + if (!allocs) continue + picked[item.id] = { + shopPOItemId: item.id, + itemId: item.itemId, + itemName: item.itemName, + storeId: po.storeId, + storeName: po.storeName, + shopPOId: po.id, + orderedQty: item.quantity, + loadedQty: Math.max(item.quantityFulfilled, loaded[item.id] ?? 0), + remainingQty: item.quantity - Math.max(item.quantityFulfilled, loaded[item.id] ?? 0), + checked: true, + quantity: allocs.find((a) => a.warehouseId === prefill.warehouseId)?.quantity ?? 0, + extras: allocs.filter((a) => a.warehouseId !== prefill.warehouseId), + } + } + } + setLines(picked) + } }) .catch((err) => toast.add({ title: 'Failed to load Shop POs', description: getApiError(err), type: 'error' })) .finally(() => setLoadingPos(false)) - }, [routeId]) + }, [routeId, sheetId]) + + const neededWarehouses = useMemo(() => { + const ids = new Set() + if (warehouseId) ids.add(warehouseId) + for (const l of Object.values(lines)) for (const e of l.extras) if (e.warehouseId) ids.add(e.warehouseId) + for (const m of manualItems) if (m.warehouseId) ids.add(m.warehouseId) + return Array.from(ids).sort().join(',') + }, [warehouseId, lines, manualItems]) + + useEffect(() => { + for (const id of neededWarehouses.split(',').filter(Boolean)) { + if (requestedStockRef.current.has(id)) continue + requestedStockRef.current.add(id) + stockApi.balances({ locationId: id, pageSize: 1000 }) + .then((res) => { + const byItem: Record = {} + for (const b of getPaginatedItems(res)) byItem[b.itemId] = b.quantity + setStockByWh((prev) => ({ ...prev, [id]: byItem })) + }) + .catch((err) => { + requestedStockRef.current.delete(id) + toast.add({ title: 'Failed to load warehouse stock', description: getApiError(err), type: 'error' }) + }) + } + }, [neededWarehouses]) + + const stockLoaded = !!warehouseId && !!stockByWh[warehouseId] + const availIn = (whId: string, itemId: string) => stockByWh[whId]?.[itemId] ?? 0 + const extrasTotal = (l: PickedLine) => l.extras.reduce((sum, e) => sum + (Number(e.quantity) || 0), 0) + + // Keep quantities within stock when the main warehouse changes or its stock loads + useEffect(() => { + if (!stockLoaded) return + setLines((prev) => { + let changed = false + const next: Record = {} + for (const [key, l] of Object.entries(prev)) { + const extras = l.extras.filter((e) => e.warehouseId !== warehouseId) + const avail = stockByWh[warehouseId]?.[l.itemId] ?? 0 + const quantity = Math.min(l.quantity, avail) + const checked = l.checked && (quantity > 0 || extras.length > 0) + if (extras.length !== l.extras.length || quantity !== l.quantity || checked !== l.checked) changed = true + next[key] = { ...l, extras, quantity, checked } + } + return changed ? next : prev + }) + }, [warehouseId, stockLoaded, stockByWh]) const rows = useMemo(() => { const result: PickedLine[] = [] for (const po of pos) { if (storeId && po.storeId !== storeId) continue for (const item of po.items) { - const remaining = item.quantity - item.quantityFulfilled + const loadedQty = Math.max(item.quantityFulfilled, loadedByPoItem[item.id] ?? 0) + const remaining = item.quantity - loadedQty if (remaining <= 0) continue result.push( lines[item.id] ?? { @@ -152,23 +313,28 @@ export default function NewLoadingSheetPage() { storeName: po.storeName, shopPOId: po.id, orderedQty: item.quantity, + loadedQty, remainingQty: remaining, checked: false, quantity: remaining, + extras: [], } ) } } return result - }, [pos, storeId, lines]) + }, [pos, storeId, lines, loadedByPoItem]) const selectedRows = useMemo( () => rows.filter((r) => (lines[r.shopPOItemId]?.checked ?? false)), [rows, lines] ) const totalQuantity = useMemo( - () => selectedRows.reduce((sum, r) => sum + (Number(lines[r.shopPOItemId]?.quantity) || 0), 0), - [selectedRows, lines] + () => selectedRows.reduce((sum, r) => { + const l = lines[r.shopPOItemId] + return sum + (Number(l?.quantity) || 0) + (l ? extrasTotal(l) : 0) + }, 0) + manualItems.reduce((sum, m) => sum + (Number(m.quantity) || 0), 0), + [selectedRows, lines, manualItems] ) const shopGroups = useMemo(() => { @@ -187,25 +353,96 @@ export default function NewLoadingSheetPage() { const itemSummary = useMemo(() => { const map = new Map }>() for (const row of selectedRows) { - const qty = Number(lines[row.shopPOItemId]?.quantity) || 0 + const l = lines[row.shopPOItemId] + const qty = (Number(l?.quantity) || 0) + (l ? extrasTotal(l) : 0) const e = map.get(row.itemId) ?? { itemName: row.itemName, qty: 0, shops: new Set() } e.qty += qty e.shops.add(row.storeId) map.set(row.itemId, e) } + for (const m of manualItems) { + if (!m.itemId) continue + const e = map.get(m.itemId) ?? { itemName: itemsList.find((i) => i.id === m.itemId)?.name ?? '', qty: 0, shops: new Set() } + e.qty += Number(m.quantity) || 0 + map.set(m.itemId, e) + } return Array.from(map.values()).sort((a, b) => a.itemName.localeCompare(b.itemName)) - }, [selectedRows, lines]) + }, [selectedRows, lines, manualItems, itemsList]) + + const stockOverview = useMemo(() => { + const map = new Map() + for (const row of rows) { + const e = map.get(row.itemId) ?? { itemId: row.itemId, itemName: row.itemName, open: 0 } + e.open += row.remainingQty + map.set(row.itemId, e) + } + return Array.from(map.values()).sort((a, b) => a.itemName.localeCompare(b.itemName)) + }, [rows]) + + function addManualItem() { + setManualItems((prev) => [...prev, { key: crypto.randomUUID(), itemId: '', warehouseId, quantity: 0 }]) + } + + function updateManualItem(key: string, patch: Partial) { + setManualItems((prev) => prev.map((m) => { + if (m.key !== key) return m + const next = { ...m, ...patch } + const max = availIn(next.warehouseId, next.itemId) + // item/warehouse change resets to the available quantity; a typed quantity is capped by stock + next.quantity = patch.quantity === undefined + ? (next.itemId && stockByWh[next.warehouseId] ? Math.min(max, next.quantity || max) : next.quantity) + : Math.max(0, Math.min(patch.quantity, max)) + return next + })) + } + + function removeManualItem(key: string) { + setManualItems((prev) => prev.filter((m) => m.key !== key)) + } function toggleLine(line: PickedLine, checked: boolean) { + if (checked) { + const avail = availIn(warehouseId, line.itemId) + if (!stockLoaded || avail <= 0) return + const quantity = Math.max(0, Math.min(line.remainingQty - extrasTotal(line), avail)) + setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked, quantity } })) + return + } setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked } })) } function setLineQuantity(line: PickedLine, quantity: number) { - setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, quantity } })) + const max = Math.max(0, Math.min(line.remainingQty - extrasTotal(line), availIn(warehouseId, line.itemId))) + setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, quantity: Math.min(Math.max(0, quantity), max) } })) + } + + function updateExtras(line: PickedLine, extras: ExtraAllocation[]) { + setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, extras } })) + } + + function addExtra(line: PickedLine) { + updateExtras(line, [...line.extras, { warehouseId: '', quantity: 0 }]) + } + + function setExtraWarehouse(line: PickedLine, index: number, whId: string) { + const remaining = line.remainingQty - line.quantity - line.extras.reduce((s, e, i) => (i === index ? s : s + (Number(e.quantity) || 0)), 0) + const quantity = Math.max(0, Math.min(remaining, availIn(whId, line.itemId))) + updateExtras(line, line.extras.map((e, i) => (i === index ? { warehouseId: whId, quantity } : e))) + } + + function setExtraQuantity(line: PickedLine, index: number, quantity: number) { + const e = line.extras[index] + const others = line.quantity + line.extras.reduce((s, x, i) => (i === index ? s : s + (Number(x.quantity) || 0)), 0) + const max = Math.max(0, Math.min(line.remainingQty - others, availIn(e.warehouseId, line.itemId))) + updateExtras(line, line.extras.map((x, i) => (i === index ? { ...x, quantity: Math.min(Math.max(0, quantity), max) } : x))) + } + + function removeExtra(line: PickedLine, index: number) { + updateExtras(line, line.extras.filter((_, i) => i !== index)) } async function handleSubmit() { - if (!routeId) { + if (!routeId && manualItems.length === 0) { toast.add({ title: 'Select a route first', type: 'error' }) return } @@ -213,7 +450,7 @@ export default function NewLoadingSheetPage() { toast.add({ title: 'Select a warehouse', type: 'error' }) return } - if (selectedRows.length === 0) { + if (selectedRows.length === 0 && manualItems.length === 0) { toast.add({ title: 'Select at least one item', type: 'error' }) return } @@ -226,25 +463,65 @@ export default function NewLoadingSheetPage() { return } - const items: LoadingSheetItemInput[] = selectedRows.map((r) => { + const items: LoadingSheetItemInput[] = [] + for (const r of selectedRows) { const line = lines[r.shopPOItemId] - return { - fK_ItemId: line.itemId, - fK_WarehouseId: warehouseId, - quantity: line.quantity, - fK_ShopPOItemId: line.shopPOItemId, + const allocations = [{ warehouseId, quantity: line.quantity }, ...line.extras] + for (const a of allocations) { + if (!a.warehouseId || !(a.quantity > 0)) continue + if (a.quantity > availIn(a.warehouseId, line.itemId)) { + toast.add({ title: `Not enough stock for ${line.itemName} in the selected warehouse`, type: 'error' }) + return + } + items.push({ + fK_ItemId: line.itemId, + fK_WarehouseId: a.warehouseId, + quantity: a.quantity, + fK_ShopPOItemId: line.shopPOItemId, + }) } - }) + } + const seen = new Set() + for (const m of manualItems) { + const name = itemsList.find((i) => i.id === m.itemId)?.name ?? 'item' + if (!m.itemId || !m.warehouseId || !(m.quantity > 0)) { + toast.add({ title: 'Complete or remove the manually added items', description: 'Each needs an item, a warehouse and a quantity above zero.', type: 'error' }) + return + } + if (m.quantity > availIn(m.warehouseId, m.itemId)) { + toast.add({ title: `Not enough stock for ${name} in the selected warehouse`, type: 'error' }) + return + } + const dupKey = m.itemId + '|' + m.warehouseId + if (seen.has(dupKey)) { + toast.add({ title: `${name} is added twice for the same warehouse`, type: 'error' }) + return + } + seen.add(dupKey) + items.push({ fK_ItemId: m.itemId, fK_WarehouseId: m.warehouseId, quantity: m.quantity, fK_ShopPOItemId: null }) + } + if (items.length === 0) { + toast.add({ title: 'Enter a quantity greater than zero', type: 'error' }) + return + } setSubmitting(true) try { - const existing = getPaginatedItems( - await loadingSheetsApi.list({ rideId, status: 'Planned', pageSize: 1 }) - )[0] + const existing = sheetId + ? await loadingSheetsApi.get(sheetId) + : getPaginatedItems(await loadingSheetsApi.list({ rideId, status: 'Planned', pageSize: 1 }))[0] const sheet = existing ?? await loadingSheetsApi.create({ fK_RideId: rideId, populatedByUserId }) + // The backend replaces the sheet's items with what is sent, so when topping up an + // existing planned sheet keep the rows it already has. + if (!isEdit && existing) { + for (const e of existing.items) { + if (items.some((i) => i.fK_ItemId === e.fK_ItemId && i.fK_WarehouseId === e.fK_WarehouseId && (i.fK_ShopPOItemId ?? null) === e.fK_ShopPOItemId)) continue + items.push({ fK_ItemId: e.fK_ItemId, fK_WarehouseId: e.fK_WarehouseId, quantity: e.quantityDispatched, fK_ShopPOItemId: e.fK_ShopPOItemId }) + } + } await loadingSheetsApi.updateItems(sheet.id, items) toast.add({ - title: existing ? 'Items added to the ride\'s existing planned loading sheet' : 'Loading sheet created', + title: isEdit ? 'Loading sheet updated' : existing ? 'Items added to the ride\'s existing planned loading sheet' : 'Loading sheet created', type: 'success', }) router.push('/operations/loading') @@ -263,7 +540,7 @@ export default function NewLoadingSheetPage() { Back
-

New Loading Sheet

+

{isEdit ? 'Edit Loading Sheet' : 'New Loading Sheet'}

Select a route first. Confirmed Shop POs for that route's shops will then be available to pick items from. Pick the ride to load last. @@ -358,12 +635,14 @@ export default function NewLoadingSheetPage() { ) : ( shopGroups.map((group) => { const groupLines = group.rows.map((r) => lines[r.shopPOItemId] ?? r) - const allChecked = groupLines.every((l) => l.checked) + const tickable = groupLines.filter((l) => stockLoaded && availIn(warehouseId, l.itemId) > 0) + const allChecked = tickable.length > 0 && tickable.every((l) => l.checked) const someChecked = groupLines.some((l) => l.checked) return (

group.rows.forEach((r) => toggleLine(lines[r.shopPOItemId] ?? r, v === true))} /> @@ -386,6 +665,48 @@ export default function NewLoadingSheetPage() { )}
+ {routeId && stockOverview.length > 0 && ( +
+
+ Stock in {warehousesList.find((w) => w.id === warehouseId)?.name ?? 'selected warehouse'} +
+ {!warehouseId ? ( +

Select a warehouse to see available stock.

+ ) : ( + + + + + + + + + + + {stockOverview.map((o) => { + const avail = availIn(warehouseId, o.itemId) + return ( + + + + + + + ) + })} + +
ItemOpen on POsIn stockCan load
{o.itemName}{o.open} + {!stockLoaded ? '…' : avail <= 0 ? 'No stock' : avail} + + {stockLoaded ? Math.min(o.open, avail) : '…'} + {stockLoaded && avail < o.open && ( + ({o.open - Math.min(o.open, avail)} short) + )} +
+ )} +
+ )} + !v && setViewStoreId('')}> @@ -394,6 +715,9 @@ export default function NewLoadingSheetPage() { Confirmed PO items. Tick the items to load and set the quantity. + {!warehouseId && ( +

Select a warehouse first to see available stock.

+ )}
@@ -402,34 +726,93 @@ export default function NewLoadingSheetPage() { + + + {viewGroup?.rows.map((row) => { const line = lines[row.shopPOItemId] ?? row + const avail = availIn(warehouseId, row.itemId) + const noStock = !warehouseId || !stockLoaded || avail <= 0 + const shortBy = row.remainingQty - line.quantity - extrasTotal(line) return ( - - - - - - - + + + + + + + + + + + + {line.checked && line.extras.map((extra, idx) => ( + + + + + + + ))} + {line.checked && shortBy > 0 && ( + + + + + )} + ) })} @@ -442,13 +825,86 @@ export default function NewLoadingSheetPage() { +
+
+
+

Additional Items

+

+ Load items that are not on any Shop PO, such as extra stock for the vehicle. +

+
+ +
+ + {manualItems.length === 0 ? ( +

No additional items.

+ ) : ( +
+ {manualItems.map((m) => { + const avail = availIn(m.warehouseId, m.itemId) + const loaded = !!stockByWh[m.warehouseId] + return ( +
+
+ + +
+
+ + +
+
+ In stock + + {!m.itemId || !m.warehouseId ? '—' : !loaded ? '…' : avail <= 0 ? 'No stock' : avail} + +
+
+ + updateManualItem(m.key, { quantity: Number(e.target.value) })} + /> +
+ +
+ ) + })} +
+ )} +
+

Ride & Assignment

- setRideId(v ?? '')} disabled={isEdit}> @@ -498,7 +954,7 @@ export default function NewLoadingSheetPage() { {e.itemName} - {e.shops.size} shop{e.shops.size === 1 ? '' : 's'} + {e.shops.size > 0 ? `${e.shops.size} shop${e.shops.size === 1 ? '' : 's'}` : 'added manually'} {e.qty} @@ -519,7 +975,7 @@ export default function NewLoadingSheetPage() { Cancel
Item PO OrderedLoadedOpenIn stock Qty to load
- toggleLine(row, v === true)} - /> - {row.itemName}{row.shopPOId.substring(0, 8)}{row.remainingQty} - setLineQuantity(row, Number(e.target.value))} - /> -
+ toggleLine(row, v === true)} + /> + {row.itemName}{row.shopPOId.substring(0, 8)}{row.orderedQty}{row.loadedQty}{row.remainingQty} + {!warehouseId ? '—' : !stockLoaded ? '…' : avail <= 0 ? 'No stock' : avail} + + setLineQuantity(row, Number(e.target.value))} + /> +
+ + + {extra.warehouseId ? (stockByWh[extra.warehouseId] ? (availIn(extra.warehouseId, row.itemId) || 'No stock') : '…') : '—'} + +
+ setExtraQuantity(line, idx, Number(e.target.value))} + /> + +
+
+ {shortBy} will stay open on the PO for another sheet + +