mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-06 08:53:39 +00:00
651 lines
22 KiB
TypeScript
651 lines
22 KiB
TypeScript
import { Link, useParams } from "react-router-dom";
|
|
import { isAxiosError } from "axios";
|
|
import { useState } from "react";
|
|
import {
|
|
ArrowLeft,
|
|
CalendarClock,
|
|
ChevronRight,
|
|
FileText,
|
|
Flag,
|
|
ListChecks,
|
|
MapPin,
|
|
Package,
|
|
PackageCheck,
|
|
Pencil,
|
|
Route,
|
|
TrainFront,
|
|
} from "lucide-react";
|
|
import { Box, Button, Group, Loader, Menu, Stack, Text } from "@mantine/core";
|
|
|
|
import { CheckpointTimeModal } from "@/components/trainScheduling/CheckpointTimeModal";
|
|
import { CheckpointLogTable } from "@/components/trainScheduling/CheckpointLogTable";
|
|
import { JourneySpine } from "@/components/trainScheduling/JourneySpine";
|
|
import { LogPassYardWorkModal } from "@/components/trainScheduling/LogPassYardWorkModal";
|
|
import { TrackStatusCard } from "@/components/trainScheduling/TrackStatusCard";
|
|
import { Chip, SectionHead } from "@/components/trainScheduling/trackPrimitives";
|
|
import { track as T } from "@/components/trainScheduling/trackTheme";
|
|
import type {
|
|
CheckpointHandlingTimes,
|
|
TrackStation,
|
|
TrainCheckpoint,
|
|
} from "@/types/trainScheduling";
|
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
import { api } from "@/services/api";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { openPdfBlob } from "@/components/warehouses/pdf";
|
|
import { trainSchedulingService } from "@/services/trainScheduling.service";
|
|
|
|
type CheckpointModalValues = { occurredAt: string; note: string } & Required<
|
|
Record<keyof CheckpointHandlingTimes, string | null>
|
|
>;
|
|
|
|
/**
|
|
* The station-work stamps off the modal.
|
|
*
|
|
* On an edit a cleared picker means "remove this stamp", so nulls are sent.
|
|
* On a first log there is nothing to remove, and the record endpoint takes no
|
|
* nulls — the untouched pickers are dropped instead.
|
|
*/
|
|
const pickHandling = (
|
|
values: CheckpointModalValues,
|
|
keepNulls: boolean,
|
|
): CheckpointHandlingTimes =>
|
|
Object.fromEntries(
|
|
(
|
|
[
|
|
"unloadingStartedAt",
|
|
"unloadingCompletedAt",
|
|
"loadingStartedAt",
|
|
"loadingCompletedAt",
|
|
] as const
|
|
)
|
|
.map((field) => [field, values[field]] as const)
|
|
.filter(([, value]) => keepNulls || value !== null),
|
|
);
|
|
|
|
const parseError = (error: unknown, fallback: string) => {
|
|
if (isAxiosError(error)) {
|
|
const data = error.response?.data as Record<string, unknown> | undefined;
|
|
const message = data?.message;
|
|
if (Array.isArray(message)) return message.join(", ");
|
|
if (typeof message === "string") return message;
|
|
}
|
|
return fallback;
|
|
};
|
|
|
|
function formatDateTime(iso?: string | null) {
|
|
if (!iso) return "—";
|
|
return new Date(iso).toLocaleString(undefined, {
|
|
month: "short",
|
|
day: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|
|
|
|
const CARD = {
|
|
background: T.surface,
|
|
border: `1px solid ${T.border}`,
|
|
borderRadius: 16,
|
|
overflow: "hidden" as const,
|
|
};
|
|
|
|
export default function TrainScheduleTrackPage() {
|
|
const { scheduleId } = useParams<{ scheduleId: string }>();
|
|
const { toast } = useToast();
|
|
const trackQuery = useQuery(
|
|
api.trainScheduling.trainTrack.queryOptions({
|
|
input: { id: scheduleId ?? "" },
|
|
enabled: Boolean(scheduleId),
|
|
}),
|
|
);
|
|
const recordCheckpoint = useMutation(
|
|
api.trainScheduling.recordCheckpoint.mutationOptions(),
|
|
);
|
|
const updateCheckpoint = useMutation(
|
|
api.trainScheduling.updateCheckpoint.mutationOptions(),
|
|
);
|
|
// Time-entry dialogs: logging a pass at a yard with no work (the yard-work
|
|
// modal carries its own picker), and correcting an already-logged leg.
|
|
const [logModal, setLogModal] = useState<{
|
|
station: TrackStation;
|
|
isFinal: boolean;
|
|
} | null>(null);
|
|
const [editModal, setEditModal] = useState<TrainCheckpoint | null>(null);
|
|
// Yard work drives the log-pass modal: which bookings board/alight per stop.
|
|
const yardWorkQuery = useQuery(
|
|
api.trainScheduling.yardWork.queryOptions({
|
|
input: { scheduleId: scheduleId ?? "" },
|
|
enabled: Boolean(scheduleId) && trackQuery.data?.status === "DISPATCHED",
|
|
}),
|
|
);
|
|
// Marshalling: the on-board list, reprinted after station work. Numbered
|
|
// per corridor stop that actually coupled/uncoupled something (Marshalling
|
|
// 2, 3, 4…) — falls back to the single "current position" doc when nothing
|
|
// has happened yet.
|
|
const marshallingStopsQuery = useQuery(
|
|
api.trainScheduling.marshallingStops.queryOptions({
|
|
input: { id: scheduleId ?? "" },
|
|
enabled:
|
|
Boolean(scheduleId) &&
|
|
["DISPATCHED", "ARRIVED"].includes(trackQuery.data?.status ?? ""),
|
|
}),
|
|
);
|
|
const marshallingStops = marshallingStopsQuery.data ?? [];
|
|
const intercityMarshalling = useMutation({
|
|
mutationFn: (stopIndex?: number) =>
|
|
stopIndex != null
|
|
? trainSchedulingService.downloadMarshallingDocumentAt(scheduleId ?? "", stopIndex)
|
|
: trainSchedulingService.downloadIntercityMarshallingDocument(scheduleId ?? ""),
|
|
});
|
|
const openIntercityMarshalling = async (stopIndex?: number) => {
|
|
const pdfWindow = window.open("", "_blank");
|
|
try {
|
|
const blob = await intercityMarshalling.mutateAsync(stopIndex);
|
|
const filename =
|
|
stopIndex != null ? `marshalling-${stopIndex}-${scheduleId}.pdf` : `intercity-marshalling-${scheduleId}.pdf`;
|
|
const opened = openPdfBlob(blob, filename, pdfWindow);
|
|
toast({
|
|
title: stopIndex != null ? `Marshalling ${stopIndex} ready` : "Intercity marshalling ready",
|
|
description: opened
|
|
? "The PDF opened in a browser tab for printing or saving."
|
|
: "The browser blocked the preview tab, so the PDF was downloaded.",
|
|
});
|
|
} catch (error) {
|
|
pdfWindow?.close();
|
|
toast({
|
|
title: "Could not open intercity marshalling document",
|
|
description: parseError(error, "Please try again"),
|
|
variant: "destructive",
|
|
});
|
|
}
|
|
};
|
|
const [yardModal, setYardModal] = useState<{
|
|
station: TrackStation;
|
|
isFinal: boolean;
|
|
alreadyLogged: boolean;
|
|
} | null>(null);
|
|
|
|
if (trackQuery.isLoading) {
|
|
return (
|
|
<Group justify="center" py="xl">
|
|
<Loader size="sm" color="edr-green" />
|
|
</Group>
|
|
);
|
|
}
|
|
|
|
const track = trackQuery.data;
|
|
if (!track || !scheduleId) {
|
|
return (
|
|
<Text c="dimmed" py="xl" px="lg">
|
|
Tracking data not found.
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
const canLog = track.status === "DISPATCHED";
|
|
const totalStations = track.stations.length;
|
|
const reached = Math.min(track.currentSequenceNo + 1, totalStations);
|
|
const progressPct =
|
|
totalStations > 1 ? (track.currentSequenceNo / (totalStations - 1)) * 100 : 0;
|
|
const clampedPct = Math.min(100, Math.max(0, progressPct));
|
|
const currentStation =
|
|
track.stations[Math.max(0, track.currentSequenceNo)]?.label ?? "—";
|
|
const inTransit = track.status === "DISPATCHED";
|
|
const arrived = track.status === "ARRIVED";
|
|
|
|
// Yard work at a station: boarders not yet loaded, and loaded bookings that
|
|
// alight there. When either exists, logging the pass goes through the modal
|
|
// so the operator sees (and can act on) both lists; empty yards log directly.
|
|
const yardWorkFor = (station: TrackStation | undefined) =>
|
|
yardWorkQuery.data?.yards.find((y) => y.yardId === station?.yardId);
|
|
const stationHasWork = (station: TrackStation | undefined) => {
|
|
const yard = yardWorkFor(station);
|
|
return Boolean(
|
|
yard &&
|
|
(yard.toLoad.some((r) => !r.loadedAt) || yard.toUnload.some((r) => r.canUnload)),
|
|
);
|
|
};
|
|
|
|
const handleLog = (sequenceNo: number) => {
|
|
const station = track.stations.find((s) => s.sequenceNo === sequenceNo);
|
|
if (!station) return;
|
|
const isFinal = sequenceNo === track.stations[totalStations - 1]?.sequenceNo;
|
|
if (stationHasWork(station)) {
|
|
setYardModal({ station, isFinal, alreadyLogged: false });
|
|
return;
|
|
}
|
|
setLogModal({ station, isFinal });
|
|
};
|
|
|
|
const submitLog = (values: CheckpointModalValues) => {
|
|
if (!logModal) return;
|
|
const { station, isFinal } = logModal;
|
|
recordCheckpoint.mutate(
|
|
{
|
|
id: scheduleId,
|
|
payload: {
|
|
sequenceNo: station.sequenceNo,
|
|
occurredAt: values.occurredAt,
|
|
...(values.note ? { note: values.note } : {}),
|
|
// Nothing to clear on a first log — send only what was entered.
|
|
...pickHandling(values, false),
|
|
},
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
setLogModal(null);
|
|
toast({
|
|
title: isFinal
|
|
? "Train arrived — assets freed, moved to destination yard"
|
|
: "Checkpoint logged",
|
|
});
|
|
},
|
|
onError: (err) =>
|
|
toast({
|
|
title: "Could not log checkpoint",
|
|
description: parseError(err, "Please try again"),
|
|
variant: "destructive",
|
|
}),
|
|
},
|
|
);
|
|
};
|
|
|
|
const submitEdit = (values: CheckpointModalValues) => {
|
|
if (!editModal) return;
|
|
updateCheckpoint.mutate(
|
|
{
|
|
id: scheduleId,
|
|
sequenceNo: editModal.sequenceNo,
|
|
payload: {
|
|
occurredAt: values.occurredAt,
|
|
note: values.note || null,
|
|
// Nulls are meaningful here: clearing a picker clears the stamp.
|
|
...pickHandling(values, true),
|
|
},
|
|
},
|
|
{
|
|
onSuccess: () => {
|
|
setEditModal(null);
|
|
toast({ title: "Checkpoint updated" });
|
|
},
|
|
onError: (err) =>
|
|
toast({
|
|
title: "Could not update checkpoint",
|
|
description: parseError(err, "Please try again"),
|
|
variant: "destructive",
|
|
}),
|
|
},
|
|
);
|
|
};
|
|
// Legs stay correctable for as long as the journey exists — while rolling
|
|
// and after arrival.
|
|
const canEdit = track.status === "DISPATCHED" || track.status === "ARRIVED";
|
|
|
|
// "Forgot to load" catch: while the train sits at the current station, any
|
|
// boarder there that is still unloaded can be loaded until the next pass.
|
|
const currentStationObj = track.stations.find(
|
|
(s) => s.sequenceNo === track.currentSequenceNo,
|
|
);
|
|
const currentYard = canLog ? yardWorkFor(currentStationObj) : undefined;
|
|
const forgottenBoarders =
|
|
currentYard?.toLoad.filter((r) => !r.loadedAt) ?? [];
|
|
|
|
// The stop the operator acts on next — drives the left rail's action card.
|
|
const nextStation = canLog
|
|
? track.stations.find((s) => s.sequenceNo === track.currentSequenceNo + 1)
|
|
: undefined;
|
|
const nextIsFinal =
|
|
nextStation?.sequenceNo === track.stations[totalStations - 1]?.sequenceNo;
|
|
|
|
return (
|
|
<Box style={{ background: T.bg, minHeight: "100%" }}>
|
|
{/* ── Top bar ── */}
|
|
<Group
|
|
gap={14}
|
|
px={36}
|
|
py={16}
|
|
wrap="nowrap"
|
|
align="center"
|
|
style={{ background: T.surface, borderBottom: `1px solid ${T.border}` }}
|
|
>
|
|
<Button
|
|
component={Link}
|
|
to={`/dashboard/operations/train-scheduling-v2/${scheduleId}`}
|
|
variant="default"
|
|
radius={9}
|
|
size="compact-sm"
|
|
leftSection={<ArrowLeft size={15} />}
|
|
>
|
|
Back to schedule
|
|
</Button>
|
|
<Group gap={8} align="center" wrap="nowrap" visibleFrom="sm">
|
|
<Text size="12.5px" c={T.muted}>
|
|
Train scheduling
|
|
</Text>
|
|
<ChevronRight size={13} color={T.text3} />
|
|
<Text size="12.5px" fw={600} c={T.text}>
|
|
{track.trainNumber ?? "Schedule"} · Tracking
|
|
</Text>
|
|
</Group>
|
|
<Box style={{ flex: 1 }} />
|
|
{(inTransit || arrived) && marshallingStops.length === 0 ? (
|
|
<Button
|
|
variant="default"
|
|
radius={9}
|
|
size="compact-sm"
|
|
leftSection={<FileText size={15} color={T.brand} />}
|
|
loading={intercityMarshalling.isPending}
|
|
onClick={() => void openIntercityMarshalling()}
|
|
>
|
|
Intercity Marshalling
|
|
</Button>
|
|
) : null}
|
|
{(inTransit || arrived) && marshallingStops.length > 0 ? (
|
|
<Menu position="bottom-end" withinPortal>
|
|
<Menu.Target>
|
|
<Button
|
|
variant="default"
|
|
radius={9}
|
|
size="compact-sm"
|
|
leftSection={<FileText size={15} color={T.brand} />}
|
|
loading={intercityMarshalling.isPending}
|
|
>
|
|
Marshalling
|
|
</Button>
|
|
</Menu.Target>
|
|
<Menu.Dropdown>
|
|
{marshallingStops.map((stop) => (
|
|
<Menu.Item
|
|
key={stop.stopIndex}
|
|
onClick={() => void openIntercityMarshalling(stop.stopIndex)}
|
|
>
|
|
{`Marshalling ${stop.stopIndex} — ${stop.yardLabel}`}
|
|
</Menu.Item>
|
|
))}
|
|
</Menu.Dropdown>
|
|
</Menu>
|
|
) : null}
|
|
</Group>
|
|
|
|
{/* ── Two-column work surface ── */}
|
|
<Group
|
|
align="flex-start"
|
|
gap={28}
|
|
px={36}
|
|
pt={28}
|
|
pb={56}
|
|
wrap="wrap"
|
|
style={{ width: "100%" }}
|
|
>
|
|
{/* left rail */}
|
|
<Stack gap={16} style={{ width: 352, flexShrink: 0, flexGrow: 1, maxWidth: "100%" }}>
|
|
<TrackStatusCard
|
|
trainNumber={track.trainNumber}
|
|
direction={track.direction}
|
|
status={track.status}
|
|
progressPct={clampedPct}
|
|
reached={reached}
|
|
totalStations={totalStations}
|
|
currentStation={currentStation}
|
|
stateLine={
|
|
arrived
|
|
? "Journey complete"
|
|
: inTransit
|
|
? "En route"
|
|
: "Awaiting dispatch"
|
|
}
|
|
origin={track.origin}
|
|
destination={track.destination}
|
|
stats={[
|
|
{
|
|
icon: CalendarClock,
|
|
label: "Departed",
|
|
value: formatDateTime(track.actualDepartureAt),
|
|
},
|
|
{
|
|
icon: Flag,
|
|
label: "Arrived",
|
|
value: formatDateTime(track.actualArrivalAt),
|
|
},
|
|
{ icon: MapPin, label: "Current station", value: currentStation },
|
|
{
|
|
icon: TrainFront,
|
|
label: "Stations reached",
|
|
value: `${reached} of ${totalStations}`,
|
|
},
|
|
]}
|
|
/>
|
|
|
|
{/* next action */}
|
|
{nextStation ? (
|
|
<Stack gap={14} p={18} style={CARD}>
|
|
<Group gap={9} align="center" wrap="nowrap">
|
|
<Text
|
|
size="9.5px"
|
|
fw={700}
|
|
tt="uppercase"
|
|
c={T.muted}
|
|
style={{ letterSpacing: 1 }}
|
|
>
|
|
Next action
|
|
</Text>
|
|
<Box style={{ flex: 1 }} />
|
|
<Chip bg={T.surface3} fg={T.text2}>
|
|
{`STOP ${reached + 1} OF ${totalStations}`}
|
|
</Chip>
|
|
</Group>
|
|
<Text size="15px" fw={700} c={T.text} lh={1.3}>
|
|
{nextIsFinal
|
|
? `Mark arrived at ${nextStation.label}`
|
|
: `Log pass at ${nextStation.label}`}
|
|
</Text>
|
|
<Text size="12px" c={T.text2} lh={1.45}>
|
|
{nextIsFinal
|
|
? "Marks the train arrived: remaining bookings arrive, assets are freed."
|
|
: "Logging the pass marks arriving bookings and unlocks loading for cargo boarding here."}
|
|
</Text>
|
|
<Group gap={8} wrap="nowrap">
|
|
<Button
|
|
color="edr-green"
|
|
radius={9}
|
|
size="compact-sm"
|
|
style={{ flex: 1 }}
|
|
leftSection={nextIsFinal ? <Flag size={14} /> : <MapPin size={14} />}
|
|
loading={
|
|
recordCheckpoint.isPending &&
|
|
recordCheckpoint.variables?.payload.sequenceNo ===
|
|
nextStation.sequenceNo
|
|
}
|
|
onClick={() => handleLog(nextStation.sequenceNo)}
|
|
>
|
|
{nextIsFinal ? "Mark arrived" : "Log pass"}
|
|
</Button>
|
|
<Button
|
|
variant="default"
|
|
radius={9}
|
|
size="compact-sm"
|
|
leftSection={<Package size={14} />}
|
|
onClick={() =>
|
|
setYardModal({
|
|
station: nextStation,
|
|
isFinal: Boolean(nextIsFinal),
|
|
alreadyLogged: false,
|
|
})
|
|
}
|
|
>
|
|
Yard work
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
) : null}
|
|
|
|
{/* forgotten boarders */}
|
|
{currentStationObj && forgottenBoarders.length > 0 ? (
|
|
<Stack
|
|
gap={11}
|
|
p={16}
|
|
style={{
|
|
background: T.amberDim,
|
|
border: `1px solid ${T.amberBorder}`,
|
|
borderRadius: 14,
|
|
}}
|
|
>
|
|
<Group gap={9} align="center" wrap="nowrap">
|
|
<PackageCheck size={16} color={T.amber} style={{ flexShrink: 0 }} />
|
|
<Text size="13px" fw={700} c={T.amber}>
|
|
{forgottenBoarders.length} booking
|
|
{forgottenBoarders.length === 1 ? "" : "s"} not loaded
|
|
</Text>
|
|
</Group>
|
|
<Text size="11.5px" c={T.amberText} lh={1.45}>
|
|
The train is at {currentStationObj.label} — cargo boarding here can still
|
|
be loaded before the next station is logged.
|
|
</Text>
|
|
<Button
|
|
size="compact-sm"
|
|
radius={9}
|
|
variant="white"
|
|
w="fit-content"
|
|
styles={{
|
|
root: { borderColor: T.amberBorder, border: `1px solid ${T.amberBorder}` },
|
|
label: { color: T.amber, fontWeight: 700, fontSize: 12.5 },
|
|
}}
|
|
onClick={() =>
|
|
setYardModal({
|
|
station: currentStationObj,
|
|
isFinal:
|
|
currentStationObj.sequenceNo ===
|
|
track.stations[totalStations - 1]?.sequenceNo,
|
|
alreadyLogged: true,
|
|
})
|
|
}
|
|
>
|
|
Open yard work
|
|
</Button>
|
|
</Stack>
|
|
) : null}
|
|
</Stack>
|
|
|
|
{/* main column */}
|
|
<Stack gap={20} style={{ flex: 1, minWidth: 520 }}>
|
|
<Box style={CARD}>
|
|
<SectionHead
|
|
icon={<Route size={17} />}
|
|
title="Journey & station work"
|
|
hint={
|
|
canLog
|
|
? "Every stop with its pass time and loading windows — the final station marks arrival."
|
|
: arrived
|
|
? "This train has arrived at its destination."
|
|
: "Tracking becomes available once the train is dispatched."
|
|
}
|
|
right={
|
|
<Group gap={12} wrap="nowrap" visibleFrom="md">
|
|
{[
|
|
[T.brand, "Passed"],
|
|
[T.amber, "Active"],
|
|
[T.text3, "Upcoming"],
|
|
].map(([color, label]) => (
|
|
<Group key={label} gap={5} wrap="nowrap">
|
|
<Box
|
|
style={{
|
|
width: 7,
|
|
height: 7,
|
|
borderRadius: 999,
|
|
background: color,
|
|
}}
|
|
/>
|
|
<Text size="11px" fw={600} c={T.muted}>
|
|
{label}
|
|
</Text>
|
|
</Group>
|
|
))}
|
|
</Group>
|
|
}
|
|
/>
|
|
<JourneySpine
|
|
scheduleId={scheduleId}
|
|
stations={track.stations}
|
|
currentSequenceNo={track.currentSequenceNo}
|
|
checkpoints={track.checkpoints}
|
|
stationWorkLogs={track.stationWorkLogs}
|
|
canLog={canLog}
|
|
loggingSeq={
|
|
recordCheckpoint.isPending
|
|
? recordCheckpoint.variables?.payload.sequenceNo
|
|
: null
|
|
}
|
|
onLogCheckpoint={handleLog}
|
|
onEditCheckpoint={canEdit ? setEditModal : undefined}
|
|
/>
|
|
</Box>
|
|
|
|
<Box style={CARD}>
|
|
<SectionHead
|
|
icon={<ListChecks size={16} />}
|
|
title="Checkpoint log"
|
|
hint="Raw event trail — every logged pass with its correction history"
|
|
right={
|
|
<Chip bg={T.surface3} fg={T.text2}>
|
|
{`${track.checkpoints.length} EVENT${
|
|
track.checkpoints.length === 1 ? "" : "S"
|
|
}`}
|
|
</Chip>
|
|
}
|
|
/>
|
|
<CheckpointLogTable
|
|
checkpoints={track.checkpoints}
|
|
onEdit={canEdit ? setEditModal : undefined}
|
|
/>
|
|
</Box>
|
|
</Stack>
|
|
</Group>
|
|
|
|
<CheckpointTimeModal
|
|
opened={logModal !== null}
|
|
onClose={() => setLogModal(null)}
|
|
title={
|
|
logModal?.isFinal
|
|
? `Mark arrived at ${logModal.station.label}`
|
|
: `Log pass at ${logModal?.station.label ?? "station"}`
|
|
}
|
|
icon={logModal?.isFinal ? <Flag size={18} /> : <MapPin size={18} />}
|
|
description={
|
|
logModal?.isFinal
|
|
? "Marks the train arrived: remaining bookings arrive, assets are freed."
|
|
: undefined
|
|
}
|
|
submitLabel={logModal?.isFinal ? "Mark arrived" : "Log pass"}
|
|
submitColor={logModal?.isFinal ? "teal" : "edr-green"}
|
|
loading={recordCheckpoint.isPending}
|
|
onSubmit={submitLog}
|
|
/>
|
|
|
|
<CheckpointTimeModal
|
|
opened={editModal !== null}
|
|
onClose={() => setEditModal(null)}
|
|
title={`Edit ${editModal?.label ?? "checkpoint"}`}
|
|
icon={<Pencil size={18} />}
|
|
description="Corrects this leg's time, station work and note only — nothing else changes."
|
|
initialOccurredAt={editModal?.occurredAt}
|
|
initialNote={editModal?.note}
|
|
initialHandling={editModal}
|
|
submitLabel="Save"
|
|
loading={updateCheckpoint.isPending}
|
|
onSubmit={submitEdit}
|
|
/>
|
|
|
|
<LogPassYardWorkModal
|
|
opened={yardModal !== null}
|
|
onClose={() => setYardModal(null)}
|
|
scheduleId={scheduleId}
|
|
station={yardModal?.station ?? null}
|
|
stations={track.stations}
|
|
isFinal={yardModal?.isFinal ?? false}
|
|
alreadyLogged={yardModal?.alreadyLogged ?? false}
|
|
/>
|
|
</Box>
|
|
);
|
|
}
|