import { useMemo, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Alert, Badge, Button, Checkbox, Group, Loader, Modal, SegmentedControl, Stack, Table, Text, } from "@mantine/core"; import { useToast } from "@/hooks/use-toast"; import { importOperationsService } from "@/services/importOperations.service"; import type { EmptyContainerReturn, EmptyContainerSize, } from "@/types/importOperations"; import type { TrainScheduleDetail } from "@/types/trainScheduling"; import { packEmptiesOntoWagons, wagonsNeeded } from "./emptyContainerLoad.util"; /** Empties still on the ground — past these the box has already left the yard. */ const LOADABLE_STATUSES = ["RETURNED", "ASSIGNED_STORAGE", "DOCUMENTATION_CLEARED"]; interface LoadEmptyContainersModalProps { opened: boolean; onClose: () => void; schedule: TrainScheduleDetail; } /** * Loads returned empty containers onto an export departure. Wagons are filled * one 40ft OR two 20ft each (see `packEmptiesOntoWagons`), drawing only on * wagons of this train that carry no cargo booking and no empty already. */ export function LoadEmptyContainersModal({ opened, onClose, schedule, }: LoadEmptyContainersModalProps) { const { toast } = useToast(); const qc = useQueryClient(); const [selected, setSelected] = useState([]); const [sizeOverrides, setSizeOverrides] = useState>({}); const returnsQuery = useQuery({ queryKey: ["empty-container-returns"], queryFn: () => importOperationsService.listEmptyReturns(), enabled: opened, }); const returns = returnsQuery.data ?? []; const loaded = useMemo( () => returns.filter((ret) => ret.trainScheduleId === schedule.id), [returns, schedule.id], ); const available = useMemo( () => returns.filter( (ret) => !ret.trainScheduleId && LOADABLE_STATUSES.includes(ret.status), ), [returns], ); const sizeOf = (ret: EmptyContainerReturn): EmptyContainerSize => sizeOverrides[ret.id] ?? (ret.containerSize === "20" ? "20" : "40"); // A wagon is up for grabs when no booking rides it and no empty sits on it. const freeWagons = useMemo(() => { const takenByEmpties = new Set( loaded.map((ret) => ret.wagonSequenceNo).filter((no): no is number => no != null), ); return (schedule.trainSet?.wagons ?? []) .filter((wagon) => !wagon.allocations?.length && !takenByEmpties.has(wagon.sequenceNo)) .map((wagon) => wagon.sequenceNo) .sort((a, b) => a - b); }, [schedule.trainSet?.wagons, loaded]); const picks = useMemo( () => available .filter((ret) => selected.includes(ret.id)) .map((ret) => ({ id: ret.id, containerSize: sizeOf(ret) })), // eslint-disable-next-line react-hooks/exhaustive-deps [available, selected, sizeOverrides], ); const needed = wagonsNeeded(picks); const { assignments, unplaced } = packEmptiesOntoWagons(picks, freeWagons); const load = useMutation({ mutationFn: () => importOperationsService.loadEmptyContainersOnTrain({ trainScheduleId: schedule.id, trainNumber: schedule.trainNumber ?? undefined, items: assignments, }), onSuccess: () => { toast({ title: `${assignments.length} empty container(s) loaded` }); qc.invalidateQueries({ queryKey: ["empty-container-returns"] }); qc.invalidateQueries({ queryKey: ["train-scheduling"] }); setSelected([]); onClose(); }, onError: (error: any) => { toast({ variant: "destructive", title: "Failed to load empty containers", description: error?.response?.data?.message || error?.message, }); }, }); return ( One 40ft or two 20ft containers per wagon. {freeWagons.length} free wagon {freeWagons.length === 1 ? "" : "s"} on this train. {loaded.length > 0 ? ( {loaded.map((ret) => ( {ret.containerNumber} · wagon {ret.wagonSequenceNo ?? "—"} ))} ) : null} {returnsQuery.isLoading ? ( ) : available.length === 0 ? ( No returned empty containers are waiting — record returns in Container Returns. ) : ( Container Size Facility Returned Status {available.map((ret) => { const checked = selected.includes(ret.id); return ( setSelected( event.currentTarget.checked ? [...selected, ret.id] : selected.filter((id) => id !== ret.id), ) } /> {ret.containerNumber} {/* Legacy returns carry no size — the operator sets it here because the wagon rule cannot be applied without it. */} setSizeOverrides({ ...sizeOverrides, [ret.id]: value as EmptyContainerSize, }) } data={[ { label: "20ft", value: "20" }, { label: "40ft", value: "40" }, ]} /> {ret.facility ?? "—"} {ret.returnDate ? new Date(ret.returnDate).toLocaleDateString() : "—"} {ret.status} ); })}
)} {unplaced.length > 0 ? ( {needed} wagon(s) needed but only {freeWagons.length} free — unselect{" "} {unplaced.length} container(s) or add wagons to the consist. ) : picks.length > 0 ? ( {picks.length} container(s) → wagons{" "} {[...new Set(assignments.map((a) => a.wagonSequenceNo))].join(", ")} ) : null}
); } export default LoadEmptyContainersModal;