From 89c0c17be7198e7ea879b7782fe7bf8744da5407 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Tue, 29 Sep 2026 17:07:16 +0530 Subject: [PATCH 1/2] ui fix --- Distribution/Features/ShopPO/ShopPODTOs.cs | 2 + Distribution/Features/ShopPO/ShopPOService.cs | 1 + .../loading-sheet-items-dialog.tsx | 618 ++++++++++++------ .../sales/_components/sales-client.tsx | 23 +- distribution_fe/types/api.ts | 2 + 5 files changed, 424 insertions(+), 222 deletions(-) diff --git a/Distribution/Features/ShopPO/ShopPODTOs.cs b/Distribution/Features/ShopPO/ShopPODTOs.cs index 63f3a05..39bc7af 100644 --- a/Distribution/Features/ShopPO/ShopPODTOs.cs +++ b/Distribution/Features/ShopPO/ShopPODTOs.cs @@ -14,6 +14,8 @@ public record ShopPOResponse( Guid Id, Guid StoreId, string StoreName, + string? StoreAddress, + string? StoreContactNumber, Guid RaisedById, Guid? InvoiceId, decimal TotalAmount, diff --git a/Distribution/Features/ShopPO/ShopPOService.cs b/Distribution/Features/ShopPO/ShopPOService.cs index aa07213..98a04a9 100644 --- a/Distribution/Features/ShopPO/ShopPOService.cs +++ b/Distribution/Features/ShopPO/ShopPOService.cs @@ -14,6 +14,7 @@ public class ShopPOService( { private static ShopPOResponse ToResponse(Models.ShopPO p, Dictionary? users = null) => new(p.Id, p.FK_StoreId, p.Store?.StoreName ?? string.Empty, + p.Store?.StoreAddress, p.Store?.ContactNumber, p.RaisedById, p.InvoiceId, p.TotalAmount, p.Status, p.ConfirmedAt, p.ConfirmedBy, p.FulfilledAt, p.Items.Where(i => i.IsActive).Select(i => 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 d3d80e1..aa18dad 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 @@ -20,14 +20,16 @@ import { Checkbox } from '@/components/ui/checkbox' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { HugeiconsIcon } from '@hugeicons/react' -import { ViewIcon } from '@hugeicons/core-free-icons' +import { ArrowDown01Icon, ArrowUp01Icon, Alert02Icon } from '@hugeicons/core-free-icons' import { toast } from '@/components/ui/toast' +import { cn } from '@/lib/utils' import { shopPos as shopPosApi, regions as regionsApi, routes as routesApi, warehouses as warehousesApi, loadingSheets as loadingSheetsApi, + stock as stockApi, getApiError, getPaginatedItems, } from '@/lib/api/index' @@ -35,6 +37,26 @@ import type { LoadingSheetResponse, ShopPOResponse, RegionResponse, RouteRespons import type { LoadingSheetItemInput } from '@/lib/api/operations' const ALL = '__all__' +const STEPS = [ + { + n: 1, + label: 'Route & Warehouse', + title: 'Where is this load coming from?', + help: 'Pick the route this ride will deliver, and the warehouse the vehicle will be loaded from. Every item on this sheet will be dispatched from that one warehouse.', + }, + { + n: 2, + label: 'Pick Items', + title: 'What should be loaded?', + help: "Click a shop to open its pending orders, tick the items to load, and adjust quantities if needed. The \"In stock\" column shows what's on hand at the warehouse you picked.", + }, + { + n: 3, + label: 'Review & Confirm', + title: 'Double-check before saving', + help: 'This is everything that will be saved to the sheet. Items marked "Short" have more requested than is in stock — confirming the sheet will fail until those are fixed.', + }, +] as const interface LoadingSheetItemsDialogProps { open: boolean @@ -57,6 +79,8 @@ interface PickedLine { } export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: LoadingSheetItemsDialogProps) { + const [step, setStep] = useState<1 | 2 | 3>(1) + const [regionsList, setRegionsList] = useState([]) const [routesList, setRoutesList] = useState([]) const [warehousesList, setWarehousesList] = useState([]) @@ -65,22 +89,27 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa const [routeId, setRouteId] = useState('') const [warehouseId, setWarehouseId] = useState('') const [shopId, setShopId] = useState('') - const [viewStoreId, setViewStoreId] = useState('') + const [expandedStores, setExpandedStores] = useState>(new Set()) const [pos, setPos] = useState([]) const [loading, setLoading] = useState(false) const [submitting, setSubmitting] = useState(false) const [lines, setLines] = useState>({}) + const [stockByItem, setStockByItem] = useState>({}) + const [stockLoading, setStockLoading] = useState(false) + useEffect(() => { if (!open) return + setStep(1) setRegionId('') setRouteId('') setWarehouseId('') setShopId('') - setViewStoreId('') + setExpandedStores(new Set()) setPos([]) setLines({}) + setStockByItem({}) Promise.all([ regionsApi.list({ pageSize: 200 }), @@ -108,7 +137,6 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa if (!open) return setLines({}) setShopId('') - setViewStoreId('') if (!routeId) { setPos([]) return @@ -121,7 +149,31 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa }) .catch((err) => toast.add({ title: 'Failed to load Shop POs', description: getApiError(err), type: 'error' })) .finally(() => setLoading(false)) - }, [open, routeId]) + }, [open, routeId, regionId]) + + // Stock in the chosen warehouse is fetched once the user moves past step 1, so + // step 2 can show — and warn against — quantities that exceed what's on hand. + useEffect(() => { + if (!open || !warehouseId || step < 2) return + setStockLoading(true) + stockApi.balances({ locationId: warehouseId, locationType: 'Warehouse', pageSize: 1000 }) + .then((res) => { + const map: Record = {} + for (const b of getPaginatedItems(res)) map[b.itemId] = b.quantity + setStockByItem(map) + }) + .catch((err) => toast.add({ title: 'Failed to load warehouse stock', description: getApiError(err), type: 'error' })) + .finally(() => setStockLoading(false)) + }, [open, warehouseId, step]) + + // First-time users land on step 2 with everything collapsed and no obvious next + // move, so the first shop is opened automatically to show what "pick items" means. + useEffect(() => { + if (step !== 2 || pos.length === 0 || expandedStores.size > 0) return + const firstStoreId = pos[0]?.storeId + if (firstStoreId) setExpandedStores(new Set([firstStoreId])) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [step, pos]) const rows = useMemo(() => { const result: PickedLine[] = [] @@ -164,22 +216,25 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa [allShopGroups, shopId] ) - const viewGroup = allShopGroups.find((g) => g.storeId === viewStoreId) + const selectedRows = useMemo( + () => rows.filter((r) => (lines[r.shopPOItemId]?.checked ?? false) && lines[r.shopPOItemId].quantity > 0), + [rows, lines] + ) const summary = useMemo(() => { - const map = new Map }>() - for (const row of rows) { - const line = lines[row.shopPOItemId] ?? row - if (!line.checked || !(line.quantity > 0)) continue - const e = map.get(row.itemId) ?? { itemName: row.itemName, qty: 0, shops: new Set() } + const map = new Map }>() + for (const row of selectedRows) { + const line = lines[row.shopPOItemId] + const e = map.get(row.itemId) ?? { itemId: row.itemId, itemName: row.itemName, qty: 0, available: stockByItem[row.itemId] ?? 0, shops: new Set() } e.qty += line.quantity e.shops.add(row.storeId) map.set(row.itemId, e) } return Array.from(map.values()).sort((a, b) => a.itemName.localeCompare(b.itemName)) - }, [rows, lines]) + }, [selectedRows, lines, stockByItem]) const summaryTotal = summary.reduce((s, e) => s + e.qty, 0) + const shortItems = summary.filter((e) => e.qty > e.available) function toggleLine(line: PickedLine, checked: boolean) { setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, checked } })) @@ -189,23 +244,45 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa setLines((prev) => ({ ...prev, [line.shopPOItemId]: { ...line, quantity } })) } - 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 - } - const selected = rows.filter((r) => (lines[r.shopPOItemId]?.checked ?? false) && lines[r.shopPOItemId].quantity > 0) - if (selected.length === 0) { - toast.add({ title: 'Select at least one item', type: 'error' }) - return - } + function toggleExpanded(storeId: string) { + setExpandedStores((prev) => { + const next = new Set(prev) + if (next.has(storeId)) next.delete(storeId) + else next.add(storeId) + return next + }) + } - const items: LoadingSheetItemInput[] = selected.map((r) => { + function goNext() { + if (step === 1) { + if (!routeId) { + toast.add({ title: 'Select a route first', type: 'error' }) + return + } + if (!warehouseId) { + toast.add({ title: 'Select a warehouse first', type: 'error' }) + return + } + setStep(2) + return + } + if (step === 2) { + if (selectedRows.length === 0) { + toast.add({ title: 'Select at least one item', type: 'error' }) + return + } + setStep(3) + } + } + + function goBack() { + setStep((s) => (s === 3 ? 2 : 1) as 1 | 2) + } + + async function handleConfirmSave() { + if (!sheet) return + + const items: LoadingSheetItemInput[] = selectedRows.map((r) => { const line = lines[r.shopPOItemId] return { fK_ItemId: line.itemId, @@ -247,203 +324,314 @@ export function LoadingSheetItemsDialog({ open, onClose, onSuccess, sheet }: Loa if (!sheet) return null + const selectedWarehouseName = warehousesList.find((w) => w.id === warehouseId)?.name ?? '' + return ( !v && onClose()}> Set Loading Sheet Items - Select a route first. Confirmed Shop POs for that route will then be available to pick - items from. - {sheet.status === 'Confirmed' && ( - <> This sheet is already confirmed, so the items you pick are saved on a new planned sheet for the same ride. - )} + {sheet.status === 'Confirmed' + ? 'This sheet is already confirmed, so the items you pick are saved on a new planned sheet for the same ride.' + : 'Work through the steps below to build the loading sheet.'} -
-
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
- {!routeId ? ( -

Select a route to see shops with confirmed POs.

- ) : loading ? ( -

Loading…

- ) : shopGroups.length === 0 ? ( -

No shops with confirmed POs found for this selection.

- ) : ( - shopGroups.map((group) => { - const groupLines = group.rows.map((r) => lines[r.shopPOItemId] ?? r) - const allChecked = groupLines.every((l) => l.checked) - const someChecked = groupLines.some((l) => l.checked) - return ( -
- group.rows.forEach((r) => toggleLine(lines[r.shopPOItemId] ?? r, v === true))} - /> - {group.storeName} - - {group.poIds.size} PO{group.poIds.size === 1 ? '' : 's'} - - -
- ) - }) - )} -
- -
-
- Loading Sheet Summary - {summary.length} item{summary.length === 1 ? '' : 's'} · {summaryTotal} units -
- {summary.length === 0 ? ( -

Tick items above to build the summary.

- ) : ( - - - {summary.map((e) => ( - - - - - - ))} - -
{e.itemName}{e.shops.size} shop{e.shops.size === 1 ? '' : 's'}{e.qty}
- )} -
- - - - - - - !v && setViewStoreId('')}> - - - {viewGroup?.storeName} - - Confirmed PO items. Tick the items to load and set the quantity. - - -
- - - - - - - - - - - - {viewGroup?.rows.map((row) => { - const line = lines[row.shopPOItemId] ?? row - return ( - - - - - - - - ) - })} - -
ItemPOOrderedQty to load
- toggleLine(row, v === true)} - /> - {row.itemName}{row.shopPOId.substring(0, 8)}{row.remainingQty} - setLineQuantity(row, Number(e.target.value))} - /> -
+ {/* Step indicator */} +
+ {STEPS.map((s, idx) => ( +
+ + {s.label} + {idx < STEPS.length - 1 &&
}
- - - - -
+ ))} + + + {/* Step title & guidance */} +
+

{STEPS[step - 1].title}

+

{STEPS[step - 1].help}

+
+ + {/* Step 1: Route & Warehouse */} + {step === 1 && ( +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ )} + + {/* Step 2: Pick Items */} + {step === 2 && ( +
+
+
+ Route {routesList.find((r) => r.id === routeId)?.routeName} + {' · '}Warehouse {selectedWarehouseName} +
+ +
+ +
+ {loading ? ( +

Loading…

+ ) : shopGroups.length === 0 ? ( +

No shops with confirmed POs found for this selection.

+ ) : ( + shopGroups.map((group) => { + const groupLines = group.rows.map((r) => lines[r.shopPOItemId] ?? r) + const allChecked = groupLines.every((l) => l.checked) + const someChecked = groupLines.some((l) => l.checked) + const checkedCount = groupLines.filter((l) => l.checked).length + const expanded = expandedStores.has(group.storeId) + return ( +
+
+ group.rows.forEach((r) => toggleLine(lines[r.shopPOItemId] ?? r, v === true))} + /> + +
+ {expanded && ( +
+ + + + + + + + + + + + {group.rows.map((row) => { + const line = lines[row.shopPOItemId] ?? row + const available = stockByItem[row.itemId] ?? 0 + const short = line.checked && line.quantity > available + return ( + + + + + + + + ) + })} + +
ItemOrderedIn stockQty to load
+ toggleLine(row, v === true)} + /> + {row.itemName}{row.remainingQty} + {stockLoading ? '…' : available} + +
+ setLineQuantity(row, Number(e.target.value))} + /> + {short && ( + + )} +
+ {short && ( +

+ Only {available} available — rest stays pending +

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

+ {selectedRows.length} item line{selectedRows.length === 1 ? '' : 's'} selected. Expand a shop to tick items and set quantities. +

+
+ )} + + {/* Step 3: Review & Confirm */} + {step === 3 && ( +
+
+ Route {routesList.find((r) => r.id === routeId)?.routeName} + {' · '}Warehouse {selectedWarehouseName} +
+ + {shortItems.length > 0 && ( +
+ + + {shortItems.length} item{shortItems.length === 1 ? '' : 's'} exceed{shortItems.length === 1 ? 's' : ''} what's + in stock at {selectedWarehouseName}. Saving will still work, but confirming this sheet will fail until the + quantities are reduced to what's available — go back to adjust them. + +
+ )} + +
+
+ Loading Sheet Summary + {summary.length} item{summary.length === 1 ? '' : 's'} · {summaryTotal} units +
+ {summary.length === 0 ? ( +

No items selected.

+ ) : ( + + + + + + + + + + + + {summary.map((e) => { + const shortQty = Math.max(0, e.qty - e.available) + return ( + + + + + + + + ) + })} + +
ItemShopsIn stockQty to loadShort
{e.itemName}{e.shops.size} shop{e.shops.size === 1 ? '' : 's'}{e.available}{e.qty} 0 ? 'font-medium text-amber-600' : 'text-muted-foreground')}> + {shortQty > 0 ? shortQty : '—'} +
+ )} +
+
+ )} + + +
+ {step > 1 && ( + + )} +
+
+ + {step < 3 ? ( + + ) : ( + + )} +
+
) diff --git a/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx b/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx index d2704bb..96b9d4c 100644 --- a/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx +++ b/distribution_fe/app/(dashboard)/sales/_components/sales-client.tsx @@ -151,6 +151,20 @@ export function SalesClient() { fields: [{ key: 'storeName', primary: true }], minWidth: 180, }, + { + key: 'storeDetails', + title: 'Store Contact', + fields: ['storeAddress', 'storeContactNumber'], + minWidth: 200, + cellRender: (row) => ( +
+ {row.storeAddress ?? '—'} + {row.storeContactNumber && ( + {row.storeContactNumber} + )} +
+ ), + }, { key: 'totalAmount', title: 'Total', @@ -172,7 +186,7 @@ export function SalesClient() { { key: 'createdBy', title: 'Created By', - fields: ['raisedByName'], + fields: ['raisedByName', 'createdAt'], minWidth: 160, cellRender: (row) => (
@@ -180,6 +194,7 @@ export function SalesClient() { {row.raisedByRole && ( {row.raisedByRole} )} + {fmtDate(row.createdAt)}
), }, @@ -221,12 +236,6 @@ export function SalesClient() { — ), }, - { - key: 'createdAt', - title: 'Created', - fields: [{ key: 'createdAt', transform: (v) => fmtDate(v as string) }], - width: 140, - }, ], rowActions: { actions: () => [ diff --git a/distribution_fe/types/api.ts b/distribution_fe/types/api.ts index 31bbcc1..9527713 100644 --- a/distribution_fe/types/api.ts +++ b/distribution_fe/types/api.ts @@ -639,6 +639,8 @@ export interface ShopPOResponse { id: string storeId: string storeName: string + storeAddress: string | null + storeContactNumber: string | null raisedById: string invoiceId: string | null totalAmount: number From d8ba9568178b1e1c3cdeba46f1cee9f9dfd15db4 Mon Sep 17 00:00:00 2001 From: DeepnaPooja Date: Tue, 29 Sep 2026 17:29:02 +0530 Subject: [PATCH 2/2] fix loading sheet UI --- .../operations/loading/new/page.tsx | 134 ++++++++++++++++-- 1 file changed, 126 insertions(+), 8 deletions(-) diff --git a/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx b/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx index 5953b47..c3ebb7e 100644 --- a/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx +++ b/distribution_fe/app/(dashboard)/operations/loading/new/page.tsx @@ -25,6 +25,7 @@ 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 { cn } from '@/lib/utils' import { shopPos as shopPosApi, regions as regionsApi, @@ -53,6 +54,39 @@ import type { LoadingSheetItemInput } from '@/lib/api/operations' const ALL = '__all__' +const STEPS = [ + { + n: 1, + label: 'Route & Warehouse', + title: 'Where is this load coming from?', + help: "Pick the route this ride will deliver (optional if you're only adding items manually below) and the warehouse every item will be dispatched from.", + }, + { + n: 2, + label: 'Ride & Assignment', + title: 'Which vehicle, and who is populating this sheet?', + help: 'Choose the open ride whose vehicle gets loaded, and who is creating this sheet.', + }, + { + n: 3, + label: 'Items', + title: 'What should be loaded from Shop POs?', + help: "Expand a shop to tick items and set quantities. If the main warehouse doesn't have enough stock, use \"Take remaining from another warehouse\" to top up the rest.", + }, + { + n: 4, + label: 'Additional Items', + title: "Anything to load that's not on a PO?", + help: "Optional — add extra stock for the vehicle that isn't tied to any shop order.", + }, + { + n: 5, + label: 'Review & Confirm', + title: 'Double-check before saving', + help: 'This is everything that will be saved to the sheet. Fix anything short before confirming later on the loading sheet list.', + }, +] as const + interface ExtraAllocation { warehouseId: string quantity: number @@ -115,6 +149,8 @@ function LoadingSheetForm() { const [rideId, setRideId] = useState('') const [populatedByUserId, setPopulatedByUserId] = useState('') + const [step, setStep] = useState<1 | 2 | 3 | 4 | 5>(1) + const [viewStoreId, setViewStoreId] = useState('') const [itemsList, setItemsList] = useState([]) const [manualItems, setManualItems] = useState([]) @@ -441,6 +477,34 @@ function LoadingSheetForm() { updateExtras(line, line.extras.filter((_, i) => i !== index)) } + function goNext() { + if (step === 1) { + if (!warehouseId) { + toast.add({ title: 'Select a warehouse first', type: 'error' }) + return + } + setStep(2) + return + } + if (step === 2) { + 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 + } + setStep(3) + return + } + if (step < 5) setStep((s) => (s + 1) as typeof step) + } + + function goBack() { + setStep((s) => Math.max(1, s - 1) as typeof step) + } + async function handleSubmit() { if (!routeId && manualItems.length === 0) { toast.add({ title: 'Select a route first', type: 'error' }) @@ -541,13 +605,43 @@ function LoadingSheetForm() {

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

+

Work through the steps below to build the loading sheet.

+ {/* Step indicator */} +
+ {STEPS.map((s, idx) => ( +
+ + {s.label} + {idx < STEPS.length - 1 &&
} +
+ ))} +
+ + {/* Step title & guidance */} +
+

{STEPS[step - 1].title}

+

{STEPS[step - 1].help}

+
+ + {step === 1 && (

Route & Warehouse

@@ -616,7 +710,9 @@ function LoadingSheetForm() {
+ )} + {step === 3 && (

Items

@@ -824,7 +920,9 @@ function LoadingSheetForm() {
+ )} + {step === 4 && (
@@ -897,7 +995,9 @@ function LoadingSheetForm() {
)}
+ )} + {step === 2 && (

Ride & Assignment

@@ -935,7 +1035,9 @@ function LoadingSheetForm() {
+ )} + {step === 5 && (

Loading Sheet Summary

@@ -969,15 +1071,31 @@ function LoadingSheetForm() {

Total Quantity

{totalQuantity}

+
+
+ )} -
- + )} +
+
+ + {step < 5 ? ( + + ) : ( -
+ )}