feat: full wagon cancel, leg board, wagon dates

feat(freight): editable train leg times, SL invoice payer
This commit is contained in:
Marshal
2026-08-15 10:12:37 +00:00
parent c9c2d4dcb3
commit dc38e843a6
31 changed files with 1619 additions and 217 deletions

View File

@@ -34,6 +34,7 @@ import {
Navigation,
Package,
PackageCheck,
Grid3x3,
Route as RouteIcon,
Ruler,
Send,
@@ -41,6 +42,7 @@ import {
Weight,
Workflow as WorkflowIcon,
} from "lucide-react";
import { DateTimePicker } from "@mantine/dates";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Link, useParams } from "react-router-dom";
@@ -57,6 +59,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 { LegLoadBoardPanel } from "@/components/trainScheduling/LegLoadBoardPanel";
import { LoadEmptyContainersModal } from "@/components/trainScheduling/LoadEmptyContainersModal";
import MergeScheduleTrainModal from "@/components/trainScheduling/MergeScheduleTrainModal";
import ScheduleHistoryPanel from "@/components/trainScheduling/ScheduleHistoryPanel";
@@ -125,6 +128,13 @@ export default function TrainScheduleV2DetailPage() {
const [gatepassFileUrl, setGatepassFileUrl] = useState("");
const [gatepassNotes, setGatepassNotes] = useState("");
const [dispatchConfirmOpen, setDispatchConfirmOpen] = useState(false);
// Actual departure — staff often dispatch on paper first and record it later,
// so the time is picked (defaults to now when the dialog opens).
const [dispatchAt, setDispatchAt] = useState<Date | null>(null);
const openDispatchConfirm = () => {
setDispatchAt(new Date());
setDispatchConfirmOpen(true);
};
const [switchTarget, setSwitchTarget] = useState<EligibleContainerBooking | null>(null);
const [visualization3DOpen, setVisualization3DOpen] = useState(false);
const [loadEmptiesOpen, setLoadEmptiesOpen] = useState(false);
@@ -477,7 +487,10 @@ export default function TrainScheduleV2DetailPage() {
const runDispatch = async () => {
setDispatchConfirmOpen(false);
try {
await dispatch.mutateAsync(scheduleId);
await dispatch.mutateAsync({
id: scheduleId,
payload: dispatchAt ? { actualDepartureAt: dispatchAt.toISOString() } : {},
});
await openMarshallingDocument({
title: "Train dispatched",
successDescription: "Marshalling document generated for the dispatched train.",
@@ -873,7 +886,7 @@ export default function TrainScheduleV2DetailPage() {
radius="md"
leftSection={<Send size={18} />}
loading={dispatch.isPending}
onClick={() => setDispatchConfirmOpen(true)}
onClick={openDispatchConfirm}
>
Dispatch train
</Button>
@@ -1271,6 +1284,9 @@ export default function TrainScheduleV2DetailPage() {
<Tabs.Tab value="legs" leftSection={<RouteIcon size={16} />}>
Leg capacity
</Tabs.Tab>
<Tabs.Tab value="leg-board" leftSection={<Grid3x3 size={16} />}>
Leg board
</Tabs.Tab>
<Tabs.Tab value="history" leftSection={<HistoryIcon size={16} />}>
History
</Tabs.Tab>
@@ -1359,6 +1375,13 @@ export default function TrainScheduleV2DetailPage() {
<LegCapacityPanel schedule={schedule} />
</Tabs.Panel>
<Tabs.Panel value="leg-board">
<LegLoadBoardPanel
schedule={schedule}
onChanged={() => void detailQuery.refetch()}
/>
</Tabs.Panel>
<Tabs.Panel value="history">
{scheduleId ? <ScheduleHistoryPanel scheduleId={scheduleId} /> : null}
</Tabs.Panel>
@@ -1444,6 +1467,17 @@ export default function TrainScheduleV2DetailPage() {
undone.
</Text>
<DateTimePicker
label="Actual departure"
description="When the train left — defaults to now; a past time is fine."
value={dispatchAt}
onChange={(v) => setDispatchAt(v ? new Date(v) : null)}
maxDate={new Date()}
valueFormat="DD MMM YYYY HH:mm"
clearable={false}
radius="md"
/>
{hasDispatchWarnings ? (
<Alert
color="orange"