This commit is contained in:
Marshal
2026-07-14 11:06:49 +00:00
parent 957a185a4d
commit 6d0cf50b4d
64 changed files with 4896 additions and 404 deletions

View File

@@ -7,7 +7,6 @@ import {
Group,
Menu,
Modal,
MultiSelect,
Select,
SimpleGrid,
Stack,
@@ -41,10 +40,7 @@ import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import { FreightTypeBadge } from "@/components/trainScheduling/ScheduleStatusBadge";
import BookingWindowSettingsModal from "@/components/trainScheduling/BookingWindowSettingsModal";
import EditScheduleDateModal from "@/components/trainScheduling/EditScheduleDateModal";
import {
locomotiveOption,
showScheduleWarnings,
} from "@/components/trainScheduling/locomotiveOptions";
import { showScheduleWarnings } from "@/components/trainScheduling/locomotiveOptions";
import {
RouteCorridor,
StatusPill,
@@ -119,7 +115,7 @@ export default function TrainScheduleV2ListPage() {
useState<TrainScheduleListItem | null>(null);
const [routeId, setRouteId] = useState("");
const [scheduleDate, setScheduleDate] = useState("");
const [locomotiveIds, setLocomotiveIds] = useState<string[]>([]);
const [trainId, setTrainId] = useState("");
// Recomputed each time the create modal opens so a long-lived tab can't keep
// offering a stale "now" as the earliest selectable departure.
const minScheduleDate = useMemo(
@@ -186,9 +182,10 @@ export default function TrainScheduleV2ListPage() {
const routesQuery = useQuery(
api.routes.list.queryOptions({ input: { status: "AVAILABLE" } }),
);
const locomotivesQuery = useQuery(
api.trainScheduling.availableLocomotives.queryOptions({
input: { routeId: routeId || undefined },
const trainsQuery = useQuery(
api.trainScheduling.availableTrains.queryOptions({
input: { routeId },
enabled: Boolean(routeId),
}),
);
const create = useMutation(api.trainScheduling.createSchedule.mutationOptions());
@@ -203,17 +200,17 @@ export default function TrainScheduleV2ListPage() {
const selectedRoute = activeRoutes.find((r) => r.id === routeId);
const locomotiveYardHint = useMemo(() => {
const trainYardHint = useMemo(() => {
if (!selectedRoute) return "Select a route first";
const originLabel =
selectedRoute.originYard?.label ??
selectedRoute.originYard?.code ??
"the route origin yard";
return `All in-service locomotives are shown — those not yet at ${originLabel} or already on future schedules are flagged`;
return `All schedulable built trains are shown — those not yet at ${originLabel} or already on future schedules are flagged`;
}, [selectedRoute]);
useEffect(() => {
setLocomotiveIds([]);
setTrainId("");
}, [routeId]);
// Filtering, sorting, and paging all happen server-side — `schedules` IS the
@@ -326,10 +323,29 @@ export default function TrainScheduleV2ListPage() {
cell: ({ row }) => <FreightTypeBadge freightType={row.original.freightType} />,
},
{
id: "loco",
header: "Locomotives",
id: "train",
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.
if (row.original.train) {
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}
</Text>
{row.original.train.trainName ? (
<Text size="xs" c="dimmed" lh={1.2}>
{row.original.train.trainName}
</Text>
) : null}
</Stack>
</Group>
);
}
const locos =
row.original.locomotives && row.original.locomotives.length > 0
? row.original.locomotives
@@ -456,9 +472,9 @@ export default function TrainScheduleV2ListPage() {
}, [navigate, cancel.isPending, cancel, toast]);
const handleCreate = async () => {
if (!routeId || !scheduleDate || locomotiveIds.length < 2) {
if (!routeId || !scheduleDate || !trainId) {
toast({
title: "Select route, date, and at least two locomotives",
title: "Select route, date, and the train to run",
variant: "destructive",
});
return;
@@ -475,7 +491,7 @@ export default function TrainScheduleV2ListPage() {
payload: {
routeId,
scheduleDate: new Date(scheduleDate).toISOString(),
locomotiveIds,
trainId,
},
});
toast({ title: "Train schedule created" });
@@ -728,7 +744,7 @@ export default function TrainScheduleV2ListPage() {
/>
{routeId ? (
<Text size="xs" c="dimmed">
{locomotiveYardHint}
{trainYardHint}
</Text>
) : null}
<TextInput
@@ -738,24 +754,26 @@ export default function TrainScheduleV2ListPage() {
value={scheduleDate}
onChange={(e) => setScheduleDate(e.currentTarget.value)}
/>
<MultiSelect
label="Locomotives"
description="A train must be pulled by at least two locomotives (front and back)"
placeholder={
routeId ? "Select at least two locomotives" : "Select a route first"
}
data={(locomotivesQuery.data ?? []).map((l) => locomotiveOption(l))}
value={locomotiveIds}
onChange={setLocomotiveIds}
<Select
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)` : ""
}`,
}))}
value={trainId || null}
onChange={(v) => setTrainId(v ?? "")}
searchable
disabled={!routeId}
error={
locomotiveIds.length > 0 && locomotiveIds.length < 2
? "Select at least two locomotives"
: undefined
}
nothingFoundMessage={
routeId ? "No available locomotives for this corridor" : "Select a route first"
routeId
? "No built trains yet — assemble one in the Train Builder first"
: "Select a route first"
}
/>
<Group justify="flex-end">