feat(train-scheduling): load returned empties onto an export train

Empties had no way onto a departure: the return record could name a train
but nothing seated it on a wagon. Export schedules now expose a loading
action that packs selected returns onto free wagons at one 40ft or two 20ft
each, enforced both in the picker and in the API (existing empties on the
schedule count against their wagon).

Adds container_size, train_schedule_id and wagon_sequence_no to
freight.empty_container_returns.
This commit is contained in:
Hagernesh
2026-08-14 09:57:20 +00:00
parent 30c3567048
commit b4f2ec1c75
15 changed files with 630 additions and 2 deletions

View File

@@ -57,6 +57,7 @@ import { ContainerPlacementGrid } from "@/components/trainScheduling/ContainerPl
import { FleetAvailabilitySummary } from "@/components/trainScheduling/FleetAvailabilitySummary";
import { IntercityRideAlongPanel } from "@/components/trainScheduling/IntercityRideAlongPanel";
import { LegCapacityPanel } from "@/components/trainScheduling/LegCapacityPanel";
import { LoadEmptyContainersModal } from "@/components/trainScheduling/LoadEmptyContainersModal";
import MergeScheduleTrainModal from "@/components/trainScheduling/MergeScheduleTrainModal";
import ScheduleHistoryPanel from "@/components/trainScheduling/ScheduleHistoryPanel";
// import { ImportLoadingConfirmationPanel } from "@/components/trainScheduling/ImportLoadingConfirmationPanel";
@@ -126,6 +127,7 @@ export default function TrainScheduleV2DetailPage() {
const [dispatchConfirmOpen, setDispatchConfirmOpen] = useState(false);
const [switchTarget, setSwitchTarget] = useState<EligibleContainerBooking | null>(null);
const [visualization3DOpen, setVisualization3DOpen] = useState(false);
const [loadEmptiesOpen, setLoadEmptiesOpen] = useState(false);
const autoPreviewedRef = useRef(false);
const detailQuery = useQuery(
@@ -967,6 +969,20 @@ export default function TrainScheduleV2DetailPage() {
Merge
</Button>
) : null}
{/* Empties ride an export departure back to Djibouti — offered only
while the train can still take load. */}
{schedule.direction === "EXPORT" &&
["DRAFT", "SCHEDULED"].includes(schedule.status) ? (
<Button
variant="light"
color="edr-green"
size="compact-sm"
leftSection={<ContainerIcon size={14} />}
onClick={() => setLoadEmptiesOpen(true)}
>
Load Empty Container
</Button>
) : null}
{(schedule.trainSet?.wagons?.length ?? 0) > 0 ? (
<Button
variant="gradient"
@@ -1365,6 +1381,12 @@ export default function TrainScheduleV2DetailPage() {
onSaved={() => void detailQuery.refetch()}
/>
<LoadEmptyContainersModal
opened={loadEmptiesOpen}
onClose={() => setLoadEmptiesOpen(false)}
schedule={schedule}
/>
<MergeScheduleTrainModal
scheduleId={scheduleId ?? null}
currentTrainId={schedule.trainSet?.trainId ?? null}

View File

@@ -36,6 +36,7 @@ import { importOperationsService } from "@/services/importOperations.service";
import type {
EmptyContainerReturn,
EmptyContainerReturnStatus,
EmptyContainerSize,
} from "@/types/importOperations";
import type { TrainScheduleListItem } from "@/types/trainScheduling";
import { formatDateTime } from "@/lib/format";
@@ -267,6 +268,7 @@ export default function ContainerReturnsPage() {
returnType: "EDR" | "CUSTOMER";
containers: Array<{
containerNumber: string;
containerSize?: EmptyContainerSize;
returnDate: string;
warehouse: string;
condition?: string;
@@ -279,6 +281,7 @@ export default function ContainerReturnsPage() {
for (const container of truck.containers) {
const result = await importOperationsService.createEmptyReturn({
containerNumber: container.containerNumber,
containerSize: container.containerSize,
returnDate: new Date(container.returnDate).toISOString(),
bookingId: truck.bookingId,
customerId: truck.customerId ?? undefined,
@@ -844,6 +847,8 @@ function ContainerReturnModal({ opened, onClose, group, onSubmit, loading }: Con
.filter((c) => selectedContainers.includes(c.key))
.map((c) => ({
containerNumber: c.containerNumber,
// Inventory records "20ft"/"40ft"; the wagon rule only needs the number.
containerSize: c.size?.includes("40") ? ("40" as const) : c.size ? ("20" as const) : undefined,
returnDate,
warehouse: selectedWarehouse?.name || warehouse,
condition: condition || undefined,