mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
add loading confirmation for import-Djibouti trains before dispatch
This commit is contained in:
@@ -453,6 +453,19 @@ export class TrainSchedulingController {
|
|||||||
return this.trainSchedulingService.confirmImportLoadedOnTrain(id, dto);
|
return this.trainSchedulingService.confirmImportLoadedOnTrain(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("schedules/:id/confirm-loading")
|
||||||
|
@TrainSchedulingManage()
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
"Confirm cargo loaded on the train (any direction; unblocks import-Djibouti dispatch)",
|
||||||
|
})
|
||||||
|
confirmScheduleLoading(
|
||||||
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
|
@Body() dto: ImportDjiboutiActionDto,
|
||||||
|
) {
|
||||||
|
return this.trainSchedulingService.confirmScheduleLoading(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
@Post("schedules/:id/import-djibouti/depart")
|
@Post("schedules/:id/import-djibouti/depart")
|
||||||
@TrainSchedulingManage()
|
@TrainSchedulingManage()
|
||||||
@ApiOperation({ summary: "Depart loaded import train from Djibouti" })
|
@ApiOperation({ summary: "Depart loaded import train from Djibouti" })
|
||||||
|
|||||||
@@ -1515,6 +1515,24 @@ export class TrainSchedulingService {
|
|||||||
return this.getImportDjiboutiOperation(schedule.id);
|
return this.getImportDjiboutiOperation(schedule.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm cargo is loaded on the train from the workspace, for any direction.
|
||||||
|
* For import-from-Djibouti trains this stamps the ImportDjiboutiOperation's
|
||||||
|
* loadedOnTrainAt (the flag dispatch checks) — gatepass must already be granted.
|
||||||
|
* For every other schedule there is no departure loading gate, so this is a
|
||||||
|
* success no-op and simply returns the current detail.
|
||||||
|
*/
|
||||||
|
async confirmScheduleLoading(scheduleId: string, dto: ImportDjiboutiActionDto = {}) {
|
||||||
|
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||||
|
if (!schedule) {
|
||||||
|
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||||
|
}
|
||||||
|
if (this.isImportDjiboutiSchedule(schedule)) {
|
||||||
|
await this.confirmImportLoadedOnTrain(scheduleId, dto);
|
||||||
|
}
|
||||||
|
return this.getTrainScheduleById(scheduleId);
|
||||||
|
}
|
||||||
|
|
||||||
async departImportFromDjibouti(scheduleId: string, dto: ImportDjiboutiActionDto = {}) {
|
async departImportFromDjibouti(scheduleId: string, dto: ImportDjiboutiActionDto = {}) {
|
||||||
const schedule = await this.getImportDjiboutiSchedule(scheduleId);
|
const schedule = await this.getImportDjiboutiSchedule(scheduleId);
|
||||||
const operation = await this.getOrCreateImportDjiboutiOperation(scheduleId);
|
const operation = await this.getOrCreateImportDjiboutiOperation(scheduleId);
|
||||||
@@ -3931,6 +3949,18 @@ export class TrainSchedulingService {
|
|||||||
const allocationIds = allocations.map((a) => a.id);
|
const allocationIds = allocations.map((a) => a.id);
|
||||||
const allocatedBookingIds = new Set(allocations.map((a) => a.bookingId));
|
const allocatedBookingIds = new Set(allocations.map((a) => a.bookingId));
|
||||||
|
|
||||||
|
// Import-from-Djibouti trains can only dispatch once loading is confirmed
|
||||||
|
// (loadedOnTrainAt on the operation). Other directions have no departure
|
||||||
|
// loading gate, so the workspace shows the confirm button as already done.
|
||||||
|
const requiresLoadingConfirmation = this.isImportDjiboutiSchedule(schedule);
|
||||||
|
let loadingConfirmed = !requiresLoadingConfirmation;
|
||||||
|
if (requiresLoadingConfirmation) {
|
||||||
|
const op = await this.dataSource
|
||||||
|
.getRepository(ImportDjiboutiOperation)
|
||||||
|
.findOne({ where: { trainScheduleId: schedule.id } });
|
||||||
|
loadingConfirmed = Boolean(op?.loadedOnTrainAt);
|
||||||
|
}
|
||||||
|
|
||||||
const windowCfg = await this.getWindowConfig();
|
const windowCfg = await this.getWindowConfig();
|
||||||
|
|
||||||
const [containerItems, bulkLoads] = await Promise.all([
|
const [containerItems, bulkLoads] = await Promise.all([
|
||||||
@@ -3964,6 +3994,8 @@ export class TrainSchedulingService {
|
|||||||
freightType: this.resolveScheduleFreightType(schedule),
|
freightType: this.resolveScheduleFreightType(schedule),
|
||||||
trainNumber: schedule.trainNumber ?? null,
|
trainNumber: schedule.trainNumber ?? null,
|
||||||
direction: schedule.direction ?? null,
|
direction: schedule.direction ?? null,
|
||||||
|
requiresLoadingConfirmation,
|
||||||
|
loadingConfirmed,
|
||||||
// Booking-window phase + phase deadlines drive the countdown timers in the
|
// Booking-window phase + phase deadlines drive the countdown timers in the
|
||||||
// operations workspace (display only — the window engine enforces them).
|
// operations workspace (display only — the window engine enforces them).
|
||||||
windowPhase: schedule.windowPhase ?? null,
|
windowPhase: schedule.windowPhase ?? null,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import {
|
|||||||
Inbox,
|
Inbox,
|
||||||
PackageCheck,
|
PackageCheck,
|
||||||
PackageX,
|
PackageX,
|
||||||
Repeat,
|
// Repeat, // used by the hidden Move (reassign) button
|
||||||
Train,
|
Train,
|
||||||
Weight,
|
Weight,
|
||||||
X,
|
X,
|
||||||
@@ -156,6 +156,9 @@ export function ScheduleWorkspacePanel({
|
|||||||
const setLoading = useMutation(
|
const setLoading = useMutation(
|
||||||
api.trainScheduling.setLoadingStatus.mutationOptions(),
|
api.trainScheduling.setLoadingStatus.mutationOptions(),
|
||||||
);
|
);
|
||||||
|
const confirmLoading = useMutation(
|
||||||
|
api.trainScheduling.confirmLoading.mutationOptions(),
|
||||||
|
);
|
||||||
const moveSchedule = useMutation(
|
const moveSchedule = useMutation(
|
||||||
api.trainScheduling.moveBookingSchedule.mutationOptions(),
|
api.trainScheduling.moveBookingSchedule.mutationOptions(),
|
||||||
);
|
);
|
||||||
@@ -266,6 +269,25 @@ export function ScheduleWorkspacePanel({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const doConfirmLoading = () => {
|
||||||
|
confirmLoading
|
||||||
|
.mutateAsync({ id: schedule.id })
|
||||||
|
.then(() => {
|
||||||
|
toast({ title: "Loading confirmed", description: "The train is cleared to dispatch." });
|
||||||
|
onChanged();
|
||||||
|
})
|
||||||
|
.catch((error) =>
|
||||||
|
toast({
|
||||||
|
title: "Could not confirm loading",
|
||||||
|
description: apiErrorMessage(
|
||||||
|
error,
|
||||||
|
"Grant the Djibouti gatepass first, then confirm loading.",
|
||||||
|
),
|
||||||
|
variant: "destructive",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const doMove = () => {
|
const doMove = () => {
|
||||||
if (!moveBookingId || !moveTarget) return;
|
if (!moveBookingId || !moveTarget) return;
|
||||||
moveSchedule
|
moveSchedule
|
||||||
@@ -376,6 +398,54 @@ export function ScheduleWorkspacePanel({
|
|||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{/* Loading confirmation — required before dispatch for import-Djibouti
|
||||||
|
trains; shown for every direction so staff have one place to confirm. */}
|
||||||
|
{canManage ? (
|
||||||
|
<Group
|
||||||
|
gap={10}
|
||||||
|
p="sm"
|
||||||
|
wrap="nowrap"
|
||||||
|
align="center"
|
||||||
|
justify="space-between"
|
||||||
|
style={{
|
||||||
|
borderRadius: 10,
|
||||||
|
background: schedule.loadingConfirmed
|
||||||
|
? "var(--mantine-color-edr-green-0)"
|
||||||
|
: "var(--mantine-color-yellow-0)",
|
||||||
|
border: `1px solid ${
|
||||||
|
schedule.loadingConfirmed
|
||||||
|
? "var(--mantine-color-edr-green-2)"
|
||||||
|
: "var(--mantine-color-yellow-3)"
|
||||||
|
}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap={8} wrap="nowrap" align="center" style={{ minWidth: 0 }}>
|
||||||
|
{schedule.loadingConfirmed ? (
|
||||||
|
<CheckCircle2 size={18} color="var(--mantine-color-edr-green-7)" />
|
||||||
|
) : (
|
||||||
|
<PackageCheck size={18} color="#B7791F" />
|
||||||
|
)}
|
||||||
|
<Text size="sm" fw={600}>
|
||||||
|
{schedule.loadingConfirmed
|
||||||
|
? "Loading confirmed — cleared to dispatch"
|
||||||
|
: "Confirm loading before dispatching this train"}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
{!schedule.loadingConfirmed ? (
|
||||||
|
<Button
|
||||||
|
size="compact-sm"
|
||||||
|
color="edr-green"
|
||||||
|
radius="md"
|
||||||
|
leftSection={<CheckCircle2 size={14} />}
|
||||||
|
loading={confirmLoading.isPending}
|
||||||
|
onClick={doConfirmLoading}
|
||||||
|
>
|
||||||
|
Confirm loading
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</Group>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Two-panel board */}
|
{/* Two-panel board */}
|
||||||
<Group align="stretch" gap="lg" grow wrap="wrap">
|
<Group align="stretch" gap="lg" grow wrap="wrap">
|
||||||
{/* Pool */}
|
{/* Pool */}
|
||||||
@@ -477,6 +547,7 @@ export function ScheduleWorkspacePanel({
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : null}
|
) : null}
|
||||||
|
{/* Reassign-to-another-train — hidden for now.
|
||||||
<Tooltip label="Reassign to another train" withArrow>
|
<Tooltip label="Reassign to another train" withArrow>
|
||||||
<Button
|
<Button
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
@@ -492,6 +563,7 @@ export function ScheduleWorkspacePanel({
|
|||||||
Move
|
Move
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
*/}
|
||||||
<Tooltip label="Remove from this train" withArrow>
|
<Tooltip label="Remove from this train" withArrow>
|
||||||
<Button
|
<Button
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
|
|||||||
@@ -330,6 +330,8 @@ export const URL_CONSTANTS = {
|
|||||||
`/train-scheduling/schedules/${id}/import-loading-status`,
|
`/train-scheduling/schedules/${id}/import-loading-status`,
|
||||||
LOADING_STATUS: (id: string) =>
|
LOADING_STATUS: (id: string) =>
|
||||||
`/train-scheduling/schedules/${id}/loading-status`,
|
`/train-scheduling/schedules/${id}/loading-status`,
|
||||||
|
CONFIRM_LOADING: (id: string) =>
|
||||||
|
`/train-scheduling/schedules/${id}/confirm-loading`,
|
||||||
IMPORT_DJIBOUTI: (id: string) =>
|
IMPORT_DJIBOUTI: (id: string) =>
|
||||||
`/train-scheduling/schedules/${id}/import-djibouti`,
|
`/train-scheduling/schedules/${id}/import-djibouti`,
|
||||||
IMPORT_DJIBOUTI_DOCUMENTS: (id: string) =>
|
IMPORT_DJIBOUTI_DOCUMENTS: (id: string) =>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Group,
|
Group,
|
||||||
|
List,
|
||||||
Loader,
|
Loader,
|
||||||
|
Modal,
|
||||||
Paper,
|
Paper,
|
||||||
RingProgress,
|
RingProgress,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -15,6 +18,7 @@ import {
|
|||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { isAxiosError } from "axios";
|
import { isAxiosError } from "axios";
|
||||||
import {
|
import {
|
||||||
|
AlertTriangle,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
CalendarClock,
|
CalendarClock,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@@ -44,7 +48,7 @@ import {
|
|||||||
} from "@/components/trainScheduling/containerPlacement.util";
|
} from "@/components/trainScheduling/containerPlacement.util";
|
||||||
import { ContainerPlacementGrid } from "@/components/trainScheduling/ContainerPlacementGrid";
|
import { ContainerPlacementGrid } from "@/components/trainScheduling/ContainerPlacementGrid";
|
||||||
import { FleetAvailabilitySummary } from "@/components/trainScheduling/FleetAvailabilitySummary";
|
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 { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog";
|
||||||
import BookingWindowSettingsModal from "@/components/trainScheduling/BookingWindowSettingsModal";
|
import BookingWindowSettingsModal from "@/components/trainScheduling/BookingWindowSettingsModal";
|
||||||
// import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
|
// import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
|
||||||
@@ -100,6 +104,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
const [gatepassReference, setGatepassReference] = useState("");
|
const [gatepassReference, setGatepassReference] = useState("");
|
||||||
const [gatepassFileUrl, setGatepassFileUrl] = useState("");
|
const [gatepassFileUrl, setGatepassFileUrl] = useState("");
|
||||||
const [gatepassNotes, setGatepassNotes] = useState("");
|
const [gatepassNotes, setGatepassNotes] = useState("");
|
||||||
|
const [dispatchConfirmOpen, setDispatchConfirmOpen] = useState(false);
|
||||||
const autoPreviewedRef = useRef(false);
|
const autoPreviewedRef = useRef(false);
|
||||||
|
|
||||||
const detailQuery = useQuery(
|
const detailQuery = useQuery(
|
||||||
@@ -148,12 +153,14 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const importLoadingQuery = useQuery(
|
// Superseded by the Workspace tab Load/Unload toggle — see commented
|
||||||
api.trainScheduling.importLoadingBookings.queryOptions({
|
// "Import loading confirmation" card below.
|
||||||
input: { id: scheduleId ?? "" },
|
// const importLoadingQuery = useQuery(
|
||||||
enabled: Boolean(scheduleId && schedule?.direction === "IMPORT"),
|
// api.trainScheduling.importLoadingBookings.queryOptions({
|
||||||
}),
|
// input: { id: scheduleId ?? "" },
|
||||||
);
|
// enabled: Boolean(scheduleId && schedule?.direction === "IMPORT"),
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
|
||||||
const eligibleFilters = useMemo(
|
const eligibleFilters = useMemo(
|
||||||
() =>
|
() =>
|
||||||
@@ -360,6 +367,22 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
const canEditBookings = ["DRAFT", "SCHEDULED"].includes(schedule.status);
|
const canEditBookings = ["DRAFT", "SCHEDULED"].includes(schedule.status);
|
||||||
const canFinalize = schedule.status === "DRAFT" && (schedule.bookings?.length ?? 0) > 0;
|
const canFinalize = schedule.status === "DRAFT" && (schedule.bookings?.length ?? 0) > 0;
|
||||||
const canDispatch = schedule.status === "SCHEDULED";
|
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 finalizeStep = hasContainerStep ? 3 : 2;
|
||||||
const canModifyBookings = canEditBookings && !["DISPATCHED", "ARRIVED"].includes(schedule.status);
|
const canModifyBookings = canEditBookings && !["DISPATCHED", "ARRIVED"].includes(schedule.status);
|
||||||
const canPrintMarshalling =
|
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 () => {
|
const handleAssign = async () => {
|
||||||
if (!allSelectedIds.length) return;
|
if (!allSelectedIds.length) return;
|
||||||
|
|
||||||
@@ -779,22 +820,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
radius="md"
|
radius="md"
|
||||||
leftSection={<Send size={18} />}
|
leftSection={<Send size={18} />}
|
||||||
loading={dispatch.isPending}
|
loading={dispatch.isPending}
|
||||||
onClick={async () => {
|
onClick={() => setDispatchConfirmOpen(true)}
|
||||||
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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Dispatch train
|
Dispatch train
|
||||||
</Button>
|
</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" ? (
|
{schedule?.direction === "IMPORT" ? (
|
||||||
<Paper radius="xl" p="lg" withBorder>
|
<Paper radius="xl" p="lg" withBorder>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
@@ -1018,6 +1047,7 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
) : null}
|
) : null}
|
||||||
|
*/}
|
||||||
|
|
||||||
<Tabs defaultValue="workflow" radius="md" color="edr-green" keepMounted={false}>
|
<Tabs defaultValue="workflow" radius="md" color="edr-green" keepMounted={false}>
|
||||||
<Tabs.List mb="md">
|
<Tabs.List mb="md">
|
||||||
@@ -1119,6 +1149,102 @@ export default function TrainScheduleV2DetailPage() {
|
|||||||
onClose={() => setWindowSettingsOpen(false)}
|
onClose={() => setWindowSettingsOpen(false)}
|
||||||
onSaved={() => void detailQuery.refetch()}
|
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>
|
</PageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -558,6 +558,14 @@ export const api = {
|
|||||||
() => TRAIN_SCHEDULING_INVALIDATIONS,
|
() => TRAIN_SCHEDULING_INVALIDATIONS,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
confirmLoading: endpoint<{ id: string }, TrainScheduleDetail>(
|
||||||
|
"train-scheduling",
|
||||||
|
"confirm-loading",
|
||||||
|
({ id }) => trainSchedulingService.confirmLoading(id),
|
||||||
|
undefined,
|
||||||
|
() => TRAIN_SCHEDULING_INVALIDATIONS,
|
||||||
|
),
|
||||||
|
|
||||||
pinWagons: endpoint<
|
pinWagons: endpoint<
|
||||||
{ id: string; payload: PinWagonsPayload },
|
{ id: string; payload: PinWagonsPayload },
|
||||||
TrainScheduleDetail
|
TrainScheduleDetail
|
||||||
|
|||||||
@@ -369,6 +369,16 @@ export const trainSchedulingService = {
|
|||||||
return unwrap(response.data);
|
return unwrap(response.data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
confirmLoading: async (
|
||||||
|
scheduleId: string,
|
||||||
|
): Promise<TrainScheduleDetail> => {
|
||||||
|
const response = await client.post<TrainScheduleDetail>(
|
||||||
|
URL_CONSTANTS.TRAIN_SCHEDULING.CONFIRM_LOADING(scheduleId),
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
return unwrap(response.data);
|
||||||
|
},
|
||||||
|
|
||||||
getImportDjiboutiOperation: async (
|
getImportDjiboutiOperation: async (
|
||||||
scheduleId: string,
|
scheduleId: string,
|
||||||
): Promise<ImportDjiboutiOperation> => {
|
): Promise<ImportDjiboutiOperation> => {
|
||||||
|
|||||||
@@ -429,6 +429,10 @@ export interface TrainScheduleDetail {
|
|||||||
freightType?: FreightType | null;
|
freightType?: FreightType | null;
|
||||||
trainNumber?: string | null;
|
trainNumber?: string | null;
|
||||||
direction?: string | null;
|
direction?: string | null;
|
||||||
|
/** True when this schedule needs loading confirmed before dispatch (import-Djibouti). */
|
||||||
|
requiresLoadingConfirmation?: boolean;
|
||||||
|
/** True when loading is already confirmed (or not required for this direction). */
|
||||||
|
loadingConfirmed?: boolean;
|
||||||
windowPhase?: BookingWindowPhase | string | null;
|
windowPhase?: BookingWindowPhase | string | null;
|
||||||
windowOpensAt?: string | null;
|
windowOpensAt?: string | null;
|
||||||
windowClosesAt?: string | null;
|
windowClosesAt?: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user