From ae302d0c9cd108bee964807c8202e458172f9732 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Wed, 23 Sep 2026 12:46:23 +0530 Subject: [PATCH] Fix loading sheet creation flow --- .../LoadingEvent/LoadingSheetController.cs | 16 +- .../Features/LoadingEvent/LoadingSheetDTOs.cs | 4 +- .../LoadingEvent/LoadingSheetService.cs | 53 ++- ...g.tsx => confirm-loading-sheet-dialog.tsx} | 66 ++- .../loading-sheet-items-dialog.tsx | 32 +- .../_components/loading-sheets-client.tsx | 32 +- .../operations/loading/new/page.tsx | 420 ++++++++++++++++++ 7 files changed, 540 insertions(+), 83 deletions(-) rename distribution_fe/app/(dashboard)/operations/loading/_components/{loading-sheet-create-dialog.tsx => confirm-loading-sheet-dialog.tsx} (50%) create mode 100644 distribution_fe/app/(dashboard)/operations/loading/new/page.tsx diff --git a/Distribution/Features/LoadingEvent/LoadingSheetController.cs b/Distribution/Features/LoadingEvent/LoadingSheetController.cs index 96fc079..a028b50 100644 --- a/Distribution/Features/LoadingEvent/LoadingSheetController.cs +++ b/Distribution/Features/LoadingEvent/LoadingSheetController.cs @@ -31,10 +31,13 @@ public class LoadingSheetController(LoadingSheetService service, ICurrentUserSer [HttpPost] public async Task Create(CreateLoadingSheetRequest request, CancellationToken ct) { - var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + // TEMPORARY: currentUser.UserId is always unset due to the broken auth middleware + // (testbug.md #28) — fall back to the explicitly supplied user until that's fixed. + var userId = currentUser.UserId ?? request.PopulatedByUserId; + if (userId is null) return BadRequest(new { error = "PopulatedByUserId is required while auth is unavailable" }); try { - var result = await service.CreateAsync(request, userId, ct); + var result = await service.CreateAsync(request, userId.Value, ct); return CreatedAtAction(nameof(GetById), new { id = result.Id }, result); } catch (KeyNotFoundException) { return NotFound(); } @@ -54,12 +57,15 @@ public class LoadingSheetController(LoadingSheetService service, ICurrentUserSer } [HttpPost("{id:guid}/confirm")] - public async Task Confirm(Guid id, CancellationToken ct) + public async Task Confirm(Guid id, ConfirmLoadingSheetRequest request, CancellationToken ct) { - var userId = currentUser.UserId ?? throw new UnauthorizedAccessException(); + // TEMPORARY: currentUser.UserId is always unset due to the broken auth middleware + // (testbug.md #28) — fall back to the explicitly supplied user until that's fixed. + var userId = currentUser.UserId ?? request.ConfirmedByUserId; + if (userId is null) return BadRequest(new { error = "ConfirmedByUserId is required while auth is unavailable" }); try { - var result = await service.ConfirmAsync(id, userId, ct); + var result = await service.ConfirmAsync(id, userId.Value, ct); return Ok(result); } catch (KeyNotFoundException) { return NotFound(); } diff --git a/Distribution/Features/LoadingEvent/LoadingSheetDTOs.cs b/Distribution/Features/LoadingEvent/LoadingSheetDTOs.cs index cbd9a23..cf4b28b 100644 --- a/Distribution/Features/LoadingEvent/LoadingSheetDTOs.cs +++ b/Distribution/Features/LoadingEvent/LoadingSheetDTOs.cs @@ -25,7 +25,9 @@ public record LoadingSheetItemResponse( public record LoadingSheetOrderResponse(Guid ShopPOId); -public record CreateLoadingSheetRequest(Guid FK_RideId); +public record CreateLoadingSheetRequest(Guid FK_RideId, Guid? PopulatedByUserId = null); public record SetItemsRequest(List Items); public record SetItemInput(Guid FK_ItemId, Guid FK_WarehouseId, decimal Quantity, Guid? FK_ShopPOItemId = null); + +public record ConfirmLoadingSheetRequest(Guid? ConfirmedByUserId = null); diff --git a/Distribution/Features/LoadingEvent/LoadingSheetService.cs b/Distribution/Features/LoadingEvent/LoadingSheetService.cs index 07507cd..a5f4ad6 100644 --- a/Distribution/Features/LoadingEvent/LoadingSheetService.cs +++ b/Distribution/Features/LoadingEvent/LoadingSheetService.cs @@ -64,26 +64,47 @@ public class LoadingSheetService( if (sheet.Status != LoadingSheetStatus.Planned) throw new InvalidOperationException("Items can only be changed while the sheet is Planned"); - var shopPOItemIds = request.Items - .Where(i => i.FK_ShopPOItemId.HasValue) - .Select(i => i.FK_ShopPOItemId!.Value) - .ToList(); - var shopPOIds = shopPOItemIds.Count > 0 - ? await repo.GetShopPOIdsByItemIdsAsync(shopPOItemIds) - : new List(); - + // 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. await uow.BeginAsync(ct); try { - repo.RemoveItems(sheet.Items); - await repo.AddItemsAsync(request.Items.Select(i => new LoadingSheetItem + var newItems = new List(); + foreach (var input in request.Items) { - FK_LoadingSheetId = sheet.Id, - FK_ItemId = i.FK_ItemId, - FK_WarehouseId = i.FK_WarehouseId, - FK_ShopPOItemId = i.FK_ShopPOItemId, - QuantityDispatched = i.Quantity - })); + var existing = sheet.Items.FirstOrDefault(i => + i.FK_ItemId == input.FK_ItemId && + i.FK_WarehouseId == input.FK_WarehouseId && + i.FK_ShopPOItemId == input.FK_ShopPOItemId); + + if (existing is not null) + existing.QuantityDispatched = input.Quantity; + else + newItems.Add(new LoadingSheetItem + { + FK_LoadingSheetId = sheet.Id, + FK_ItemId = input.FK_ItemId, + FK_WarehouseId = input.FK_WarehouseId, + FK_ShopPOItemId = input.FK_ShopPOItemId, + QuantityDispatched = input.Quantity + }); + } + if (newItems.Count > 0) + await repo.AddItemsAsync(newItems); + + var shopPOItemIds = sheet.Items.Select(i => i.FK_ShopPOItemId) + .Concat(newItems.Select(i => i.FK_ShopPOItemId)) + .Where(x => x.HasValue) + .Select(x => x!.Value) + .Distinct() + .ToList(); + var shopPOIds = shopPOItemIds.Count > 0 + ? await repo.GetShopPOIdsByItemIdsAsync(shopPOItemIds) + : new List(); + await repo.ReplaceOrdersAsync(sheet.Id, shopPOIds); await uow.CommitAsync(ct); } diff --git a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-create-dialog.tsx b/distribution_fe/app/(dashboard)/operations/loading/_components/confirm-loading-sheet-dialog.tsx similarity index 50% rename from distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-create-dialog.tsx rename to distribution_fe/app/(dashboard)/operations/loading/_components/confirm-loading-sheet-dialog.tsx index b0031bc..3a5ad36 100644 --- a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-create-dialog.tsx +++ b/distribution_fe/app/(dashboard)/operations/loading/_components/confirm-loading-sheet-dialog.tsx @@ -14,83 +14,83 @@ import { Button } from '@/components/ui/button' import { Dialog, DialogContent, - DialogHeader, - DialogTitle, DialogDescription, DialogFooter, + DialogHeader, + DialogTitle, } from '@/components/ui/dialog' import { FieldGroup } from '@/components/ui/field' import { toast } from '@/components/ui/toast' -import { loadingSheets as loadingSheetsApi, rides as ridesApi, getApiError, getPaginatedItems } from '@/lib/api/index' -import type { RideResponse } from '@/types/api' +import { loadingSheets as loadingSheetsApi, users as usersApi, getApiError, getPaginatedItems } from '@/lib/api/index' +import type { LoadingSheetResponse, UserResponse } from '@/types/api' const schema = z.object({ - fK_RideId: z.string().min(1, 'Ride is required'), + confirmedByUserId: z.string().min(1, 'Confirmed by is required'), }) type FormValues = z.infer -interface LoadingSheetCreateDialogProps { +interface ConfirmLoadingSheetDialogProps { open: boolean onClose: () => void onSuccess: () => void + sheet: LoadingSheetResponse | null } -export function LoadingSheetCreateDialog({ open, onClose, onSuccess }: LoadingSheetCreateDialogProps) { - const [ridesList, setRidesList] = useState([]) +export function ConfirmLoadingSheetDialog({ open, onClose, onSuccess, sheet }: ConfirmLoadingSheetDialogProps) { + const [usersList, setUsersList] = useState([]) const form = useForm({ resolver: zodResolver(schema), - defaultValues: { fK_RideId: '' }, + defaultValues: { confirmedByUserId: '' }, }) useEffect(() => { if (!open) return - form.reset({ fK_RideId: '' }) - ridesApi.list({ status: 'Open', pageSize: 200 }) - .then((res) => setRidesList(getPaginatedItems(res))) - .catch((err) => toast.add({ title: 'Failed to load rides', description: getApiError(err), type: 'error' })) + form.reset({ confirmedByUserId: '' }) + usersApi.list({ pageSize: 200 }) + .then((res) => setUsersList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load users', description: getApiError(err), type: 'error' })) }, [open]) + if (!sheet) return null + async function onSubmit(values: FormValues) { try { - await loadingSheetsApi.create(values) - toast.add({ title: 'Loading sheet created', type: 'success' }) + await loadingSheetsApi.confirm(sheet!.id, values.confirmedByUserId) + toast.add({ title: 'Loading sheet confirmed — stock committed to vehicle', type: 'success' }) onSuccess() onClose() } catch (err) { - toast.add({ title: 'Failed to create loading sheet', description: getApiError(err), type: 'error' }) + toast.add({ title: 'Failed to confirm loading sheet', description: getApiError(err), type: 'error' }) } } return ( - !v && onClose()}> - + { if (!v) onClose() }}> + - New Loading Sheet - - Create a loading sheet for an open ride. Only one planned sheet per ride is allowed. - + Confirm Loading Sheet + {sheet.items.length} item(s) will be committed to the vehicle's stock.
( @@ -99,11 +99,9 @@ export function LoadingSheetCreateDialog({ open, onClose, onSuccess }: LoadingSh - +
diff --git a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-items-dialog.tsx b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-items-dialog.tsx index 5d9b929..5b1b20a 100644 --- a/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-items-dialog.tsx +++ b/distribution_fe/app/(dashboard)/operations/loading/_components/loading-sheet-items-dialog.tsx @@ -88,6 +88,8 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa useEffect(() => { if (!open) return setRouteId('') + setPos([]) + setLines({}) routesApi.list({ regionId: regionId || undefined, pageSize: 200 }) .then((res) => setRoutesList(getPaginatedItems(res))) .catch((err) => toast.add({ title: 'Failed to load routes', description: getApiError(err), type: 'error' })) @@ -95,15 +97,20 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa useEffect(() => { if (!open) return + setLines({}) + if (!routeId) { + setPos([]) + return + } setLoading(true) - shopPosApi.list({ regionId: regionId || undefined, routeId: routeId || undefined, confirmed: true, pageSize: 200 }) + shopPosApi.list({ regionId: regionId || undefined, routeId, confirmed: true, pageSize: 200 }) .then((res) => { const items = getPaginatedItems(res).filter((po) => po.status === 'Pending' || po.status === 'PartiallyFulfilled') setPos(items) }) .catch((err) => toast.add({ title: 'Failed to load Shop POs', description: getApiError(err), type: 'error' })) .finally(() => setLoading(false)) - }, [open, regionId, routeId]) + }, [open, routeId]) const rows = useMemo(() => { const result: PickedLine[] = [] @@ -138,6 +145,10 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa async function handleSubmit() { if (!sheet) return + if (!routeId) { + toast.add({ title: 'Select a route first', type: 'error' }) + return + } if (!warehouseId) { toast.add({ title: 'Select a warehouse first', type: 'error' }) return @@ -179,8 +190,8 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa Set Loading Sheet Items - Pick items from confirmed Shop POs to load onto the vehicle for this ride. Narrow the list - by region/route first. + Select a route first. Confirmed Shop POs for that route will then be available to pick + items from. @@ -201,13 +212,12 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa
- - setRouteId(v ?? '')}> - + - All routes {routesList.map((r) => ( {r.routeName} ))} @@ -242,10 +252,12 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa - {loading ? ( + {!routeId ? ( + Select a route to see its confirmed Shop POs. + ) : loading ? ( Loading… ) : rows.length === 0 ? ( - No eligible Shop PO items found for this filter. + No eligible Shop PO items found for this route. ) : ( rows.map((row) => { const line = lines[row.shopPOItemId] ?? row 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 b58ff27..e510d08 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 @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect, useCallback, useMemo } from 'react' +import { useRouter } from 'next/navigation' import { HugeiconsIcon } from '@hugeicons/react' import { Add01Icon } from '@hugeicons/core-free-icons' @@ -22,8 +23,8 @@ import type { LoadingSheetResponse, LoadingSheetStatus, RideResponse, RegionResp const ALL = '__all__' import { LoadingSheetViewSheet } from './loading-sheet-view-sheet' -import { LoadingSheetCreateDialog } from './loading-sheet-create-dialog' import { LoadingSheetItemsDialog } from './loading-sheet-items-dialog' +import { ConfirmLoadingSheetDialog } from './confirm-loading-sheet-dialog' const fmtDate = (v: string) => new Date(v).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) @@ -36,15 +37,16 @@ const LS_STATUS_VARIANT: Record([]) const [loading, setLoading] = useState(true) const [total, setTotal] = useState(0) const [page, setPage] = useState(1) const [pageSize, setPageSize] = useState(20) - const [createOpen, setCreateOpen] = useState(false) const [viewOpen, setViewOpen] = useState(false) const [itemsOpen, setItemsOpen] = useState(false) + const [confirmOpen, setConfirmOpen] = useState(false) const [selectedSheet, setSelectedSheet] = useState(null) const [ridesList, setRidesList] = useState([]) @@ -116,14 +118,9 @@ export function LoadingSheetsClient() { setItemsOpen(true) } - async function handleConfirm(row: LoadingSheetResponse) { - try { - await loadingSheetsApi.confirm(row.id) - toast.add({ title: 'Loading sheet confirmed — stock committed to vehicle', type: 'success' }) - fetchData(page, pageSize) - } catch (err) { - toast.add({ title: 'Failed to confirm loading sheet', description: getApiError(err), type: 'error' }) - } + function handleConfirm(row: LoadingSheetResponse) { + setSelectedSheet(row) + setConfirmOpen(true) } async function handleGenerateTasks(row: LoadingSheetResponse) { @@ -233,7 +230,7 @@ export function LoadingSheetsClient() { Items loaded onto a vehicle before a delivery run

- @@ -329,12 +326,6 @@ export function LoadingSheetsClient() { }} /> - setCreateOpen(false)} - onSuccess={() => fetchData(page, pageSize)} - /> - setViewOpen(false)} @@ -347,6 +338,13 @@ export function LoadingSheetsClient() { onSuccess={() => fetchData(page, pageSize)} sheet={selectedSheet} /> + + setConfirmOpen(false)} + onSuccess={() => fetchData(page, pageSize)} + sheet={selectedSheet} + /> ) } diff --git a/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx b/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx new file mode 100644 index 0000000..38dff7b --- /dev/null +++ b/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx @@ -0,0 +1,420 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { useRouter } from 'next/navigation' +import { HugeiconsIcon } from '@hugeicons/react' +import { ArrowLeft01Icon } from '@hugeicons/core-free-icons' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { Checkbox } from '@/components/ui/checkbox' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' +import { FieldGroup } from '@/components/ui/field' +import { Separator } from '@/components/ui/separator' +import { toast } from '@/components/ui/toast' +import { + shopPos as shopPosApi, + regions as regionsApi, + routes as routesApi, + stores as storesApi, + warehouses as warehousesApi, + rides as ridesApi, + users as usersApi, + loadingSheets as loadingSheetsApi, + getApiError, + getPaginatedItems, +} from '@/lib/api/index' +import type { + ShopPOResponse, + RegionResponse, + RouteResponse, + StoreResponse, + WarehouseResponse, + RideResponse, + UserResponse, +} from '@/types/api' +import type { LoadingSheetItemInput } from '@/lib/api/operations' + +const ALL = '__all__' + +interface PickedLine { + shopPOItemId: string + itemId: string + itemName: string + storeName: string + orderedQty: number + remainingQty: number + checked: boolean + quantity: number +} + +export default function NewLoadingSheetPage() { + const router = useRouter() + + const [regionsList, setRegionsList] = useState([]) + const [routesList, setRoutesList] = useState([]) + const [storesList, setStoresList] = useState([]) + const [warehousesList, setWarehousesList] = useState([]) + const [ridesList, setRidesList] = useState([]) + const [usersList, setUsersList] = useState([]) + + const [regionId, setRegionId] = useState('') + const [routeId, setRouteId] = useState('') + const [storeId, setStoreId] = useState('') + const [warehouseId, setWarehouseId] = useState('') + const [rideId, setRideId] = useState('') + const [populatedByUserId, setPopulatedByUserId] = useState('') + + const [pos, setPos] = useState([]) + const [loadingPos, setLoadingPos] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [lines, setLines] = useState>({}) + + useEffect(() => { + Promise.all([ + regionsApi.list({ pageSize: 200 }), + warehousesApi.list({ pageSize: 200 }), + ridesApi.list({ status: 'Open', pageSize: 200 }), + usersApi.list({ pageSize: 200 }), + ]) + .then(([regionsRes, warehousesRes, ridesRes, usersRes]) => { + 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' })) + }, []) + + useEffect(() => { + setRouteId('') + setStoreId('') + setPos([]) + setLines({}) + routesApi.list({ regionId: regionId || undefined, pageSize: 200 }) + .then((res) => setRoutesList(getPaginatedItems(res))) + .catch((err) => toast.add({ title: 'Failed to load routes', description: getApiError(err), type: 'error' })) + }, [regionId]) + + useEffect(() => { + setStoreId('') + setLines({}) + if (!routeId) { + setStoresList([]) + setPos([]) + return + } + storesApi.list({ routeId, pageSize: 200 }) + .then((res) => setStoresList(getPaginatedItems(res))) + .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) => { + const items = getPaginatedItems(res).filter((po) => po.status === 'Pending' || po.status === 'PartiallyFulfilled') + setPos(items) + }) + .catch((err) => toast.add({ title: 'Failed to load Shop POs', description: getApiError(err), type: 'error' })) + .finally(() => setLoadingPos(false)) + }, [routeId]) + + 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 + if (remaining <= 0) continue + result.push( + lines[item.id] ?? { + shopPOItemId: item.id, + itemId: item.itemId, + itemName: item.itemName, + storeName: po.storeName, + orderedQty: item.quantity, + remainingQty: remaining, + checked: false, + quantity: remaining, + } + ) + } + } + return result + }, [pos, storeId, lines]) + + 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] + ) + + function toggleLine(line: PickedLine, checked: boolean) { + setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked } })) + } + + function setLineQuantity(line: PickedLine, quantity: number) { + setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, quantity } })) + } + + async function handleSubmit() { + if (!routeId) { + toast.add({ title: 'Select a route first', type: 'error' }) + return + } + if (!warehouseId) { + toast.add({ title: 'Select a warehouse', type: 'error' }) + return + } + if (selectedRows.length === 0) { + toast.add({ title: 'Select at least one item', type: 'error' }) + return + } + if (!rideId) { + toast.add({ title: 'Select a ride to load the vehicle', type: 'error' }) + return + } + if (!populatedByUserId) { + toast.add({ title: 'Select who is populating this sheet', type: 'error' }) + return + } + + const items: LoadingSheetItemInput[] = selectedRows.map((r) => { + const line = lines[r.shopPOItemId] + return { + fK_ItemId: line.itemId, + fK_WarehouseId: warehouseId, + quantity: line.quantity, + fK_ShopPOItemId: line.shopPOItemId, + } + }) + + setSubmitting(true) + try { + const sheet = await loadingSheetsApi.create({ fK_RideId: rideId, populatedByUserId }) + await loadingSheetsApi.updateItems(sheet.id, items) + toast.add({ title: 'Loading sheet created', type: 'success' }) + router.push('/operations/loading') + } catch (err) { + toast.add({ title: 'Failed to create loading sheet', description: getApiError(err), type: 'error' }) + } finally { + setSubmitting(false) + } + } + + return ( +
+
+ +
+

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. +

+
+
+ +
+

Route & Warehouse

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+
+ +
+
+

Items

+

+ Pick items from confirmed Shop POs for the selected route/shop. +

+
+ +
+ + + + + + + + + + + + {!routeId ? ( + + ) : loadingPos ? ( + + ) : rows.length === 0 ? ( + + ) : ( + rows.map((row) => { + const line = lines[row.shopPOItemId] ?? row + return ( + + + + + + + + ) + }) + )} + +
ShopItemOrderedQty to load
Select a route to see its confirmed Shop POs.
Loading…
No eligible Shop PO items found for this route.
+ toggleLine(row, v === true)} + /> + {row.storeName}{row.itemName}{row.remainingQty} + setLineQuantity(row, Number(e.target.value))} + /> +
+
+
+ +
+

Ride & Assignment

+ +
+
+ + +
+ +
+ + + Temporary — required until auth resolves the logged-in user automatically. +
+
+
+
+ +
+
+

Loading Sheet Summary

+

+ {selectedRows.length} item{selectedRows.length === 1 ? '' : 's'} selected. +

+ + + +
+

Total Quantity

+

{totalQuantity}

+
+ +
+ + +
+
+
+
+ ) +}