import { Link, useParams } from "react-router-dom"; import { isAxiosError } from "axios"; import { useState } from "react"; import { ArrowLeft, CalendarClock, CheckCircle2, FileText, Flag, MapPin, Navigation, PackageCheck, Pencil, Train, } from "lucide-react"; import { Alert, Badge, Box, Button, Group, Loader, Paper, RingProgress, Stack, Text, ThemeIcon, Timeline, Title, } from "@mantine/core"; import { PageContainer } from "@/components/page"; import { CheckpointTimeModal } from "@/components/trainScheduling/CheckpointTimeModal"; import { LogPassYardWorkModal } from "@/components/trainScheduling/LogPassYardWorkModal"; import { RouteCorridorTrack } from "@/components/trainScheduling/RouteCorridorTrack"; import type { 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"; import { openPdfBlob } from "@/components/warehouses/pdf"; import { trainSchedulingService } from "@/services/trainScheduling.service"; const parseError = (error: unknown, fallback: string) => { if (isAxiosError(error)) { const data = error.response?.data as Record | 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", }); } /** * 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; 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(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 2: the current on-board list, reprinted after station work. const intercityMarshalling = useMutation({ mutationFn: () => trainSchedulingService.downloadIntercityMarshallingDocument(scheduleId ?? ""), }); const openIntercityMarshalling = async () => { const pdfWindow = window.open("", "_blank"); try { const blob = await intercityMarshalling.mutateAsync(); const opened = openPdfBlob(blob, `intercity-marshalling-${scheduleId}.pdf`, pdfWindow); toast({ title: "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 ( ); } const track = trackQuery.data; if (!track || !scheduleId) { return ( Tracking data not found. ); } 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: { occurredAt: string; note: string }) => { if (!logModal) return; const { station, isFinal } = logModal; recordCheckpoint.mutate( { id: scheduleId, payload: { sequenceNo: station.sequenceNo, occurredAt: values.occurredAt, ...(values.note ? { note: values.note } : {}), }, }, { 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: { occurredAt: string; note: string }) => { if (!editModal) return; updateCheckpoint.mutate( { id: scheduleId, sequenceNo: editModal.sequenceNo, payload: { occurredAt: values.occurredAt, note: values.note || null }, }, { 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) ?? []; return ( {inTransit || arrived ? ( ) : null} {/* ── Hero: gradient wash, route + a bold progress ring woven together ── */} {/* soft decorative glow, purely artistic */} {/* left — identity + route */} Train tracking {track.trainNumber ? ( {track.trainNumber} ) : null} {track.direction ? ( {track.direction} ) : null} {arrived ? "Journey complete" : inTransit ? `En route · ${currentStation}` : "Awaiting dispatch"} {/* right — progress ring, the artistic focal point */} {Math.round(clampedPct)}% {reached}/{totalStations} stops } /> {/* glass meta strip below the wash */} } label="Current" value={currentStation} /> } label="Departed" value={formatDateTime(track.actualDepartureAt)} /> } label="Arrived" value={formatDateTime(track.actualArrivalAt)} /> } label="Stations" value={`${reached} of ${totalStations}`} /> {/* ── Route corridor ── */} } title="Route corridor" hint={ canLog ? "Log the train passing each station; the final station marks arrival." : arrived ? "This train has arrived at its destination." : "Tracking becomes available once the train is dispatched." } /> {/* Cargo the operator forgot: boarders at the CURRENT station stay loadable until the next pass is logged. */} {currentStationObj && forgottenBoarders.length > 0 ? ( } title={`${forgottenBoarders.length} booking${ forgottenBoarders.length === 1 ? "" : "s" } at ${currentStationObj.label} not loaded yet`} > The train is at {currentStationObj.label} — cargo boarding here can still be loaded before the next station is logged. ) : 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}`} {cp.kind} {canEdit ? ( ) : null} } > {formatDateTime(cp.occurredAt)} {cp.note ? ( {cp.note} ) : null} ))} )} setLogModal(null)} title={ logModal?.isFinal ? `Mark arrived at ${logModal.station.label}` : `Log pass at ${logModal?.station.label ?? "station"}` } icon={logModal?.isFinal ? : } 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} /> setEditModal(null)} title={`Edit ${editModal?.label ?? "checkpoint"}`} icon={} description="Corrects this leg's time and note only — nothing else changes." initialOccurredAt={editModal?.occurredAt} initialNote={editModal?.note} submitLabel="Save" loading={updateCheckpoint.isPending} onSubmit={submitEdit} /> setYardModal(null)} scheduleId={scheduleId} station={yardModal?.station ?? null} isFinal={yardModal?.isFinal ?? false} alreadyLogged={yardModal?.alreadyLogged ?? false} /> ); }