mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
Merge branch 'dev'
This commit is contained in:
@@ -19,7 +19,8 @@ import {
|
||||
ThemeIcon,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import { DatePickerInput } from "@mantine/dates";
|
||||
import { getDateRangePresets } from "@/components/common/dateRangePresets";
|
||||
import { useDebouncedValue } from "@mantine/hooks";
|
||||
import {
|
||||
AlertTriangle,
|
||||
@@ -792,24 +793,19 @@ export default function BatchBoardPage() {
|
||||
w={140}
|
||||
styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }}
|
||||
/>
|
||||
<DateInput
|
||||
<DatePickerInput
|
||||
type="range"
|
||||
size="sm"
|
||||
radius="lg"
|
||||
placeholder="Departs from"
|
||||
value={departureFrom}
|
||||
onChange={(v) => setDepartureFrom(v ? new Date(v) : null)}
|
||||
placeholder="Departure date range"
|
||||
value={[departureFrom, departureTo]}
|
||||
onChange={([from, to]) => {
|
||||
setDepartureFrom(from ? new Date(from) : null);
|
||||
setDepartureTo(to ? new Date(to) : null);
|
||||
}}
|
||||
presets={getDateRangePresets()}
|
||||
clearable
|
||||
w={140}
|
||||
styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }}
|
||||
/>
|
||||
<DateInput
|
||||
size="sm"
|
||||
radius="lg"
|
||||
placeholder="Departs to"
|
||||
value={departureTo}
|
||||
onChange={(v) => setDepartureTo(v ? new Date(v) : null)}
|
||||
clearable
|
||||
w={140}
|
||||
w={230}
|
||||
styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }}
|
||||
/>
|
||||
<Select
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Badge,
|
||||
Box,
|
||||
@@ -7,6 +8,7 @@ import {
|
||||
Group,
|
||||
List,
|
||||
Loader,
|
||||
Menu,
|
||||
Modal,
|
||||
Paper,
|
||||
RingProgress,
|
||||
@@ -19,7 +21,6 @@ import {
|
||||
import { isAxiosError } from "axios";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
CalendarClock,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
@@ -29,6 +30,7 @@ import {
|
||||
FileText,
|
||||
History as HistoryIcon,
|
||||
LayoutGrid,
|
||||
MoreHorizontal,
|
||||
Navigation,
|
||||
Package,
|
||||
PackageCheck,
|
||||
@@ -42,7 +44,7 @@ import {
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
|
||||
import { KpiStrip, PageContainer } from "@/components/page";
|
||||
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||
import {
|
||||
@@ -886,284 +888,248 @@ export default function TrainScheduleV2DetailPage() {
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/dashboard/operations/train-scheduling-v2"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="compact-sm"
|
||||
leftSection={<ArrowLeft size={16} />}
|
||||
w="fit-content"
|
||||
>
|
||||
Back to schedules
|
||||
</Button>
|
||||
|
||||
<Paper
|
||||
radius="xl"
|
||||
p="xl"
|
||||
style={{ position: "relative", overflow: "hidden" }}
|
||||
>
|
||||
<Stack gap="lg" style={{ position: "relative" }}>
|
||||
<Group justify="space-between" align="flex-start" wrap="wrap">
|
||||
<Group gap="md" align="flex-start" wrap="nowrap">
|
||||
<ThemeIcon size={56} radius="lg" variant="light" color="#F2A516">
|
||||
<Train size={28} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={6}>
|
||||
<Group gap="sm" align="center" wrap="wrap">
|
||||
{schedule.reference ? (
|
||||
<Badge
|
||||
variant="filled"
|
||||
color="edr-green"
|
||||
radius="sm"
|
||||
style={{ fontWeight: 700, fontFamily: "monospace" }}
|
||||
>
|
||||
{schedule.reference}
|
||||
</Badge>
|
||||
) : null}
|
||||
<Title order={2} fw={700} style={{ color: "#0f172a" }}>
|
||||
{schedule.route?.name ?? "Train schedule"}
|
||||
</Title>
|
||||
{schedule.train?.trainName ? (
|
||||
<Text fw={700} style={{ color: "#0f172a" }}>
|
||||
{schedule.train.trainName}
|
||||
</Text>
|
||||
) : null}
|
||||
{schedule.train ? (
|
||||
<Text size="xs" c="dimmed" ff="monospace">
|
||||
Train {schedule.train.code}
|
||||
</Text>
|
||||
) : null}
|
||||
</Group>
|
||||
|
||||
{/* Voyage (train) number and trade direction — the two things
|
||||
operations identify a run by, so they read at a glance
|
||||
rather than as small badges among the rest. */}
|
||||
<Group gap="lg" align="center" wrap="wrap">
|
||||
{schedule.trainNumber ? (
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" fw={600} tt="uppercase" lh={1.2}>
|
||||
Train No.
|
||||
</Text>
|
||||
<Text
|
||||
ff="monospace"
|
||||
fw={800}
|
||||
lh={1.1}
|
||||
style={{ fontSize: 32, color: "#0f172a" }}
|
||||
>
|
||||
{schedule.trainNumber}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
{schedule.voyageNumber ? (
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" fw={600} tt="uppercase" lh={1.2}>
|
||||
Voyage No.
|
||||
</Text>
|
||||
<Text
|
||||
ff="monospace"
|
||||
fw={800}
|
||||
lh={1.1}
|
||||
style={{ fontSize: 32, color: "#0f172a" }}
|
||||
>
|
||||
{schedule.voyageNumber}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
{/* Merging rewrites the consist, so it is offered only while
|
||||
the departure can still be edited. */}
|
||||
{canEditBookings ? (
|
||||
<Button
|
||||
variant="light"
|
||||
size="compact-sm"
|
||||
leftSection={<Merge size={14} />}
|
||||
onClick={() => setMergeModalOpen(true)}
|
||||
>
|
||||
Merge
|
||||
</Button>
|
||||
) : null}
|
||||
{schedule.direction ? (
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" fw={600} tt="uppercase" lh={1.2}>
|
||||
Direction
|
||||
</Text>
|
||||
<Text
|
||||
fw={800}
|
||||
lh={1.1}
|
||||
tt="uppercase"
|
||||
style={{
|
||||
fontSize: 32,
|
||||
letterSpacing: 0.5,
|
||||
color:
|
||||
schedule.direction === "IMPORT"
|
||||
? "#2E5B96"
|
||||
: schedule.direction === "EXPORT"
|
||||
? "#0A6F4D"
|
||||
: "#0f172a",
|
||||
}}
|
||||
>
|
||||
{schedule.direction}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
</Group>
|
||||
{(schedule.stops?.length ?? 0) >= 3 ||
|
||||
(schedule.bookings ?? []).some(
|
||||
(b) => b.tradeDirection === "DOMESTIC",
|
||||
) ? (
|
||||
<SegmentOccupancyStrip
|
||||
stops={schedule.stops ?? []}
|
||||
bookings={schedule.bookings ?? []}
|
||||
maxWagons={schedule.maxWagons}
|
||||
maxGrossTons={schedule.maxGrossWeightTons}
|
||||
<PageHeader
|
||||
title={schedule.route?.name ?? "Train schedule"}
|
||||
backTo="/dashboard/operations/train-scheduling-v2"
|
||||
breadcrumbs={[
|
||||
{
|
||||
label: "Train schedules",
|
||||
href: "/dashboard/operations/train-scheduling-v2",
|
||||
},
|
||||
{ label: schedule.reference ?? "Schedule" },
|
||||
]}
|
||||
subtitle={
|
||||
schedule.train ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
{schedule.train.trainName ?? `Train ${schedule.train.code}`}
|
||||
{schedule.train.trainName ? ` · Train ${schedule.train.code}` : ""}
|
||||
</Text>
|
||||
) : undefined
|
||||
}
|
||||
meta={
|
||||
<Group gap={6} wrap="wrap">
|
||||
{schedule.reference ? (
|
||||
<Badge
|
||||
variant="filled"
|
||||
color="edr-green"
|
||||
radius="sm"
|
||||
style={{ fontWeight: 700, fontFamily: "monospace" }}
|
||||
>
|
||||
{schedule.reference}
|
||||
</Badge>
|
||||
) : null}
|
||||
<FreightTypeBadge freightType={schedule.freightType} />
|
||||
<StatusPill status={schedule.status} />
|
||||
{gatepassApplies && gatepassSecured ? (
|
||||
<Badge
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="sm"
|
||||
leftSection={<CheckCircle2 size={12} />}
|
||||
>
|
||||
Gate pass secured
|
||||
</Badge>
|
||||
) : null}
|
||||
{previewResult ? (
|
||||
<Badge
|
||||
radius="sm"
|
||||
variant="light"
|
||||
color={previewResult.valid ? "edr-green" : "red"}
|
||||
leftSection={
|
||||
<Box
|
||||
w={8}
|
||||
h={8}
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
background: previewResult.valid
|
||||
? "var(--mantine-color-edr-green-6)"
|
||||
: "var(--mantine-color-red-6)",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Box maw={340}>
|
||||
<RouteCorridor
|
||||
origin={
|
||||
schedule.originStation?.label ?? schedule.originStation?.code
|
||||
}
|
||||
destination={
|
||||
schedule.destinationStation?.label ??
|
||||
schedule.destinationStation?.code
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
<Group gap="sm" align="center">
|
||||
<FreightTypeBadge freightType={schedule.freightType} />
|
||||
<StatusPill status={schedule.status} />
|
||||
</Group>
|
||||
</Stack>
|
||||
</Group>
|
||||
<Group gap="sm">
|
||||
{(schedule.trainSet?.wagons?.length ?? 0) > 0 ? (
|
||||
<Button
|
||||
variant="gradient"
|
||||
gradient={{ from: "#0f172a", to: "#334155" }}
|
||||
radius="lg"
|
||||
size="sm"
|
||||
leftSection={<Eye size={16} />}
|
||||
onClick={() => setVisualization3DOpen(true)}
|
||||
>
|
||||
3D Visualization
|
||||
</Button>
|
||||
) : null}
|
||||
{canPrintMarshalling ? (
|
||||
<Button
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="lg"
|
||||
size="sm"
|
||||
leftSection={<FileText size={16} />}
|
||||
loading={downloadMarshalling.isPending}
|
||||
onClick={() => void openMarshallingDocument()}
|
||||
>
|
||||
Marshalling PDF
|
||||
</Button>
|
||||
) : null}
|
||||
{["DISPATCHED", "ARRIVED"].includes(schedule.status) ? (
|
||||
<Button
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="lg"
|
||||
size="sm"
|
||||
leftSection={<FileText size={16} />}
|
||||
loading={downloadMarshalling.isPending}
|
||||
onClick={() =>
|
||||
void openMarshallingDocument({
|
||||
title: "Intercity marshalling ready",
|
||||
variant: "INTERCITY",
|
||||
})
|
||||
}
|
||||
>
|
||||
Intercity Marshalling
|
||||
</Button>
|
||||
) : null}
|
||||
{["DISPATCHED", "ARRIVED"].includes(schedule.status) ? (
|
||||
<Button
|
||||
component={Link}
|
||||
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}/track`}
|
||||
color="edr-green"
|
||||
radius="lg"
|
||||
size="sm"
|
||||
leftSection={<Navigation size={16} />}
|
||||
>
|
||||
Track train
|
||||
</Button>
|
||||
) : null}
|
||||
{schedule.windowPhase === "PRE_WINDOW" ? (
|
||||
<Button
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="lg"
|
||||
size="sm"
|
||||
leftSection={<Clock size={16} />}
|
||||
onClick={() => setWindowSettingsOpen(true)}
|
||||
>
|
||||
Window settings
|
||||
</Button>
|
||||
) : null}
|
||||
{["DRAFT", "SCHEDULED"].includes(schedule.status) ? (
|
||||
<Button
|
||||
variant="default"
|
||||
radius="lg"
|
||||
size="sm"
|
||||
onClick={() => setMaintenanceOpen(true)}
|
||||
>
|
||||
Reschedule train
|
||||
</Button>
|
||||
) : null}
|
||||
{gatepassApplies ? (
|
||||
gatepassSecured ? (
|
||||
<Button
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
radius="lg"
|
||||
size="sm"
|
||||
leftSection={<CheckCircle2 size={16} />}
|
||||
disabled
|
||||
}
|
||||
>
|
||||
Preview {previewResult.valid ? "valid" : "has issues"}
|
||||
</Badge>
|
||||
) : null}
|
||||
</Group>
|
||||
}
|
||||
action={
|
||||
<Group gap="sm" wrap="nowrap">
|
||||
{/* Merging rewrites the consist, so it is offered only while
|
||||
the departure can still be edited. */}
|
||||
{canEditBookings ? (
|
||||
<Button
|
||||
variant="light"
|
||||
size="compact-sm"
|
||||
leftSection={<Merge size={14} />}
|
||||
onClick={() => setMergeModalOpen(true)}
|
||||
>
|
||||
Merge
|
||||
</Button>
|
||||
) : null}
|
||||
{(schedule.trainSet?.wagons?.length ?? 0) > 0 ? (
|
||||
<Button
|
||||
variant="gradient"
|
||||
gradient={{ from: "#0f172a", to: "#334155" }}
|
||||
radius="lg"
|
||||
size="compact-sm"
|
||||
leftSection={<Eye size={16} />}
|
||||
onClick={() => setVisualization3DOpen(true)}
|
||||
>
|
||||
3D Visualization
|
||||
</Button>
|
||||
) : null}
|
||||
<Menu position="bottom-end" width={240} withinPortal>
|
||||
<Menu.Target>
|
||||
<ActionIcon variant="default" size="lg" radius="md" aria-label="More actions">
|
||||
<MoreHorizontal size={16} />
|
||||
</ActionIcon>
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{canPrintMarshalling ? (
|
||||
<Menu.Item
|
||||
leftSection={<FileText size={15} />}
|
||||
disabled={downloadMarshalling.isPending}
|
||||
onClick={() => void openMarshallingDocument()}
|
||||
>
|
||||
Gate pass secured
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
color="edr-green"
|
||||
radius="lg"
|
||||
size="sm"
|
||||
leftSection={<FileText size={16} />}
|
||||
loading={secureGatepass.isPending}
|
||||
Marshalling PDF
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
{["DISPATCHED", "ARRIVED"].includes(schedule.status) ? (
|
||||
<Menu.Item
|
||||
leftSection={<FileText size={15} />}
|
||||
disabled={downloadMarshalling.isPending}
|
||||
onClick={() =>
|
||||
void openMarshallingDocument({
|
||||
title: "Intercity marshalling ready",
|
||||
variant: "INTERCITY",
|
||||
})
|
||||
}
|
||||
>
|
||||
Intercity Marshalling
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
{["DISPATCHED", "ARRIVED"].includes(schedule.status) ? (
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}/track`}
|
||||
leftSection={<Navigation size={15} />}
|
||||
>
|
||||
Track train
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
{schedule.windowPhase === "PRE_WINDOW" ? (
|
||||
<Menu.Item
|
||||
leftSection={<Clock size={15} />}
|
||||
onClick={() => setWindowSettingsOpen(true)}
|
||||
>
|
||||
Window settings
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
{["DRAFT", "SCHEDULED"].includes(schedule.status) ? (
|
||||
<Menu.Item onClick={() => setMaintenanceOpen(true)}>
|
||||
Reschedule train
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
{gatepassApplies && !gatepassSecured ? (
|
||||
<Menu.Item
|
||||
leftSection={<FileText size={15} />}
|
||||
disabled={secureGatepass.isPending}
|
||||
onClick={() => secureGatepass.mutate()}
|
||||
>
|
||||
Secure gate pass
|
||||
</Button>
|
||||
)
|
||||
) : null}
|
||||
</Group>
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</Group>
|
||||
}
|
||||
/>
|
||||
|
||||
{previewResult ? (
|
||||
<Badge
|
||||
size="lg"
|
||||
radius="sm"
|
||||
variant="light"
|
||||
color={previewResult.valid ? "edr-green" : "red"}
|
||||
leftSection={
|
||||
<Box
|
||||
w={8}
|
||||
h={8}
|
||||
{/* Ops signage: Train No. / Voyage No. / Direction read at a glance from
|
||||
across the room, so these stay large rather than folding into the
|
||||
numeric KpiStrip below. */}
|
||||
<Paper radius="xl" p="lg">
|
||||
<Stack gap="md">
|
||||
<Group gap="lg" align="center" wrap="wrap">
|
||||
{schedule.trainNumber ? (
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" fw={600} tt="uppercase" lh={1.2}>
|
||||
Train No.
|
||||
</Text>
|
||||
<Text
|
||||
ff="monospace"
|
||||
fw={800}
|
||||
lh={1.1}
|
||||
style={{ fontSize: 32, color: "#0f172a" }}
|
||||
>
|
||||
{schedule.trainNumber}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
{schedule.voyageNumber ? (
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" fw={600} tt="uppercase" lh={1.2}>
|
||||
Voyage No.
|
||||
</Text>
|
||||
<Text
|
||||
ff="monospace"
|
||||
fw={800}
|
||||
lh={1.1}
|
||||
style={{ fontSize: 32, color: "#0f172a" }}
|
||||
>
|
||||
{schedule.voyageNumber}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
{schedule.direction ? (
|
||||
<Box>
|
||||
<Text size="xs" c="dimmed" fw={600} tt="uppercase" lh={1.2}>
|
||||
Direction
|
||||
</Text>
|
||||
<Text
|
||||
fw={800}
|
||||
lh={1.1}
|
||||
tt="uppercase"
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
background: previewResult.valid
|
||||
? "var(--mantine-color-edr-green-6)"
|
||||
: "var(--mantine-color-red-6)",
|
||||
fontSize: 32,
|
||||
letterSpacing: 0.5,
|
||||
color:
|
||||
schedule.direction === "IMPORT"
|
||||
? "#2E5B96"
|
||||
: schedule.direction === "EXPORT"
|
||||
? "#0A6F4D"
|
||||
: "#0f172a",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
Preview {previewResult.valid ? "valid" : "has issues"}
|
||||
</Badge>
|
||||
) : null}
|
||||
>
|
||||
{schedule.direction}
|
||||
</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
</Group>
|
||||
{(schedule.stops?.length ?? 0) >= 3 ||
|
||||
(schedule.bookings ?? []).some(
|
||||
(b) => b.tradeDirection === "DOMESTIC",
|
||||
) ? (
|
||||
<SegmentOccupancyStrip
|
||||
stops={schedule.stops ?? []}
|
||||
bookings={schedule.bookings ?? []}
|
||||
maxWagons={schedule.maxWagons}
|
||||
maxGrossTons={schedule.maxGrossWeightTons}
|
||||
/>
|
||||
) : (
|
||||
<Box maw={340}>
|
||||
<RouteCorridor
|
||||
origin={
|
||||
schedule.originStation?.label ?? schedule.originStation?.code
|
||||
}
|
||||
destination={
|
||||
schedule.destinationStation?.label ??
|
||||
schedule.destinationStation?.code
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
|
||||
|
||||
@@ -143,6 +143,9 @@ export default function TrainScheduleV2ListPage() {
|
||||
const [scheduleDate, setScheduleDate] = useState("");
|
||||
const [trainId, setTrainId] = useState("");
|
||||
const [reverseWagonOrder, setReverseWagonOrder] = useState(false);
|
||||
// "" = a normal customer train; an id dedicates the departure to that
|
||||
// shipping line and hides it from every customer-facing view.
|
||||
const [shippingLineCompanyId, setShippingLineCompanyId] = useState("");
|
||||
// Booking window for the schedule being created: off = inherit the live global
|
||||
// rules (the default), on = the values in `windowForm` are frozen onto it.
|
||||
const [configureWindow, setConfigureWindow] = useState(false);
|
||||
@@ -219,6 +222,15 @@ export default function TrainScheduleV2ListPage() {
|
||||
enabled: Boolean(routeId),
|
||||
}),
|
||||
);
|
||||
// For the create modal's dedication picker. 100 covers every line EDR deals
|
||||
// with; fetched only while the modal is open.
|
||||
const shippingLinesQuery = useQuery(
|
||||
api.shippingLineCompanies.list.queryOptions({
|
||||
input: { page: 1, limit: 100 },
|
||||
enabled: createOpen,
|
||||
staleTime: 5 * 60_000,
|
||||
}),
|
||||
);
|
||||
const create = useMutation(api.trainScheduling.createSchedule.mutationOptions());
|
||||
const dispatchSchedule = useMutation(
|
||||
api.trainScheduling.dispatchSchedule.mutationOptions(),
|
||||
@@ -559,12 +571,14 @@ export default function TrainScheduleV2ListPage() {
|
||||
scheduleDate: new Date(scheduleDate).toISOString(),
|
||||
trainId,
|
||||
reverseWagonOrder,
|
||||
...(shippingLineCompanyId ? { shippingLineCompanyId } : {}),
|
||||
...(windowRule ? { windowRule } : {}),
|
||||
},
|
||||
});
|
||||
toast({ title: "Train schedule created" });
|
||||
showScheduleWarnings(created.warnings);
|
||||
setReverseWagonOrder(false);
|
||||
setShippingLineCompanyId("");
|
||||
setConfigureWindow(false);
|
||||
setWindowForm(null);
|
||||
setCreateOpen(false);
|
||||
@@ -857,6 +871,19 @@ export default function TrainScheduleV2ListPage() {
|
||||
: "Select a route first"
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
label="Shipping line (optional)"
|
||||
description="Dedicate this departure to one shipping line. The train is then hidden from customers and shown only in that line's portal."
|
||||
placeholder="None — normal customer train"
|
||||
clearable
|
||||
searchable
|
||||
data={(shippingLinesQuery.data?.items ?? [])
|
||||
.filter((line) => line.status === "active")
|
||||
.map((line) => ({ value: line.id, label: line.name }))}
|
||||
value={shippingLineCompanyId || null}
|
||||
onChange={(v) => setShippingLineCompanyId(v ?? "")}
|
||||
comboboxProps={{ withinPortal: true }}
|
||||
/>
|
||||
<Checkbox
|
||||
label="Reverse wagon order"
|
||||
description="Place wagons on the train in reverse — the physically-last wagon becomes position 1. Composition and allocations are unchanged; only the order flips. Applies every time this schedule's wagon plan is built."
|
||||
|
||||
Reference in New Issue
Block a user