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..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({ @@ -76,7 +78,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 +114,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,19 +231,22 @@ export default function ContainerReturnsPage() { - + 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" }, ]} /> + {filteredGroups.length === 0 ? ( @@ -282,7 +287,7 @@ export default function ContainerReturnsPage() { {group.companyName ?? "—"} - {group.returnType} + {group.returnType === "EDR" ? "EDR Last Mile" : "Customer Self-Haul"} @@ -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} + /> ); } @@ -416,7 +428,7 @@ function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: Con {group.bookingRef} - {group.returnType} + {group.returnType === "EDR" ? "EDR Last Mile" : "Customer Self-Haul"} @@ -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 + /> + +