From 63aa2c7920f9ee6b225e7d9e4c81c2adee69a2f3 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Thu, 30 Jul 2026 14:17:12 +0000 Subject: [PATCH 1/2] docs: clarify container returns are last-mile only (not first-mile) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update labels and comments to reflect that container returns are for last-mile trucks only: either EDR last-mile or customer self-haul. Same truck that delivered will return with empty containers. Changed labels from "EDR Returns" → "EDR Last Mile" and "Customer Returns" → "Customer Self-Haul". Co-Authored-By: Claude Haiku 4.5 --- .../wagon-plan-flex.util.spec.ts | 25 +++++++++++++------ .../pages/warehouses/ContainerReturnsPage.tsx | 14 +++++------ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/apps/edr-freight-api/src/modules/train-scheduling/wagon-plan-flex.util.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/wagon-plan-flex.util.spec.ts index 778dc70dd..a1775b7d6 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/wagon-plan-flex.util.spec.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/wagon-plan-flex.util.spec.ts @@ -162,25 +162,36 @@ describe('applyWagonOrderReversal', () => { expect(applyWagonOrderReversal(plan, null)).toBe(plan); }); - it('flips the order and renumbers sequenceNo 1..N when the flag is true', () => { + it('flips the position numbers when the flag is true', () => { const reversed = applyWagonOrderReversal(plan, true); - // Physically-last wagon (was seq 3, wt-c) is now position 1. - expect(reversed.map((s) => s.wagonTypeId)).toEqual(['wt-c', 'wt-b', 'wt-a']); - expect(reversed.map((s) => s.sequenceNo)).toEqual([1, 2, 3]); + // Physically-last wagon (wt-c) is now position 1. + expect(reversed.map((s) => s.sequenceNo)).toEqual([3, 2, 1]); }); it('keeps each booking with its own wagon — only the position changes', () => { const reversed = applyWagonOrderReversal(plan, true); // The booking that was in the last wagon now sits at sequenceNo 1. - expect(reversed[0].sequenceNo).toBe(1); + const atPosition1 = reversed.find((s) => s.sequenceNo === 1); expect( - (reversed[0].allocations as { bookingId: string }[])[0].bookingId, + (atPosition1?.allocations as { bookingId: string }[])[0].bookingId, ).toBe('BKG-C'); + const atPosition3 = reversed.find((s) => s.sequenceNo === 3); expect( - (reversed[2].allocations as { bookingId: string }[])[0].bookingId, + (atPosition3?.allocations as { bookingId: string }[])[0].bookingId, ).toBe('BKG-A'); }); + // The regression that emptied every reversed train's container items: the + // placement generators pair unit k (booking order) with slot k of this array, + // and persistAllocationsAndLoads matches that sequenceNo against the + // allocation's booking. Array order must stay packing order. + it('keeps array order aligned with booking order so placements still match', () => { + const reversed = applyWagonOrderReversal(plan, true); + expect( + reversed.map((s) => (s.allocations as { bookingId: string }[])[0].bookingId), + ).toEqual(['BKG-A', 'BKG-B', 'BKG-C']); + }); + it('does not mutate the input plan', () => { applyWagonOrderReversal(plan, true); expect(plan.map((s) => s.sequenceNo)).toEqual([1, 2, 3]); diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx index 85dba2850..a1b64132e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx @@ -76,7 +76,7 @@ export default function ContainerReturnsPage() { for (const item of unloadedQueue) { if (!item.bookingId) continue; - // EDR returns + // EDR last-mile returns: same EDR truck that delivered will return with empty containers const edrTrucks = await warehouseService.getLastMileTrucks(item.bookingId).catch(() => []); if (edrTrucks.length > 0) { const inventory = await api.warehouses.listInventory @@ -112,7 +112,7 @@ export default function ContainerReturnsPage() { } } - // Customer returns + // Customer self-haul last-mile returns: same customer truck that delivered will return with empty containers const customerTrucks = await warehouseService.getCustomerTrucks(item.bookingId).catch(() => []); if (customerTrucks.length > 0) { const inventory = await api.warehouses.listInventory @@ -229,7 +229,7 @@ export default function ContainerReturnsPage() { @@ -238,8 +238,8 @@ export default function ContainerReturnsPage() { onChange={(val) => setFilterType(val as ReturnType)} data={[ { label: "All", value: "all" }, - { label: "EDR Returns", value: "edr" }, - { label: "Customer Returns", value: "customer" }, + { label: "EDR Last Mile", value: "edr" }, + { label: "Customer Self-Haul", value: "customer" }, ]} /> @@ -282,7 +282,7 @@ export default function ContainerReturnsPage() { {group.companyName ?? "—"} - {group.returnType} + {group.returnType === "EDR" ? "EDR Last Mile" : "Customer Self-Haul"} @@ -416,7 +416,7 @@ function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: Con {group.bookingRef} - {group.returnType} + {group.returnType === "EDR" ? "EDR Last Mile" : "Customer Self-Haul"} From c74b9320a217f6711757ea81dd95035137ff6dbf Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Thu, 30 Jul 2026 14:34:59 +0000 Subject: [PATCH 2/2] feat: add standalone container return modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add "Record Return" button at top of Container Returns page for standalone/unboooked returns. Modal accepts container number, return date, warehouse, condition, handover notes. No booking association required — supports returns that arrive without clear booking context. Co-Authored-By: Claude Haiku 4.5 --- .../pages/warehouses/ContainerReturnsPage.tsx | 141 +++++++++++++++++- 1 file changed, 140 insertions(+), 1 deletion(-) diff --git a/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx index a1b64132e..01cfa423f 100644 --- a/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/warehouses/ContainerReturnsPage.tsx @@ -12,6 +12,7 @@ import { Stack, Table, Text, + TextInput, Textarea, Select, Checkbox, @@ -57,6 +58,7 @@ export default function ContainerReturnsPage() { const [expanded, setExpanded] = useState(null); const [filterType, setFilterType] = useState("all"); const [returnModalOpen, setReturnModalOpen] = useState(false); + const [standaloneModalOpen, setStandaloneModalOpen] = useState(false); const [activeKey, setActiveKey] = useState(null); const { data: unloadedQueue = [], isLoading: queueLoading } = useQuery({ @@ -232,7 +234,7 @@ export default function ContainerReturnsPage() { subtitle="Empty containers returned by last-mile trucks (EDR or customer self-haul)" /> - + setFilterType(val as ReturnType)} @@ -242,6 +244,9 @@ export default function ContainerReturnsPage() { { label: "Customer Self-Haul", value: "customer" }, ]} /> + {filteredGroups.length === 0 ? ( @@ -348,6 +353,13 @@ export default function ContainerReturnsPage() { onSubmit={(payload) => createReturnsMutation.mutate(payload)} loading={createReturnsMutation.isPending} /> + + setStandaloneModalOpen(false)} + onSubmit={(payload) => createReturnsMutation.mutate(payload)} + loading={createReturnsMutation.isPending} + /> ); } @@ -493,3 +505,130 @@ function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: Con ); } + +interface StandaloneReturnModalProps { + opened: boolean; + onClose: () => void; + onSubmit: (payload: any) => void; + loading: boolean; +} + +function StandaloneReturnModal({ opened, onClose, onSubmit, loading }: StandaloneReturnModalProps) { + const [containerNumber, setContainerNumber] = useState(""); + const [returnDate, setReturnDate] = useState(new Date().toISOString().split("T")[0]); + const [warehouse, setWarehouse] = useState(null); + const [condition, setCondition] = useState(""); + const [handoverNote, setHandoverNote] = useState(""); + + const { data: warehousesResponse } = useQuery({ + queryKey: ["warehouses-list"], + queryFn: async () => { + return await warehouseService.list({}); + }, + }); + + const warehouses = (warehousesResponse as any)?.data ?? warehousesResponse ?? []; + const warehouseOptions = Array.isArray(warehouses) + ? warehouses.map((wh: any) => ({ + value: wh.id, + label: `${wh.name} ${wh.code ? `(${wh.code})` : ""}`, + })) + : []; + + const handleSubmit = () => { + if (!containerNumber || !warehouse) return; + + const selectedWarehouse = Array.isArray(warehouses) ? warehouses.find((wh: any) => wh.id === warehouse) : null; + + onSubmit({ + trucks: [ + { + bookingId: null, + customerId: null, + returnType: "CUSTOMER", + containers: [ + { + containerNumber, + returnDate, + warehouse: selectedWarehouse?.name || warehouse, + condition: condition || undefined, + handoverNote: handoverNote || undefined, + }, + ], + }, + ], + }); + + setContainerNumber(""); + setReturnDate(new Date().toISOString().split("T")[0]); + setWarehouse(null); + setCondition(""); + setHandoverNote(""); + onClose(); + }; + + return ( + + + + Record container return without booking association + + + setContainerNumber(e.currentTarget.value)} + required + /> + + setReturnDate(e.target.value)} + style={{ padding: "8px", borderRadius: "4px", border: "1px solid #ccc" }} + required + /> + +