From c9300ab4cdfe93d7f751588ee30521f10c8d63d9 Mon Sep 17 00:00:00 2001 From: Hagernesh Date: Thu, 30 Jul 2026 13:52:59 +0000 Subject: [PATCH 1/3] feat: add EDR Last Mile Returns page with bulk/single form New page for empty container returns: tables show EDR trucks with company names and return containers. Modal form for processing single or bulk returns, with fields for facility, yard, condition, return date, and handover notes. Accessible via sidebar menu under Import Operations. Co-Authored-By: Claude Haiku 4.5 --- apps/edr-freight-web/backoffice/src/App.tsx | 8 + .../warehouses/EDRLastMileReturnsPage.tsx | 403 ++++++++++++++++++ 2 files changed, 411 insertions(+) create mode 100644 apps/edr-freight-web/backoffice/src/pages/warehouses/EDRLastMileReturnsPage.tsx diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index eda320c5b..2df7d462b 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -118,6 +118,7 @@ import TrainDetailPage from "./pages/trains/TrainDetailPage"; import TrainBuilderDetailPage from "./pages/trainBuilder/TrainBuilderDetailPage"; import TrainBuilderListPage from "./pages/trainBuilder/TrainBuilderListPage"; import ArrivalQueuePage from "./pages/warehouses/ArrivalQueuePage"; +import EDRLastMileReturnsPage from "./pages/warehouses/EDRLastMileReturnsPage"; import DispatchQueuePage from "./pages/warehouses/DispatchQueuePage"; import ExportDjiboutiUnloadingQueuePage from "./pages/warehouses/ExportDjiboutiUnloadingQueuePage"; import ExportWarehouseFlowPage from "./pages/warehouses/ExportWarehouseFlowPage"; @@ -411,6 +412,12 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ icon: , permission: FREIGHT_PERMS.warehouseInventory.view, }, + { + label: "EDR Last Mile Returns", + href: "/dashboard/edr-last-mile-returns", + icon: , + permission: FREIGHT_PERMS.warehouseInventory.view, + }, { label: "Dispatch Queue", href: "/dashboard/dispatch-queue", @@ -1060,6 +1067,7 @@ const App = () => { } /> } /> } /> + } /> } /> } /> (null); + const [returnModalOpen, setReturnModalOpen] = useState(false); + const [activeKey, setActiveKey] = useState(null); + + const { data: unloadedQueue = [], isLoading: queueLoading } = useQuery({ + queryKey: ["import-unloaded-queue"], + queryFn: async () => { + const response = await api.warehouses.importUnloadedQueue.call(); + return response ?? []; + }, + }); + + const bookingIds = unloadedQueue.map((item) => item.bookingId).filter(Boolean) as string[]; + const truckReturnsQuery = useQuery({ + queryKey: ["edr-last-mile-returns", bookingIds], + queryFn: async () => { + const grouped = new Map(); + + for (const item of unloadedQueue) { + if (!item.bookingId) continue; + + const edrTrucks = await warehouseService.getLastMileTrucks(item.bookingId).catch(() => []); + for (const truck of edrTrucks) { + const inventory = await api.warehouses.listInventory.call({ filter: { bookingId: item.bookingId } }).catch(() => []); + + const returnContainers = inventory + .filter((inv: any) => inv.isReturn) + .map((inv: any) => ({ + containerNumber: inv.containerNumber || "—", + size: inv.containerSize || null, + type: inv.containerType || null, + selected: false, + })); + + if (returnContainers.length > 0) { + const key = `${item.bookingId}-${truck.vehicleId}`; + grouped.set(key, { + key, + plate: [truck.truckPlateNumber, truck.trailerPlateNumber].filter(Boolean).join(" + ") || "—", + companyName: item.customerName ?? null, + bookingRef: item.bookingReference ?? item.bookingId, + bookingId: item.bookingId, + customerId: item.customerId || null, + containers: returnContainers, + }); + } + } + } + + return Array.from(grouped.values()); + }, + enabled: bookingIds.length > 0 && !queueLoading, + }); + + const trucksWithReturns = useMemo(() => truckReturnsQuery.data ?? [], [truckReturnsQuery.data]); + const controls = useListControls(trucksWithReturns, { + searchKeys: ["plate", "companyName", "bookingRef"], + }); + + const createReturnsMutation = useMutation({ + mutationFn: async (payload: { trucks: Array<{ bookingId: string; customerId: string | null; containers: Array<{ containerNumber: string; returnDate: string; facility: string; yard?: string; zone?: string; condition?: string; handoverNote?: string }> }> }) => { + const results = []; + for (const truck of payload.trucks) { + for (const container of truck.containers) { + const result = await importOperationsService.createEmptyReturn({ + containerNumber: container.containerNumber, + returnDate: new Date(container.returnDate).toISOString(), + bookingId: truck.bookingId, + customerId: truck.customerId ?? undefined, + facility: container.facility, + yard: container.yard, + zone: container.zone, + condition: container.condition, + handoverNote: container.handoverNote, + }); + results.push(result); + } + } + return results; + }, + onSuccess: () => { + toast({ title: "Empty container returns recorded" }); + qc.invalidateQueries({ queryKey: ["edr-last-mile-returns", bookingIds] }); + setReturnModalOpen(false); + setActiveKey(null); + }, + onError: (error: any) => { + toast({ + variant: "destructive", + title: "Failed to record returns", + description: error?.response?.data?.message || error?.message, + }); + }, + }); + + const activeTruck = activeKey ? trucksWithReturns.find(t => t.key === activeKey) ?? null : null; + + if (queueLoading || truckReturnsQuery.isLoading) { + return ( + + + + + + ); + } + + return ( + + + + {trucksWithReturns.length === 0 ? ( + No EDR trucks with return containers found. + ) : ( + <> + + + + + + Plate + Company + Booking Ref + Return Containers + Actions + + + + {controls.pagedRows.map((truck) => { + const isOpen = expanded === truck.key; + return ( + + + + setExpanded(isOpen ? null : truck.key)} + > + {isOpen ? : } + + + + {truck.plate} + + {truck.companyName ?? "—"} + {truck.bookingRef} + + {truck.containers.length} container{truck.containers.length !== 1 ? "s" : ""} + + + + + + {isOpen && ( + + +
+ + + + + + Container + Size + Type + + + + {truck.containers.map((container, idx) => ( + + + + + {container.containerNumber} + {container.size ?? "—"} + {container.type ?? "—"} + + ))} + +
+ + + )} + + ); + })} + + +
+ + + )} + + setReturnModalOpen(false)} + truck={activeTruck} + onSubmit={(payload) => createReturnsMutation.mutate(payload)} + loading={createReturnsMutation.isPending} + /> +
+ ); +} + +interface EmptyContainerReturnModalProps { + opened: boolean; + onClose: () => void; + truck: TruckReturn | null; + onSubmit: (payload: any) => void; + loading: boolean; +} + +function EmptyContainerReturnModal({ opened, onClose, truck, onSubmit, loading }: EmptyContainerReturnModalProps) { + const [selectedContainers, setSelectedContainers] = useState([]); + const [returnDate, setReturnDate] = useState(new Date().toISOString().split("T")[0]); + const [facility, setFacility] = useState(null); + const [yard, setYard] = useState(""); + const [condition, setCondition] = useState(""); + const [handoverNote, setHandoverNote] = useState(""); + + const handleSubmit = () => { + if (!truck || !selectedContainers.length || !facility) return; + + const containers = truck.containers + .filter((c) => selectedContainers.includes(c.containerNumber)) + .map((c) => ({ + containerNumber: c.containerNumber, + returnDate, + facility, + yard: yard || undefined, + zone: undefined, + condition: condition || undefined, + handoverNote: handoverNote || undefined, + })); + + onSubmit({ + trucks: [{ + bookingId: truck.bookingId, + customerId: truck.customerId, + containers, + }], + }); + }; + + return ( + + {truck && ( + + + {truck.plate} + {truck.bookingRef} + + +
+ Select containers to return: + + {truck.containers.map((container) => ( + { + if (e.currentTarget.checked) { + setSelectedContainers([...selectedContainers, container.containerNumber]); + } else { + setSelectedContainers(selectedContainers.filter(c => c !== container.containerNumber)); + } + }} + /> + ))} + +
+ +