remove reopen delay minutes from global rules and update related types

- Removed the  field from  and related components.
- Updated  to reflect the removal of the reopen delay input field.
- Modified  to include new train number fields:  and .
- Added  interface to manage active schedules with trade direction.
- Introduced  interface to track wagon shortages in bookings.
- Updated  logic to ensure consistent UI state representation.
- Created migrations to drop the  column and add  and  columns to the  table.
- Added tests for the new booking window display logic and wagon planning functionality.
This commit is contained in:
Marshal
2026-07-15 09:13:02 +00:00
parent 9be7f356f0
commit 11771e5f92
39 changed files with 1731 additions and 269 deletions

View File

@@ -244,6 +244,21 @@ export default function TrainScheduleV2DetailPage() {
return [];
}, [previewResult?.wagonPlan, schedule?.trainSet?.wagons]);
// EXPORT schedules render the consist back-to-front (the train turns around
// for the return run) — DISPLAY ONLY: stored sequenceNos, allocations,
// documents, and the adjust-consist / placement flows keep the as-built order.
const isExportDisplay = schedule?.direction === "EXPORT";
const displayWagonPlanOriented = useMemo(
() => (isExportDisplay ? [...displayWagonPlan].reverse() : displayWagonPlan),
[displayWagonPlan, isExportDisplay],
);
const diagramWagons = useMemo(() => {
const source = schedule?.trainSet?.wagons?.length
? schedule.trainSet.wagons
: displayWagonPlan;
return isExportDisplay ? [...source].reverse() : source;
}, [schedule?.trainSet?.wagons, displayWagonPlan, isExportDisplay]);
const runPreview = useCallback(
async (options?: { silent?: boolean; advanceStep?: boolean }) => {
if (!schedule || !scheduleId) return null;
@@ -683,7 +698,12 @@ export default function TrainScheduleV2DetailPage() {
fleetAvailability={previewResult?.fleetAvailability}
deferredBookings={previewResult?.deferredBookings}
/>
<WagonPlanGrid wagonPlan={displayWagonPlan} freightType={freightType} />
{isExportDisplay && displayWagonPlanOriented.length ? (
<Text size="xs" c="dimmed">
Shown rear-first (export direction) positions keep their original numbers.
</Text>
) : null}
<WagonPlanGrid wagonPlan={displayWagonPlanOriented} freightType={freightType} />
{canEditBookings && (previewResult || displayWagonPlan.length) ? (
<Group>
{!hasContainerStep ? (
@@ -760,15 +780,16 @@ export default function TrainScheduleV2DetailPage() {
<TrainCompositionDiagram
locomotive={schedule.trainSet?.locomotive}
locomotives={locomotives}
wagons={
schedule.trainSet?.wagons?.length
? schedule.trainSet.wagons
: displayWagonPlan
}
wagons={diagramWagons}
freightType={freightType}
trainNumber={schedule.train ? schedule.train.code : schedule.trainNumber}
trainNumber={schedule.trainNumber ?? schedule.train?.code ?? null}
totalLengthMeters={schedule.trainSet?.totalLengthMeters}
/>
{isExportDisplay && diagramWagons.length ? (
<Text size="xs" c="dimmed">
Shown rear-first (export direction) positions keep their original numbers.
</Text>
) : null}
<Paper
p="lg"
radius="lg"
@@ -885,6 +906,11 @@ export default function TrainScheduleV2DetailPage() {
{schedule.trainNumber}
</Badge>
) : null}
{schedule.train ? (
<Text size="xs" c="dimmed" ff="monospace">
Train {schedule.train.code}
</Text>
) : null}
</Group>
<Box maw={340}>
<RouteCorridor

View File

@@ -327,19 +327,24 @@ export default function TrainScheduleV2ListPage() {
header: "Train",
meta: { headerClassName, cellClassName },
cell: ({ row }) => {
// Schedules created from the Train Builder carry the train code;
// legacy rows fall back to their locomotive set.
// Schedules created from the Train Builder show the direction-matched
// run number first (falling back to the train code); legacy rows fall
// back to their locomotive set.
if (row.original.train) {
const subtitle = [row.original.trainNumber ? row.original.train.code : null,
row.original.train.trainName]
.filter(Boolean)
.join(" · ");
return (
<Group gap={6} wrap="nowrap">
<Train size={14} color="var(--mantine-color-gray-5)" />
<Stack gap={0}>
<Text size="sm" fw={600} ff="monospace" lh={1.2}>
{row.original.train.code}
{row.original.trainNumber ?? row.original.train.code}
</Text>
{row.original.train.trainName ? (
{subtitle ? (
<Text size="xs" c="dimmed" lh={1.2}>
{row.original.train.trainName}
{subtitle}
</Text>
) : null}
</Stack>
@@ -758,14 +763,21 @@ export default function TrainScheduleV2ListPage() {
label="Train"
description="A built train (Train Builder) runs this departure with its locomotives and wagons"
placeholder={routeId ? "Select a train" : "Select a route first"}
data={(trainsQuery.data ?? []).map((train) => ({
value: train.id,
label: `${train.code}${train.trainName ? `${train.trainName}` : ""} · ${
train.locomotives.length
} locos · ${train.wagonCount} wagons${train.atOriginYard ? "" : " · not at origin yard"}${
train.futureScheduleCount ? ` · ${train.futureScheduleCount} future run(s)` : ""
}`,
}))}
data={(trainsQuery.data ?? []).map((train) => {
// Route direction picks which of the train's typed pair this run uses.
const runNumber =
selectedRoute?.direction === "IMPORT"
? train.importTrainNumber
: train.exportTrainNumber;
return {
value: train.id,
label: `${train.code}${train.trainName ? `${train.trainName}` : ""}${
runNumber ? ` · runs as ${runNumber}` : ""
} · ${train.locomotives.length} locos · ${train.wagonCount} wagons${
train.atOriginYard ? "" : " · not at origin yard"
}${train.futureScheduleCount ? ` · ${train.futureScheduleCount} future run(s)` : ""}`,
};
})}
value={trainId || null}
onChange={(v) => setTrainId(v ?? "")}
searchable

View File

@@ -62,7 +62,6 @@ export default function TrainSchedulingGlobalRulesPage() {
"windowDurationHours",
"docReviewMinutes",
"paymentWindowMinutes",
"reopenDelayMinutes",
];
const payload: Partial<Record<keyof TrainSchedulingGlobalRules, number>> = {};
for (const key of fields) {
@@ -261,17 +260,6 @@ export default function TrainSchedulingGlobalRulesPage() {
min={1}
disabled={loading}
/>
<DurationField
label="Reopen delay"
description="Delay after window close before reopening when the train is not full (90 min = 11:00 close → 12:30 reopen)"
value={form.reopenDelayMinutes ?? ""}
nativeUnit="minutes"
onChange={(value) =>
setForm((current) => ({ ...current, reopenDelayMinutes: value }))
}
min={1}
disabled={loading}
/>
<Group justify="flex-end">
<Button loading={saving} disabled={loading} onClick={() => void handleSave()}>
Save rules