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)); + } + }} + /> + ))} + +
+ +