mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
add loading confirmation for import-Djibouti trains before dispatch
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Group,
|
||||
List,
|
||||
Loader,
|
||||
Modal,
|
||||
Paper,
|
||||
RingProgress,
|
||||
Stack,
|
||||
@@ -15,6 +18,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { isAxiosError } from "axios";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
CalendarClock,
|
||||
CheckCircle2,
|
||||
@@ -44,7 +48,7 @@ import {
|
||||
} from "@/components/trainScheduling/containerPlacement.util";
|
||||
import { ContainerPlacementGrid } from "@/components/trainScheduling/ContainerPlacementGrid";
|
||||
import { FleetAvailabilitySummary } from "@/components/trainScheduling/FleetAvailabilitySummary";
|
||||
import { ImportLoadingConfirmationPanel } from "@/components/trainScheduling/ImportLoadingConfirmationPanel";
|
||||
// import { ImportLoadingConfirmationPanel } from "@/components/trainScheduling/ImportLoadingConfirmationPanel";
|
||||
import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog";
|
||||
import BookingWindowSettingsModal from "@/components/trainScheduling/BookingWindowSettingsModal";
|
||||
// import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
|
||||
@@ -100,6 +104,7 @@ export default function TrainScheduleV2DetailPage() {
|
||||
const [gatepassReference, setGatepassReference] = useState("");
|
||||
const [gatepassFileUrl, setGatepassFileUrl] = useState("");
|
||||
const [gatepassNotes, setGatepassNotes] = useState("");
|
||||
const [dispatchConfirmOpen, setDispatchConfirmOpen] = useState(false);
|
||||
const autoPreviewedRef = useRef(false);
|
||||
|
||||
const detailQuery = useQuery(
|
||||
@@ -148,12 +153,14 @@ export default function TrainScheduleV2DetailPage() {
|
||||
},
|
||||
});
|
||||
|
||||
const importLoadingQuery = useQuery(
|
||||
api.trainScheduling.importLoadingBookings.queryOptions({
|
||||
input: { id: scheduleId ?? "" },
|
||||
enabled: Boolean(scheduleId && schedule?.direction === "IMPORT"),
|
||||
}),
|
||||
);
|
||||
// Superseded by the Workspace tab Load/Unload toggle — see commented
|
||||
// "Import loading confirmation" card below.
|
||||
// const importLoadingQuery = useQuery(
|
||||
// api.trainScheduling.importLoadingBookings.queryOptions({
|
||||
// input: { id: scheduleId ?? "" },
|
||||
// enabled: Boolean(scheduleId && schedule?.direction === "IMPORT"),
|
||||
// }),
|
||||
// );
|
||||
|
||||
const eligibleFilters = useMemo(
|
||||
() =>
|
||||
@@ -360,6 +367,22 @@ export default function TrainScheduleV2DetailPage() {
|
||||
const canEditBookings = ["DRAFT", "SCHEDULED"].includes(schedule.status);
|
||||
const canFinalize = schedule.status === "DRAFT" && (schedule.bookings?.length ?? 0) > 0;
|
||||
const canDispatch = schedule.status === "SCHEDULED";
|
||||
|
||||
// Dispatch readiness: bookings with no wagon, and wagon-loaded bookings whose
|
||||
// cargo staff never marked loaded. Both are warnings, not blockers — staff can
|
||||
// still dispatch after confirming.
|
||||
const dispatchBookings = schedule.bookings ?? [];
|
||||
const unassignedCount = dispatchBookings.filter((b) => !b.wagonAssigned).length;
|
||||
const unloadedCount = dispatchBookings.filter(
|
||||
(b) => b.wagonAssigned && (b.loadingStatus ?? "UNLOADED") !== "LOADED",
|
||||
).length;
|
||||
// Import-Djibouti trains are HARD-blocked from dispatch until loading is
|
||||
// confirmed in the workspace — surface it as a blocker, not just a warning.
|
||||
const loadingBlocksDispatch =
|
||||
schedule.requiresLoadingConfirmation === true &&
|
||||
schedule.loadingConfirmed !== true;
|
||||
const hasDispatchWarnings = unassignedCount > 0 || unloadedCount > 0;
|
||||
|
||||
const finalizeStep = hasContainerStep ? 3 : 2;
|
||||
const canModifyBookings = canEditBookings && !["DISPATCHED", "ARRIVED"].includes(schedule.status);
|
||||
const canPrintMarshalling =
|
||||
@@ -398,6 +421,24 @@ export default function TrainScheduleV2DetailPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const runDispatch = async () => {
|
||||
setDispatchConfirmOpen(false);
|
||||
try {
|
||||
await dispatch.mutateAsync(scheduleId);
|
||||
await openMarshallingDocument({
|
||||
title: "Train dispatched",
|
||||
successDescription: "Marshalling document generated for the dispatched train.",
|
||||
errorTitle: "Train dispatched, but document could not open",
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Dispatch failed",
|
||||
description: parseError(err, "Could not dispatch"),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleAssign = async () => {
|
||||
if (!allSelectedIds.length) return;
|
||||
|
||||
@@ -779,22 +820,7 @@ export default function TrainScheduleV2DetailPage() {
|
||||
radius="md"
|
||||
leftSection={<Send size={18} />}
|
||||
loading={dispatch.isPending}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await dispatch.mutateAsync(scheduleId);
|
||||
await openMarshallingDocument({
|
||||
title: "Train dispatched",
|
||||
successDescription: "Marshalling document generated for the dispatched train.",
|
||||
errorTitle: "Train dispatched, but document could not open",
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
title: "Dispatch failed",
|
||||
description: parseError(err, "Could not dispatch"),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}}
|
||||
onClick={() => setDispatchConfirmOpen(true)}
|
||||
>
|
||||
Dispatch train
|
||||
</Button>
|
||||
@@ -1002,6 +1028,9 @@ export default function TrainScheduleV2DetailPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Import loading confirmation — superseded by the per-booking Load/Unload
|
||||
toggle in the Workspace tab (works for all directions). Kept commented
|
||||
in case the import-only confirmation flow is needed again.
|
||||
{schedule?.direction === "IMPORT" ? (
|
||||
<Paper radius="xl" p="lg" withBorder>
|
||||
<Stack gap="md">
|
||||
@@ -1018,6 +1047,7 @@ export default function TrainScheduleV2DetailPage() {
|
||||
</Stack>
|
||||
</Paper>
|
||||
) : null}
|
||||
*/}
|
||||
|
||||
<Tabs defaultValue="workflow" radius="md" color="edr-green" keepMounted={false}>
|
||||
<Tabs.List mb="md">
|
||||
@@ -1119,6 +1149,102 @@ export default function TrainScheduleV2DetailPage() {
|
||||
onClose={() => setWindowSettingsOpen(false)}
|
||||
onSaved={() => void detailQuery.refetch()}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
opened={dispatchConfirmOpen}
|
||||
onClose={() => setDispatchConfirmOpen(false)}
|
||||
centered
|
||||
radius="lg"
|
||||
title={
|
||||
<Group gap={8}>
|
||||
<Send size={18} />
|
||||
<Text fw={700}>Dispatch this train?</Text>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
Dispatch locks the composition and begins rail movement. This cannot be
|
||||
undone.
|
||||
</Text>
|
||||
|
||||
{loadingBlocksDispatch ? (
|
||||
<Alert
|
||||
color="red"
|
||||
variant="light"
|
||||
radius="md"
|
||||
icon={<AlertTriangle size={18} />}
|
||||
title="Loading not confirmed"
|
||||
>
|
||||
This import train cannot depart until loading is confirmed. Use{" "}
|
||||
<Text span fw={700}>
|
||||
Confirm loading
|
||||
</Text>{" "}
|
||||
in the Workspace tab first.
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{hasDispatchWarnings ? (
|
||||
<Alert
|
||||
color="orange"
|
||||
variant="light"
|
||||
radius="md"
|
||||
icon={<AlertTriangle size={18} />}
|
||||
title="Some bookings are not fully ready"
|
||||
>
|
||||
<List size="sm" spacing={4}>
|
||||
{unassignedCount > 0 ? (
|
||||
<List.Item>
|
||||
<Text span fw={700}>
|
||||
{unassignedCount}
|
||||
</Text>{" "}
|
||||
booking{unassignedCount === 1 ? "" : "s"} not assigned to a wagon
|
||||
</List.Item>
|
||||
) : null}
|
||||
{unloadedCount > 0 ? (
|
||||
<List.Item>
|
||||
<Text span fw={700}>
|
||||
{unloadedCount}
|
||||
</Text>{" "}
|
||||
wagon-assigned booking{unloadedCount === 1 ? "" : "s"} still marked
|
||||
unloaded
|
||||
</List.Item>
|
||||
) : null}
|
||||
</List>
|
||||
<Text size="xs" c="dimmed" mt={6}>
|
||||
You can still dispatch — confirm to proceed.
|
||||
</Text>
|
||||
</Alert>
|
||||
) : (
|
||||
<Alert
|
||||
color="edr-green"
|
||||
variant="light"
|
||||
radius="md"
|
||||
icon={<CheckCircle2 size={18} />}
|
||||
>
|
||||
All bookings are assigned to a wagon and marked loaded.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setDispatchConfirmOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="edr-green"
|
||||
leftSection={<Send size={16} />}
|
||||
loading={dispatch.isPending}
|
||||
disabled={loadingBlocksDispatch}
|
||||
onClick={() => void runDispatch()}
|
||||
>
|
||||
{hasDispatchWarnings ? "Dispatch anyway" : "Dispatch train"}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user