mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
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:
@@ -0,0 +1,249 @@
|
||||
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<string[]>([]);
|
||||
const [sizeOverrides, setSizeOverrides] = useState<Record<string, EmptyContainerSize>>({});
|
||||
|
||||
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 (
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={onClose}
|
||||
title="Load Empty Containers"
|
||||
size="xl"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
One 40ft or two 20ft containers per wagon. {freeWagons.length} free wagon
|
||||
{freeWagons.length === 1 ? "" : "s"} on this train.
|
||||
</Text>
|
||||
|
||||
{loaded.length > 0 ? (
|
||||
<Alert color="gray" title={`${loaded.length} empty container(s) already loaded`}>
|
||||
<Group gap="xs">
|
||||
{loaded.map((ret) => (
|
||||
<Badge key={ret.id} size="sm" variant="light">
|
||||
{ret.containerNumber} · wagon {ret.wagonSequenceNo ?? "—"}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{returnsQuery.isLoading ? (
|
||||
<Group justify="center" py="md">
|
||||
<Loader size="sm" />
|
||||
</Group>
|
||||
) : available.length === 0 ? (
|
||||
<Alert color="gray">
|
||||
No returned empty containers are waiting — record returns in Container Returns.
|
||||
</Alert>
|
||||
) : (
|
||||
<Table.ScrollContainer minWidth={700}>
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w={40} />
|
||||
<Table.Th>Container</Table.Th>
|
||||
<Table.Th>Size</Table.Th>
|
||||
<Table.Th>Facility</Table.Th>
|
||||
<Table.Th>Returned</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{available.map((ret) => {
|
||||
const checked = selected.includes(ret.id);
|
||||
return (
|
||||
<Table.Tr key={ret.id}>
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
checked={checked}
|
||||
onChange={(event) =>
|
||||
setSelected(
|
||||
event.currentTarget.checked
|
||||
? [...selected, ret.id]
|
||||
: selected.filter((id) => id !== ret.id),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fw={600} size="sm">
|
||||
{ret.containerNumber}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{/* Legacy returns carry no size — the operator sets it here
|
||||
because the wagon rule cannot be applied without it. */}
|
||||
<SegmentedControl
|
||||
size="xs"
|
||||
value={sizeOf(ret)}
|
||||
onChange={(value) =>
|
||||
setSizeOverrides({
|
||||
...sizeOverrides,
|
||||
[ret.id]: value as EmptyContainerSize,
|
||||
})
|
||||
}
|
||||
data={[
|
||||
{ label: "20ft", value: "20" },
|
||||
{ label: "40ft", value: "40" },
|
||||
]}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>{ret.facility ?? "—"}</Table.Td>
|
||||
<Table.Td>
|
||||
{ret.returnDate ? new Date(ret.returnDate).toLocaleDateString() : "—"}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm" variant="light">
|
||||
{ret.status}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
|
||||
{unplaced.length > 0 ? (
|
||||
<Alert color="red">
|
||||
{needed} wagon(s) needed but only {freeWagons.length} free — unselect{" "}
|
||||
{unplaced.length} container(s) or add wagons to the consist.
|
||||
</Alert>
|
||||
) : picks.length > 0 ? (
|
||||
<Text size="sm">
|
||||
{picks.length} container(s) → wagons{" "}
|
||||
{[...new Set(assignments.map((a) => a.wagonSequenceNo))].join(", ")}
|
||||
</Text>
|
||||
) : null}
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={onClose} disabled={load.isPending}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!picks.length || unplaced.length > 0}
|
||||
loading={load.isPending}
|
||||
onClick={() => load.mutate()}
|
||||
>
|
||||
Load {picks.length || ""} on train
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default LoadEmptyContainersModal;
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { packEmptiesOntoWagons, wagonsNeeded, type EmptyLoadPick } from './emptyContainerLoad.util';
|
||||
|
||||
const pick = (id: string, containerSize: '20' | '40'): EmptyLoadPick => ({ id, containerSize });
|
||||
|
||||
describe('emptyContainerLoad.util', () => {
|
||||
it('gives each 40ft its own wagon', () => {
|
||||
const { assignments, unplaced } = packEmptiesOntoWagons(
|
||||
[pick('a', '40'), pick('b', '40')],
|
||||
[1, 2, 3],
|
||||
);
|
||||
expect(unplaced).toEqual([]);
|
||||
expect(assignments.map((a) => a.wagonSequenceNo)).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('pairs 20ft two to a wagon, last odd one alone', () => {
|
||||
const { assignments } = packEmptiesOntoWagons(
|
||||
[pick('a', '20'), pick('b', '20'), pick('c', '20')],
|
||||
[4, 5],
|
||||
);
|
||||
expect(assignments.map((a) => [a.id, a.wagonSequenceNo])).toEqual([
|
||||
['a', 4],
|
||||
['b', 4],
|
||||
['c', 5],
|
||||
]);
|
||||
});
|
||||
|
||||
it('never mixes a 40ft and a 20ft on one wagon', () => {
|
||||
const { assignments } = packEmptiesOntoWagons(
|
||||
[pick('a', '20'), pick('b', '40'), pick('c', '20')],
|
||||
[1, 2],
|
||||
);
|
||||
const bySizeOnWagon = new Map<number, string[]>();
|
||||
for (const a of assignments) {
|
||||
bySizeOnWagon.set(a.wagonSequenceNo, [
|
||||
...(bySizeOnWagon.get(a.wagonSequenceNo) ?? []),
|
||||
a.containerSize,
|
||||
]);
|
||||
}
|
||||
for (const sizes of bySizeOnWagon.values()) {
|
||||
expect(sizes.includes('40') ? sizes.length : 0).toBeLessThan(2);
|
||||
expect(sizes.length).toBeLessThanOrEqual(2);
|
||||
}
|
||||
});
|
||||
|
||||
it('reports picks that ran out of wagons instead of dropping them', () => {
|
||||
const { assignments, unplaced } = packEmptiesOntoWagons(
|
||||
[pick('a', '40'), pick('b', '40'), pick('c', '20'), pick('d', '20')],
|
||||
[7],
|
||||
);
|
||||
expect(assignments).toHaveLength(1);
|
||||
expect(unplaced.map((p) => p.id)).toEqual(['b', 'c', 'd']);
|
||||
});
|
||||
|
||||
it('counts wagons needed', () => {
|
||||
expect(wagonsNeeded([])).toBe(0);
|
||||
expect(wagonsNeeded([pick('a', '40'), pick('b', '20'), pick('c', '20')])).toBe(2);
|
||||
expect(wagonsNeeded([pick('a', '20')])).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { EmptyContainerSize } from "@/types/importOperations";
|
||||
|
||||
export interface EmptyLoadPick {
|
||||
id: string;
|
||||
containerSize: EmptyContainerSize;
|
||||
}
|
||||
|
||||
export interface EmptyLoadAssignment extends EmptyLoadPick {
|
||||
wagonSequenceNo: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill wagons with the picked empties: a wagon takes ONE 40ft or TWO 20ft,
|
||||
* never a mix. 40ft boxes are seated first so a half-filled 20ft wagon can
|
||||
* never block them, and the 20s pair up behind them.
|
||||
*
|
||||
* `freeWagons` is the caller's ordered list of wagon sequence numbers with no
|
||||
* cargo allocation. Returns the assignments that fit plus the picks that had
|
||||
* no wagon left — the caller surfaces the shortfall instead of silently
|
||||
* dropping boxes.
|
||||
*/
|
||||
export function packEmptiesOntoWagons(
|
||||
picks: EmptyLoadPick[],
|
||||
freeWagons: number[],
|
||||
): { assignments: EmptyLoadAssignment[]; unplaced: EmptyLoadPick[] } {
|
||||
const forty = picks.filter((pick) => pick.containerSize === "40");
|
||||
const twenty = picks.filter((pick) => pick.containerSize === "20");
|
||||
|
||||
const assignments: EmptyLoadAssignment[] = [];
|
||||
const unplaced: EmptyLoadPick[] = [];
|
||||
const wagons = [...freeWagons];
|
||||
|
||||
for (const pick of forty) {
|
||||
const wagon = wagons.shift();
|
||||
if (wagon == null) unplaced.push(pick);
|
||||
else assignments.push({ ...pick, wagonSequenceNo: wagon });
|
||||
}
|
||||
|
||||
for (let index = 0; index < twenty.length; index += 2) {
|
||||
const pair = twenty.slice(index, index + 2);
|
||||
const wagon = wagons.shift();
|
||||
if (wagon == null) unplaced.push(...pair);
|
||||
else assignments.push(...pair.map((pick) => ({ ...pick, wagonSequenceNo: wagon })));
|
||||
}
|
||||
|
||||
return { assignments, unplaced };
|
||||
}
|
||||
|
||||
/** Wagons the picks consume, whether or not enough are free. */
|
||||
export function wagonsNeeded(picks: EmptyLoadPick[]): number {
|
||||
const forty = picks.filter((pick) => pick.containerSize === "40").length;
|
||||
const twenty = picks.length - forty;
|
||||
return forty + Math.ceil(twenty / 2);
|
||||
}
|
||||
@@ -733,6 +733,8 @@ export const URL_CONSTANTS = {
|
||||
EMPTY_CONTAINER_RETURNS: "/import-operations/empty-container-returns",
|
||||
EMPTY_CONTAINER_RETURN_STATUS: (id: string) =>
|
||||
`/import-operations/empty-container-returns/${id}/status`,
|
||||
EMPTY_CONTAINER_RETURNS_LOAD_ON_TRAIN:
|
||||
"/import-operations/empty-container-returns/load-on-train",
|
||||
},
|
||||
|
||||
VEHICLES: {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
AssignCustomsRiskPayload,
|
||||
CreateDjiboutiIncidentPayload,
|
||||
CreateEmptyContainerReturnPayload,
|
||||
LoadEmptyContainersOnTrainPayload,
|
||||
DjiboutiIncident,
|
||||
EmptyContainerReturn,
|
||||
ImportCustomsFinalization,
|
||||
@@ -123,6 +124,16 @@ export const importOperationsService = {
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
loadEmptyContainersOnTrain: async (
|
||||
payload: LoadEmptyContainersOnTrainPayload,
|
||||
): Promise<EmptyContainerReturn[]> => {
|
||||
const response = await client.post<EmptyContainerReturn[]>(
|
||||
URL_CONSTANTS.IMPORT_OPERATIONS.EMPTY_CONTAINER_RETURNS_LOAD_ON_TRAIN,
|
||||
payload,
|
||||
);
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
updateEmptyReturnStatus: async (
|
||||
id: string,
|
||||
payload: UpdateEmptyContainerReturnStatusPayload,
|
||||
|
||||
@@ -101,6 +101,11 @@ export interface EmptyContainerReturn {
|
||||
handoverNote: string | null;
|
||||
status: EmptyContainerReturnStatus;
|
||||
wagonAllocationReference: string | null;
|
||||
/** '20' or '40' — one 40ft OR two 20ft ride a wagon. */
|
||||
containerSize: EmptyContainerSize | null;
|
||||
/** Export departure this empty is loaded on, once allocated. */
|
||||
trainScheduleId: string | null;
|
||||
wagonSequenceNo: number | null;
|
||||
performedBy: string | null;
|
||||
returnedBy: 'EDR' | 'CUSTOMER' | null;
|
||||
statusHistory: Array<{
|
||||
@@ -110,8 +115,11 @@ export interface EmptyContainerReturn {
|
||||
}>;
|
||||
}
|
||||
|
||||
export type EmptyContainerSize = '20' | '40';
|
||||
|
||||
export interface CreateEmptyContainerReturnPayload {
|
||||
containerNumber: string;
|
||||
containerSize?: EmptyContainerSize;
|
||||
bookingId?: string;
|
||||
customerId?: string;
|
||||
returnDate?: string;
|
||||
@@ -124,6 +132,16 @@ export interface CreateEmptyContainerReturnPayload {
|
||||
returnedBy?: 'EDR' | 'CUSTOMER';
|
||||
}
|
||||
|
||||
export interface LoadEmptyContainersOnTrainPayload extends ImportOperationActionPayload {
|
||||
trainScheduleId: string;
|
||||
trainNumber?: string;
|
||||
items: Array<{
|
||||
id: string;
|
||||
containerSize: EmptyContainerSize;
|
||||
wagonSequenceNo: number;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface UpdateEmptyContainerReturnStatusPayload extends ImportOperationActionPayload {
|
||||
status: EmptyContainerReturnStatus;
|
||||
wagonAllocationReference?: string;
|
||||
|
||||
Reference in New Issue
Block a user