mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 22:58:17 +00:00
Merge branch 'dev' into freight/nati-2
This commit is contained in:
@@ -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,7 +36,9 @@ 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";
|
||||
|
||||
type ReturnType = "all" | "edr" | "customer";
|
||||
@@ -100,6 +102,7 @@ export default function ContainerReturnsPage() {
|
||||
const [standaloneModalOpen, setStandaloneModalOpen] = useState(false);
|
||||
const [activeKey, setActiveKey] = useState<string | null>(null);
|
||||
const [historyRow, setHistoryRow] = useState<any | null>(null);
|
||||
const [allocateRow, setAllocateRow] = useState<EmptyContainerReturn | null>(null);
|
||||
|
||||
const { data: unloadedQueue = [], isLoading: queueLoading } = useQuery({
|
||||
queryKey: ["import-unloaded-queue"],
|
||||
@@ -265,6 +268,7 @@ export default function ContainerReturnsPage() {
|
||||
returnType: "EDR" | "CUSTOMER";
|
||||
containers: Array<{
|
||||
containerNumber: string;
|
||||
containerSize?: EmptyContainerSize;
|
||||
returnDate: string;
|
||||
warehouse: string;
|
||||
condition?: string;
|
||||
@@ -277,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,
|
||||
@@ -306,15 +311,25 @@ export default function ContainerReturnsPage() {
|
||||
});
|
||||
|
||||
const advanceStatusMutation = useMutation({
|
||||
mutationFn: (id: string) => {
|
||||
mutationFn: ({
|
||||
id,
|
||||
wagonAllocationReference,
|
||||
}: {
|
||||
id: string;
|
||||
wagonAllocationReference?: string;
|
||||
}) => {
|
||||
const current = returnedContainers.find((r: any) => r.id === id);
|
||||
const nextIndex = RETURN_STATUS_ORDER.indexOf(current?.status ?? "RETURNED") + 1;
|
||||
const status = RETURN_STATUS_ORDER[nextIndex] ?? "COMPLETED";
|
||||
return importOperationsService.updateEmptyReturnStatus(id, { status });
|
||||
return importOperationsService.updateEmptyReturnStatus(id, {
|
||||
status,
|
||||
wagonAllocationReference,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast({ title: "Return status updated" });
|
||||
qc.invalidateQueries({ queryKey: ["empty-container-returns"] });
|
||||
setAllocateRow(null);
|
||||
},
|
||||
onError: (error: any) => {
|
||||
toast({
|
||||
@@ -381,9 +396,16 @@ export default function ContainerReturnsPage() {
|
||||
id: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => (
|
||||
<Badge size="sm">
|
||||
{RETURN_STATUS_LABEL[row.original.status] ?? row.original.status}
|
||||
</Badge>
|
||||
<Stack gap={2}>
|
||||
<Badge size="sm">
|
||||
{RETURN_STATUS_LABEL[row.original.status] ?? row.original.status}
|
||||
</Badge>
|
||||
{row.original.wagonAllocationReference && (
|
||||
<Text size="xs" c="dimmed">
|
||||
Train {row.original.wagonAllocationReference}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -406,8 +428,15 @@ export default function ContainerReturnsPage() {
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
loading={advanceStatusMutation.isPending && advanceStatusMutation.variables === ret.id}
|
||||
onClick={() => advanceStatusMutation.mutate(ret.id)}
|
||||
loading={
|
||||
advanceStatusMutation.isPending &&
|
||||
advanceStatusMutation.variables?.id === ret.id
|
||||
}
|
||||
onClick={() =>
|
||||
nextStatus === "WAGON_ALLOCATED"
|
||||
? setAllocateRow(ret)
|
||||
: advanceStatusMutation.mutate({ id: ret.id })
|
||||
}
|
||||
>
|
||||
Advance to {RETURN_STATUS_LABEL[nextStatus]}
|
||||
</Button>
|
||||
@@ -629,6 +658,19 @@ export default function ContainerReturnsPage() {
|
||||
loading={createReturnsMutation.isPending}
|
||||
/>
|
||||
|
||||
<ExportTrainAllocationModal
|
||||
row={allocateRow}
|
||||
onClose={() => setAllocateRow(null)}
|
||||
onSubmit={(reference) =>
|
||||
allocateRow &&
|
||||
advanceStatusMutation.mutate({
|
||||
id: allocateRow.id,
|
||||
wagonAllocationReference: reference,
|
||||
})
|
||||
}
|
||||
loading={advanceStatusMutation.isPending}
|
||||
/>
|
||||
|
||||
<Modal opened={!!historyRow} onClose={() => setHistoryRow(null)} title="Status History" size="sm">
|
||||
{historyRow && (
|
||||
<Stack gap="sm">
|
||||
@@ -649,6 +691,123 @@ export default function ContainerReturnsPage() {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties ride an EXPORT departure back to Djibouti, so wagon allocation picks
|
||||
* from the export schedules that have not left yet (DRAFT/SCHEDULED). The pick
|
||||
* is recorded as the return's `wagonAllocationReference`.
|
||||
*/
|
||||
function ExportTrainAllocationModal({
|
||||
row,
|
||||
onClose,
|
||||
onSubmit,
|
||||
loading,
|
||||
}: {
|
||||
row: EmptyContainerReturn | null;
|
||||
onClose: () => void;
|
||||
onSubmit: (reference: string) => void;
|
||||
loading: boolean;
|
||||
}) {
|
||||
const [scheduleId, setScheduleId] = useState<string | null>(null);
|
||||
|
||||
const schedulesQuery = useQuery(
|
||||
api.trainScheduling.scheduleList.queryOptions({
|
||||
input: { filters: { pageSize: 100, sortBy: "scheduledDepartureDate", sortOrder: "ASC" } },
|
||||
enabled: Boolean(row),
|
||||
}),
|
||||
);
|
||||
|
||||
const exportTrains = useMemo(
|
||||
() =>
|
||||
((schedulesQuery.data?.items ?? []) as TrainScheduleListItem[]).filter(
|
||||
(s) => s.direction === "EXPORT" && (s.status === "DRAFT" || s.status === "SCHEDULED"),
|
||||
),
|
||||
[schedulesQuery.data],
|
||||
);
|
||||
|
||||
const referenceOf = (train: TrainScheduleListItem) =>
|
||||
train.trainNumber || train.reference || train.id;
|
||||
|
||||
return (
|
||||
<Modal opened={!!row} onClose={onClose} title="Allocate to Export Train" size="lg">
|
||||
{row && (
|
||||
<Stack gap="md">
|
||||
<Text fw={600}>{row.containerNumber}</Text>
|
||||
|
||||
{schedulesQuery.isLoading ? (
|
||||
<Group justify="center" py="md">
|
||||
<Loader size="sm" />
|
||||
</Group>
|
||||
) : exportTrains.length === 0 ? (
|
||||
<Alert color="gray">No export train is scheduled — create one in Train Scheduling.</Alert>
|
||||
) : (
|
||||
<Table highlightOnHover>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th w={40} />
|
||||
<Table.Th>Train</Table.Th>
|
||||
<Table.Th>Departure</Table.Th>
|
||||
<Table.Th>Route</Table.Th>
|
||||
<Table.Th>Wagons</Table.Th>
|
||||
<Table.Th>Status</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{exportTrains.map((train) => (
|
||||
<Table.Tr
|
||||
key={train.id}
|
||||
onClick={() => setScheduleId(train.id)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<Table.Td>
|
||||
<Checkbox
|
||||
checked={scheduleId === train.id}
|
||||
onChange={() => setScheduleId(train.id)}
|
||||
/>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fw={600} size="sm">
|
||||
{referenceOf(train)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{train.scheduleDate ? new Date(train.scheduleDate).toLocaleDateString() : "—"}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{train.origin ?? "—"} → {train.destination ?? "—"}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{train.wagonsUsed ?? 0}/{train.wagonCount}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Badge size="sm">{train.status}</Badge>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
)}
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button variant="default" onClick={onClose} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!scheduleId}
|
||||
loading={loading}
|
||||
onClick={() => {
|
||||
const train = exportTrains.find((t) => t.id === scheduleId);
|
||||
if (train) onSubmit(referenceOf(train));
|
||||
}}
|
||||
>
|
||||
Allocate
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
interface ContainerReturnModalProps {
|
||||
opened: boolean;
|
||||
onClose: () => void;
|
||||
@@ -688,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,
|
||||
|
||||
Reference in New Issue
Block a user