From 065bdd0189f5a013e38380a5e77cf14f74d89056 Mon Sep 17 00:00:00 2001 From: Marshal Date: Thu, 27 Aug 2026 07:45:32 +0000 Subject: [PATCH] tracking ui --- .../trainScheduling/CheckpointLogTable.tsx | 154 ++++ .../trainScheduling/CheckpointTimeModal.tsx | 5 +- .../trainScheduling/JourneySpine.tsx | 266 ++++++ .../trainScheduling/LogPassYardWorkModal.tsx | 110 ++- .../trainScheduling/StationWorkControls.tsx | 173 ++-- .../trainScheduling/TrackStatusCard.tsx | 205 +++++ .../trainScheduling/trackPrimitives.tsx | 149 ++++ .../components/trainScheduling/trackTheme.ts | 69 ++ .../TrainScheduleTrackPage.tsx | 814 ++++++------------ 9 files changed, 1275 insertions(+), 670 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointLogTable.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/trainScheduling/JourneySpine.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/trainScheduling/TrackStatusCard.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/trainScheduling/trackPrimitives.tsx create mode 100644 apps/edr-freight-web/backoffice/src/components/trainScheduling/trackTheme.ts diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointLogTable.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointLogTable.tsx new file mode 100644 index 000000000..1b3ff148e --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointLogTable.tsx @@ -0,0 +1,154 @@ +import { Box, Button, Group, Stack, Table, Text } from "@mantine/core"; +import { MapPin, Pencil } from "lucide-react"; + +import type { TrainCheckpoint } from "@/types/trainScheduling"; +import { handlingHours } from "./JourneySpine"; +import { Chip } from "./trackPrimitives"; +import { KIND_TONE, track } from "./trackTheme"; + +const fmt = (iso: string) => + new Date(iso).toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); + +const TH = { + fontSize: 9.5, + fontWeight: 700, + letterSpacing: 0.7, + color: track.muted, + textTransform: "uppercase", +} as const; + +/** Raw event trail under the spine — every logged pass, with its correction. */ +export function CheckpointLogTable({ + checkpoints, + onEdit, +}: { + checkpoints: TrainCheckpoint[]; + onEdit?: (checkpoint: TrainCheckpoint) => void; +}) { + if (checkpoints.length === 0) { + return ( + + + + + + No checkpoints yet + + + Each station the train passes will be logged here with its timestamp. + + + ); + } + + return ( + + + + + Station + Event + Time + Handling + Note + + + + + {checkpoints.map((cp) => { + const hours = handlingHours(cp); + const tone = KIND_TONE[cp.kind] ?? KIND_TONE.PASSED; + return ( + + + + + + + + {cp.label ?? `Station ${cp.sequenceNo}`} + + + + + + {cp.kind} + + + + + {fmt(cp.occurredAt)} + + + + + {hours === null ? "—" : `${hours} h`} + + + + + {cp.note || "—"} + + + + {onEdit ? ( + + + + ) : null} + + + ); + })} + +
+
+ ); +} diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointTimeModal.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointTimeModal.tsx index 1f8f915de..e6df245b0 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointTimeModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/CheckpointTimeModal.tsx @@ -98,7 +98,7 @@ export function CheckpointTimeModal({ onClose={onClose} centered fullScreen={isSmallScreen} - radius="lg" + radius={18} title={ {icon} @@ -137,10 +137,11 @@ export function CheckpointTimeModal({ radius="md" /> - + ) : null} + + ) : isNext ? ( + + ) : ( + + )} + + + {hours !== null || checkpoint?.note ? ( + + {hours !== null ? ( + <> + + + {hours} h handling + + + ) : null} + {hours !== null && checkpoint?.note ? ( + + ) : null} + {checkpoint?.note ? ( + + {checkpoint.note} + + ) : null} + + ) : null} + + {/* the station's work windows, inline */} + + {!isFirst ? ( + + ) : null} + {!isLast ? ( + + ) : null} + + + + ); + })} + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/LogPassYardWorkModal.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/LogPassYardWorkModal.tsx index 56da3f1ab..d09cf06c8 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/LogPassYardWorkModal.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/LogPassYardWorkModal.tsx @@ -1,6 +1,5 @@ import { Alert, - Badge, Button, Divider, Group, @@ -26,6 +25,8 @@ import { Freight } from "@edr/types"; import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge"; import { StationWorkControls } from "@/components/trainScheduling/StationWorkControls"; +import { Chip } from "./trackPrimitives"; +import { DIRECTION_TONE, track as T } from "./trackTheme"; import { useAuth } from "@/auth/useAuth"; import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "@/lib/permissions"; import { useToast } from "@/hooks/use-toast"; @@ -44,20 +45,15 @@ const fmtDate = (iso: string) => { return Number.isNaN(d.getTime()) ? iso : d.toLocaleString(); }; -const DIRECTION_COLORS: Record = { - IMPORT: "blue", - EXPORT: "teal", - DOMESTIC: "violet", -}; - /** DOMESTIC displays as "Intercity" — shared with the rest of the platform. */ const DIRECTION_LABELS: Record = Freight.TRADE_DIRECTION_LABELS; function DirectionChip({ direction }: { direction: string }) { + const tone = DIRECTION_TONE[direction] ?? { bg: T.surface3, fg: T.muted }; return ( - + {DIRECTION_LABELS[direction] ?? direction} - + ); } @@ -72,15 +68,15 @@ function SectionLabel({ }) { return ( - + {icon} - + {title} - - {count} - + + {String(count)} + ); } @@ -263,7 +259,7 @@ export function LogPassYardWorkModal({ opened={opened} onClose={onClose} size="xl" - radius="lg" + radius={18} title={ {isFinal ? : } @@ -271,9 +267,9 @@ export function LogPassYardWorkModal({ {isFinal ? "Arrival" : "Yard work"} — {station?.label ?? ""} {logged ? ( - - {isFinal ? "Arrived" : "Pass logged"} - + + {isFinal ? "ARRIVED" : "PASS LOGGED"} + ) : null} } @@ -316,7 +312,20 @@ export function LogPassYardWorkModal({ ) : null} - +
Booking @@ -331,12 +340,12 @@ export function LogPassYardWorkModal({ {arrivals.map((row) => ( - + {row.reference ?? row.id.slice(0, 8)} - {row.customer} + {row.customer} @@ -364,6 +373,7 @@ export function LogPassYardWorkModal({ >
+
Booking @@ -433,18 +456,18 @@ export function LogPassYardWorkModal({ - + {row.reference ?? row.id.slice(0, 8)} {row.isGovernment ? ( - + GOV - + ) : null} - {row.customer} + {row.customer} @@ -487,7 +510,8 @@ export function LogPassYardWorkModal({ > {!logged ? ( - r.canUnload))} + // Arrival comes BEFORE unloading: the train is marked arrived + // whenever it physically gets there, and the unloading window + // opens afterwards. Bookings then unload per booking inside the + // started window (the buttons above enforce that). + - + {isFinal + ? `Mark arrived at ${station?.label ?? "destination"}` + : `Log pass at ${station?.label ?? "station"}`} + ) : null} diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/StationWorkControls.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/StationWorkControls.tsx index 96b524551..50e57b955 100644 --- a/apps/edr-freight-web/backoffice/src/components/trainScheduling/StationWorkControls.tsx +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/StationWorkControls.tsx @@ -1,13 +1,4 @@ -import { - ActionIcon, - Badge, - Button, - Group, - Popover, - Stack, - Text, - Tooltip, -} from "@mantine/core"; +import { ActionIcon, Box, Button, Group, Popover, Stack, Text, Tooltip } from "@mantine/core"; import { DateTimePicker } from "@mantine/dates"; import { useMutation } from "@tanstack/react-query"; import { Pencil, PlayCircle, StopCircle } from "lucide-react"; @@ -18,6 +9,8 @@ import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions"; import { useToast } from "@/hooks/use-toast"; import { api } from "@/services/api"; import type { StationWorkPhaseLog } from "@/types/trainScheduling"; +import { Chip, PhaseChip, phaseState } from "./trackPrimitives"; +import { track } from "./trackTheme"; const parseError = (error: unknown, fallback: string) => { const message = (error as { response?: { data?: { message?: string | string[] } } }) @@ -28,7 +21,14 @@ const parseError = (error: unknown, fallback: string) => { const fmtTime = (iso: string) => { const d = new Date(iso); - return Number.isNaN(d.getTime()) ? iso : d.toLocaleString(); + return Number.isNaN(d.getTime()) + ? iso + : d.toLocaleString(undefined, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); }; const fmtElapsed = (fromIso: string, toIso?: string | null) => { @@ -71,9 +71,9 @@ function EditTimeButton({ setOpened((o) => !o)} > @@ -100,6 +100,7 @@ function EditTimeButton({ - + + + ) : ( <> - - - {fmtTime(log!.startedAt!)} → {ended ? fmtTime(log!.endedAt!) : "…"} ( - {fmtElapsed(log!.startedAt!, log?.endedAt)}) - - {log?.startedByName || log?.endedByName ? ( - - - {log?.endedByName ?? log?.startedByName} - - - ) : null} + + {fmtTime(log!.startedAt!)} → {ended ? fmtTime(log!.endedAt!) : "…"} + + + {fmtElapsed(log!.startedAt!, log?.endedAt)} + + {who ? ( + + + {who} + + + ) : null} + + + + doRecord("start", at)} + saving={record.isPending} + /> + {ended ? ( doRecord("start", at)} + label={`${phase} end`} + value={log!.endedAt!} + disabled={!canEnd} + disabledReason={`You don't have permission to edit the ${phase} end`} + minDate={new Date(log!.startedAt!)} + onSave={(at) => doRecord("end", at)} saving={record.isPending} /> - {ended ? ( - doRecord("end", at)} - saving={record.isPending} - /> - ) : null} - - {!ended ? ( + ) : ( - ) : null} + )} )} diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/TrackStatusCard.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/TrackStatusCard.tsx new file mode 100644 index 000000000..7feec2c59 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/TrackStatusCard.tsx @@ -0,0 +1,205 @@ +import { Box, Group, RingProgress, Stack, Text } from "@mantine/core"; +import { ArrowRight, CircleDot, Flag, Navigation } from "lucide-react"; +import type { LucideIcon } from "lucide-react"; + +import { statusMeta } from "@/components/trainScheduling/scheduleVisuals"; +import { Chip } from "./trackPrimitives"; +import { track } from "./trackTheme"; + +export interface TrackStatValue { + icon: LucideIcon; + label: string; + value: string; +} + +/** + * Left-rail identity card: gradient cap (train number, progress ring, status, + * current station) over the route strip and the stat list. + */ +export function TrackStatusCard({ + trainNumber, + direction, + status, + progressPct, + reached, + totalStations, + currentStation, + stateLine, + origin, + destination, + stats, +}: { + trainNumber?: string | null; + direction?: string | null; + status: string; + progressPct: number; + reached: number; + totalStations: number; + currentStation: string; + stateLine: string; + origin: string | null; + destination: string | null; + stats: TrackStatValue[]; +}) { + return ( + + + + + + + + + {trainNumber ?? "Train tracking"} + + + Train tracking + + + {direction ? ( + + {direction} + + ) : null} + + + + + + {Math.round(progressPct)}% + + + {reached}/{totalStations} stops + + + } + /> + + + + + {status} + + + + {stateLine} + + + {currentStation} + + + + + + + + + {origin ?? "—"} + + + + + + {destination ?? "—"} + + + + + + {stats.map((s, i) => { + const Icon = s.icon; + return ( + + + + {s.label} + + + {s.value} + + + ); + })} + + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/trackPrimitives.tsx b/apps/edr-freight-web/backoffice/src/components/trainScheduling/trackPrimitives.tsx new file mode 100644 index 000000000..564fe4bcd --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/trackPrimitives.tsx @@ -0,0 +1,149 @@ +import { Box, Group, Stack, Text } from "@mantine/core"; +import type { ReactNode } from "react"; + +import type { StationWorkPhaseLog } from "@/types/trainScheduling"; +import { PHASE_TONE, track, type PhaseState } from "./trackTheme"; + +/** Small uppercase tag — the design's one chip shape, tinted per use. */ +export function Chip({ + children, + bg, + fg, + border, +}: { + children: ReactNode; + bg: string; + fg: string; + border?: string; +}) { + return ( + + {children} + + ); +} + +/** Card header: tinted icon chip + title + one-line hint, optional right slot. */ +export function SectionHead({ + icon, + title, + hint, + right, +}: { + icon: ReactNode; + title: string; + hint: string; + right?: ReactNode; +}) { + return ( + + + {icon} + + + + {title} + + + {hint} + + + {right} + + ); +} + +/** Which of the three window states a phase log is in. */ +export function phaseState(log?: StationWorkPhaseLog | null): PhaseState { + if (log?.endedAt) return "done"; + if (log?.startedAt) return "active"; + return "idle"; +} + +export function phaseChipLabel( + phase: "loading" | "unloading", + state: PhaseState, +) { + const title = phase === "loading" ? "Loading" : "Unloading"; + const suffix = + state === "done" + ? "done" + : state === "active" + ? "in progress" + : "not started"; + return `${title} ${suffix}`; +} + +export function PhaseChip({ + phase, + state, +}: { + phase: "loading" | "unloading"; + state: PhaseState; +}) { + const tone = PHASE_TONE[state]; + return ( + + + + {phaseChipLabel(phase, state)} + + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/components/trainScheduling/trackTheme.ts b/apps/edr-freight-web/backoffice/src/components/trainScheduling/trackTheme.ts new file mode 100644 index 000000000..f9eb97485 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/trainScheduling/trackTheme.ts @@ -0,0 +1,69 @@ +/** + * Design tokens for the train-tracking surface, mirroring ui/scheule/track.pen. + * + * The rest of the scheduling pages key off `scheduleVisuals`/`freightBrand`; + * tracking is its own light "work surface" palette, so the tokens live here + * rather than widening the shared brand file. Brand green is darkened from the + * shared #1B9E7A to #0E8C68 so label text clears AA contrast on white. + */ +export const track = { + bg: "#F6F8FA", + surface: "#FFFFFF", + surface2: "#F4F7F9", + surface3: "#E9EEF3", + border: "#DCE4EC", + borderSoft: "#E8EDF2", + brand: "#0E8C68", + brandDark: "#0A6B50", + brandLight: "#12A87D", + brandDim: "#E4F5EF", + text: "#0F1D2B", + text2: "#48606F", + text3: "#9BAEBE", + muted: "#6A8296", + teal: "#0E8C82", + tealDim: "#DFF3F1", + blue: "#2563C9", + blueDim: "#E4EDFB", + amber: "#A66A08", + amberDim: "#FDF2DC", + amberBorder: "#E8C88C", + amberText: "#8A6420", + red: "#C43D3D", + redDim: "#FBE9E9", + grape: "#7C4BC4", + grapeDim: "#F0E7FB", + /** Status-cap wash on the left rail's identity card. */ + capGradient: + "linear-gradient(115deg, #0A6B50 0%, #0E8C68 55%, #12A87D 100%)", + mono: "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace", +} as const; + +/** Checkpoint-kind chip colors, keyed by TrainCheckpointKind. */ +export const KIND_TONE: Record = { + DEPARTED: { bg: track.blueDim, fg: track.blue }, + PASSED: { bg: track.brandDim, fg: track.brand }, + ARRIVED: { bg: track.tealDim, fg: track.teal }, +}; + +/** Loading/unloading window state chips. */ +export const PHASE_TONE = { + done: { bg: track.surface3, fg: track.muted }, + active: { bg: track.amberDim, fg: track.amber }, + idle: { bg: track.surface2, fg: track.text3 }, +} as const; + +export type PhaseState = keyof typeof PHASE_TONE; + +/** Trade-direction chips in the yard-work tables. */ +export const DIRECTION_TONE: Record = { + IMPORT: { bg: track.blueDim, fg: track.blue }, + EXPORT: { bg: track.tealDim, fg: track.teal }, + DOMESTIC: { bg: track.grapeDim, fg: track.grape }, +}; + +export const cardStyle = { + background: track.surface, + border: `1px solid ${track.border}`, + borderRadius: 16, +} as const; diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx index 79d8f6c56..3d74c0fb5 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleTrackPage.tsx @@ -4,48 +4,31 @@ import { useState } from "react"; import { ArrowLeft, CalendarClock, - CheckCircle2, - Clock, + ChevronRight, FileText, Flag, + ListChecks, MapPin, - Navigation, + Package, PackageCheck, Pencil, - Train, + Route, + TrainFront, } from "lucide-react"; -import { StationWorkControls } from "@/components/trainScheduling/StationWorkControls"; -import { - Alert, - Badge, - Box, - Button, - Group, - Loader, - Paper, - RingProgress, - Stack, - Text, - ThemeIcon, - Timeline, - Title, -} from "@mantine/core"; +import { Box, Button, Group, Loader, Stack, Text } from "@mantine/core"; -import { PageContainer } from "@/components/page"; 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 { RouteCorridorTrack } from "@/components/trainScheduling/RouteCorridorTrack"; +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 { - RouteCorridor, - StatusPill, - scheduleBrand, -} from "@/components/trainScheduling/scheduleVisuals"; -import { freightBrand } from "@/theme/freight-brand"; import { useMutation, useQuery } from "@tanstack/react-query"; import { api } from "@/services/api"; import { useToast } from "@/hooks/use-toast"; @@ -80,23 +63,6 @@ const pickHandling = ( .filter(([, value]) => keepNulls || value !== null), ); -/** - * Total loading and unloading at a stop, the way the reports measure it: - * earliest start to latest finish, so a stop that only loaded or only unloaded - * still reads. Null when nothing was logged. - */ -const handlingHours = (cp: TrainCheckpoint): number | null => { - const times = [cp.unloadingStartedAt, cp.loadingStartedAt] - .filter((v): v is string => Boolean(v)) - .map((v) => new Date(v).getTime()); - const ends = [cp.loadingCompletedAt, cp.unloadingCompletedAt] - .filter((v): v is string => Boolean(v)) - .map((v) => new Date(v).getTime()); - if (!times.length || !ends.length) return null; - const hours = (Math.max(...ends) - Math.min(...times)) / 3_600_000; - return Math.round(hours * 10) / 10; -}; - const parseError = (error: unknown, fallback: string) => { if (isAxiosError(error)) { const data = error.response?.data as Record | undefined; @@ -117,85 +83,12 @@ function formatDateTime(iso?: string | null) { }); } -/** - * A single fact in the hero's glass meta strip — icon chip + uppercase label + - * value, laid on the translucent panel over the gradient. - */ -function HeroStat({ - icon, - label, - value, -}: { - icon: React.ReactNode; - label: string; - value: string; -}) { - return ( - - - {icon} - - - - {label} - - - {value} - - - - ); -} - -/** Section header — icon chip + title + one-line hint. Shared by the cards. */ -function SectionHead({ - icon, - title, - hint, -}: { - icon: React.ReactNode; - title: string; - hint: string; -}) { - return ( - - - {icon} - - - - {title} - - - {hint} - - - - ); -} - -const CARD_STYLE = { - borderColor: scheduleBrand.mutedBorder, - boxShadow: scheduleBrand.shadowSm, -} as const; +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 }>(); @@ -259,22 +152,18 @@ export default function TrainScheduleTrackPage() { if (trackQuery.isLoading) { return ( - - - - - + + + ); } const track = trackQuery.data; if (!track || !scheduleId) { return ( - - - Tracking data not found. - - + + Tracking data not found. + ); } @@ -386,27 +275,50 @@ export default function TrainScheduleTrackPage() { 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 ( - - + + {/* ── Top bar ── */} + + + + Train scheduling + + + + {track.trainNumber ?? "Schedule"} · Tracking + + + {inTransit || arrived ? ( + - + + ) : null} + + {/* forgotten boarders */} + {currentStationObj && forgottenBoarders.length > 0 ? ( + + + + + {forgottenBoarders.length} booking + {forgottenBoarders.length === 1 ? "" : "s"} not loaded + + + + The train is at {currentStationObj.label} — cargo boarding here can still + be loaded before the next station is logged. + + + ) : null} - - {/* ── Loading / unloading windows per station ── */} - - } - title="Loading & unloading windows" - hint="Start and end each station's work window — times, duration and who recorded them" - /> - - {track.stations.map((s, i) => { - const isFirst = i === 0; - const isLast = i === track.stations.length - 1; - const workLog = track.stationWorkLogs?.[s.yardId]; - return ( - - - - {isLast ? : } - - - {s.label} - - {isFirst ? ( - - origin - - ) : null} - {isLast ? ( - - destination - - ) : null} - {track.currentSequenceNo === s.sequenceNo ? ( - - train here - - ) : null} - - - {!isLast ? ( - - ) : null} - {!isFirst ? ( - - ) : null} - - - ); - })} - - - - {/* ── Checkpoint log ── */} - - - } - title="Checkpoint log" - hint={`${track.checkpoints.length} event${ - track.checkpoints.length === 1 ? "" : "s" - } recorded`} - /> - - - {track.checkpoints.length === 0 ? ( - - - - - - No checkpoints yet - - - Each station the train passes will be logged here with its - timestamp. - - - ) : ( - - {track.checkpoints.map((cp) => ( - - ) : ( - - ) - } - title={ - - - - {cp.label ?? `Station ${cp.sequenceNo}`} + {/* main column */} + + + } + 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={ + + {[ + [T.brand, "Passed"], + [T.amber, "Active"], + [T.text3, "Upcoming"], + ].map(([color, label]) => ( + + + + {label} - - {cp.kind} - - {canEdit ? ( - - ) : null} - - } - > - - {formatDateTime(cp.occurredAt)} - - {handlingHours(cp) !== null ? ( - - Loading + unloading {handlingHours(cp)} h - - ) : null} - {cp.note ? ( - - {cp.note} - - ) : null} - - ))} - - )} - + ))} + + } + /> + + + + + } + title="Checkpoint log" + hint="Raw event trail — every logged pass with its correction history" + right={ + + {`${track.checkpoints.length} EVENT${ + track.checkpoints.length === 1 ? "" : "S" + }`} + + } + /> + + + + - + ); }