fix train builder and consolidation

This commit is contained in:
Marshal
2026-07-14 13:49:39 +00:00
parent 01459bd73c
commit 5cffe2860c
14 changed files with 1092 additions and 17 deletions

View File

@@ -1,5 +1,6 @@
import {
ActionIcon,
Alert,
Badge,
Box,
Button,
@@ -324,6 +325,21 @@ export default function NewBookingPage() {
const containerWeight = lines.reduce((s, l) => s + (l.quantity || 0) * (l.vgmPerUnitTons || 0), 0);
const cargoTotalWeightVgm = freightType === "CONTAINER" ? containerWeight : bulkWeight;
// 20ft containers ride two per wagon, so a booking must hold an even number
// of them — odd counts would leave half a wagon waiting on a co-loader
// (cross-booking consolidation is disabled for now).
const twentyFtCount = useMemo(() => {
const sizeById = new Map<string, string>();
for (const group of refData?.containers ?? []) {
for (const type of group.types) sizeById.set(type.id, group.size);
}
return lines.reduce((sum, l) => {
const size = l.containerTypeId ? (sizeById.get(l.containerTypeId) ?? "") : "";
return String(size).includes("20") ? sum + (l.quantity || 0) : sum;
}, 0);
}, [refData?.containers, lines]);
const hasOdd20ft = freightType === "CONTAINER" && twentyFtCount % 2 === 1;
// ---- validation ----
const lineValid = (l: ContainerLine) =>
Boolean(l.containerTypeId) && l.quantity >= 1 && l.vgmPerUnitTons > 0;
@@ -344,7 +360,7 @@ export default function NewBookingPage() {
(isGovernment ? Boolean(govCompanyId && govProfileId) : Boolean(companyId)) &&
(freightType === "BULK"
? Boolean(cargoTypeId) && bulkWeight > 0
: allLinesValid);
: allLinesValid && !hasOdd20ft);
const updateLine = (key: string, patch: Partial<ContainerLine>) =>
setLines((prev) => prev.map((l) => (l.key === key ? { ...l, ...patch } : l)));
@@ -842,6 +858,20 @@ export default function NewBookingPage() {
</Group>
) : null}
{hasOdd20ft ? (
<Alert color="red" icon={<AlertTriangle size={16} />} radius="md" mt="md">
<Text size="sm" fw={600}>
Odd number of 20ft containers ({twentyFtCount})
</Text>
<Text size="xs" mt={4}>
20ft containers travel two per wagon, so they must be booked in
even numbers. Add one more 20ft container or remove one e.g.
book {twentyFtCount + 1} or {twentyFtCount - 1} instead of{" "}
{twentyFtCount}.
</Text>
</Alert>
) : null}
<Stack gap="sm" mt="lg">
<Button
size="md"

View File

@@ -172,8 +172,8 @@ export default function TrainBuilderDetailPage() {
{ label: "Locomotives", value: composition.locomotives.length, icon: TrainFront },
{ label: "Wagons", value: totals.wagonCount, icon: TrainIcon },
{
label: "Payload available",
value: `${totals.payloadCapacityTons}T of ${totals.maxPullWeightTons}T`,
label: "Tare weight / haul limit",
value: `${totals.totalTareTons}T of ${totals.maxPullWeightTons}T`,
icon: Weight,
},
{

View File

@@ -43,6 +43,7 @@ import { CountdownTimer, DataTable, type ColumnDef } from "@edr/ui-common";
import { KpiStrip, PageContainer } from "@/components/page";
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import AdjustConsistModal from "@/components/trainScheduling/AdjustConsistModal";
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
import {
TrainConsistView,
@@ -684,6 +685,7 @@ export default function BatchScheduleDetailPage() {
);
const [activeTab, setActiveTab] = useState<string | null>("overview");
const [adjustConsistOpen, setAdjustConsistOpen] = useState(false);
const [selectedBookingId, setSelectedBookingId] = useState<string | null>(
null,
);
@@ -870,6 +872,17 @@ export default function BatchScheduleDetailPage() {
>
Refresh
</Button>
{data.train && ["DRAFT", "SCHEDULED"].includes(data.status) ? (
<Button
variant="light"
color="edr-green"
radius="md"
leftSection={<TrainFront size={16} />}
onClick={() => setAdjustConsistOpen(true)}
>
Adjust consist
</Button>
) : null}
{data.windowPhase === "DOC_REVIEW" ? (
<Button
color="yellow"
@@ -1181,6 +1194,12 @@ export default function BatchScheduleDetailPage() {
)}
</Tabs.Panel>
</Tabs>
<AdjustConsistModal
scheduleId={data.scheduleId}
opened={adjustConsistOpen}
onClose={() => setAdjustConsistOpen(false)}
/>
</PageContainer>
);
}

View File

@@ -52,6 +52,7 @@ import { IntercityRideAlongPanel } from "@/components/trainScheduling/IntercityR
import { YardWorkPanel } from "@/components/trainScheduling/YardWorkPanel";
// import { ImportLoadingConfirmationPanel } from "@/components/trainScheduling/ImportLoadingConfirmationPanel";
import { RescheduleTrainDialog } from "@/components/trainScheduling/RescheduleTrainDialog";
import AdjustConsistModal from "@/components/trainScheduling/AdjustConsistModal";
import BookingWindowSettingsModal from "@/components/trainScheduling/BookingWindowSettingsModal";
// import { ScheduleBatchPanel } from "@/components/trainScheduling/ScheduleBatchPanel";
import { ScheduleBookingsStep } from "@/components/trainScheduling/ScheduleBookingsStep";
@@ -101,6 +102,7 @@ export default function TrainScheduleV2DetailPage() {
const [previewResult, setPreviewResult] = useState<TrainSchedulePreviewResponse | null>(null);
const [containerPlacements, setContainerPlacements] = useState<ContainerPlacement[]>([]);
const [maintenanceOpen, setMaintenanceOpen] = useState(false);
const [adjustConsistOpen, setAdjustConsistOpen] = useState(false);
const [windowSettingsOpen, setWindowSettingsOpen] = useState(false);
const [gatepassSecuredAt, setGatepassSecuredAt] = useState("");
const [gatepassReference, setGatepassReference] = useState("");
@@ -949,6 +951,18 @@ export default function TrainScheduleV2DetailPage() {
Reschedule train
</Button>
) : null}
{schedule.train && ["DRAFT", "SCHEDULED"].includes(schedule.status) ? (
<Button
variant="light"
color="edr-green"
radius="lg"
size="sm"
leftSection={<Train size={16} />}
onClick={() => setAdjustConsistOpen(true)}
>
Adjust consist
</Button>
) : null}
{gatepassApplies ? (
gatepassSecured ? (
<Button
@@ -1173,6 +1187,12 @@ export default function TrainScheduleV2DetailPage() {
/>
) : null}
<AdjustConsistModal
scheduleId={schedule.id}
opened={adjustConsistOpen}
onClose={() => setAdjustConsistOpen(false)}
/>
<BookingWindowSettingsModal
scheduleId={scheduleId ?? null}
opened={windowSettingsOpen}