import { Alert, Badge, Button, Divider, Group, Loader, Modal, Stack, Table, Text, ThemeIcon, Tooltip, } from "@mantine/core"; import { DateTimePicker } from "@mantine/dates"; import { useMutation, useQuery } from "@tanstack/react-query"; import { CheckCircle2, Flag, MapPin, PackageCheck, TrainFront, } from "lucide-react"; import { useEffect, useState } from "react"; import { Freight } from "@edr/types"; import { BookingStatusBadge } from "@/components/bookings/BookingStatusBadge"; import { useAuth } from "@/auth/useAuth"; import { FREIGHT_PERMS, hasPermission as hasFreightPermission } from "@/lib/permissions"; import { useToast } from "@/hooks/use-toast"; import { api } from "@/services/api"; import type { TrackStation, YardWorkBookingRow } from "@/types/trainScheduling"; const parseError = (error: unknown, fallback: string) => { const message = (error as { response?: { data?: { message?: string | string[] } } }) ?.response?.data?.message; if (Array.isArray(message)) return message.join("; "); return message || (error as Error)?.message || fallback; }; const fmtDate = (iso: string) => { const d = new Date(iso); 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 }) { return ( {DIRECTION_LABELS[direction] ?? direction} ); } function SectionLabel({ icon, title, count, }: { icon: React.ReactNode; title: string; count: number; }) { return ( {icon} {title} {count} ); } /** * Yard-work modal for the track page's "Log pass" step. * * A train runs A→B→C→D and bookings board/alight at any stop, so logging the * pass at a yard is the moment its yard work happens: bookings destined here * flip to ARRIVED (import/export) or COMPLETED (intercity) automatically the * instant the pass is logged, and bookings boarding here become loadable — * the server only accepts a load while the train's latest checkpoint is this * yard. The modal therefore drives the sequence: log the pass first, then * load anything that boards here (including cargo the operator forgot — it * stays loadable until the next pass is logged). */ export function LogPassYardWorkModal({ opened, onClose, scheduleId, station, isFinal, alreadyLogged, }: { opened: boolean; onClose: () => void; scheduleId: string; station: TrackStation | null; isFinal: boolean; /** True when opened for the current station (pass already logged). */ alreadyLogged: boolean; }) { const { toast } = useToast(); const { user } = useAuth(); const canLoad = hasFreightPermission(user, FREIGHT_PERMS.trainScheduling.load); const canLeave = hasFreightPermission(user, FREIGHT_PERMS.trainScheduling.update); const [justLogged, setJustLogged] = useState(false); // When the train was here — defaults to now, past allowed (recorded after the fact). const [passAt, setPassAt] = useState(null); useEffect(() => { setJustLogged(false); setPassAt(new Date()); }, [station?.sequenceNo, opened]); const logged = alreadyLogged || justLogged; const yardWorkQuery = useQuery( api.trainScheduling.yardWork.queryOptions({ input: { scheduleId }, enabled: opened && Boolean(scheduleId), }), ); const recordCheckpoint = useMutation( api.trainScheduling.recordCheckpoint.mutationOptions(), ); const load = useMutation(api.trainScheduling.loadScheduleBooking.mutationOptions()); // "Leave behind": the cargo is not on the train — unassign frees its wagons // and returns the booking to the pool for a later schedule. Reversible (the // booking can be re-assigned), so no extra confirm step. const leave = useMutation(api.trainScheduling.unassignBooking.mutationOptions()); const yard = yardWorkQuery.data?.yards.find((y) => y.yardId === station?.yardId); const boarders: YardWorkBookingRow[] = yard?.toLoad ?? []; const arrivals: YardWorkBookingRow[] = yard?.toUnload ?? []; const pendingBoarders = boarders.filter((r) => !r.loadedAt); const doLogPass = () => { if (!station) return; recordCheckpoint.mutate( { id: scheduleId, payload: { sequenceNo: station.sequenceNo, ...(passAt ? { occurredAt: passAt.toISOString() } : {}), }, }, { onSuccess: () => { setJustLogged(true); toast({ title: isFinal ? "Train arrived — remaining bookings marked arrived, assets freed" : `Pass logged at ${station.label}`, description: isFinal ? undefined : arrivals.some((r) => r.canUnload) ? "Bookings arriving here have been marked arrived." : undefined, }); void yardWorkQuery.refetch(); }, onError: (err) => toast({ title: "Could not log checkpoint", description: parseError(err, "Please try again"), variant: "destructive", }), }, ); }; const doLoad = (row: YardWorkBookingRow) => { load.mutate( { scheduleId, bookingId: row.id }, { onSuccess: () => { toast({ title: `${row.reference ?? "Booking"} loaded`, description: `Cargo boarded the train at ${station?.label ?? "this yard"}.`, }); void yardWorkQuery.refetch(); }, onError: (err) => toast({ title: "Could not load booking", description: parseError(err, "Please try again"), variant: "destructive", }), }, ); }; const doLeave = (row: YardWorkBookingRow) => { leave.mutate( { id: scheduleId, bookingId: row.id }, { onSuccess: () => { toast({ title: `${row.reference ?? "Booking"} left behind`, description: "Removed from this train — wagons freed, booking returned to the pool for a later schedule.", }); void yardWorkQuery.refetch(); }, onError: (err) => toast({ title: "Could not leave booking behind", description: parseError(err, "Please try again"), variant: "destructive", }), }, ); }; const hasWork = boarders.length > 0 || arrivals.length > 0; return ( {isFinal ? : } {isFinal ? "Arrival" : "Yard work"} — {station?.label ?? ""} {logged ? ( {isFinal ? "Arrived" : "Pass logged"} ) : null} } > {yardWorkQuery.isLoading ? ( ) : !hasWork ? ( }> No bookings board or alight at this station. ) : ( <> {/* ── Arriving here ─────────────────────────────────────────── */} {arrivals.length > 0 ? ( } title="Arriving at this yard" count={arrivals.length} /> {!logged ? ( Logging the pass marks the loaded bookings below as Arrived (import/export) or Completed (intercity) automatically. ) : null} Booking Customer Direction Status Arrived {arrivals.map((row) => ( {row.reference ?? row.id.slice(0, 8)} {row.customer} {row.arrivedAt ? fmtDate(row.arrivedAt) : "—"} ))}
) : null} {arrivals.length > 0 && boarders.length > 0 ? : null} {/* ── Boarding here ─────────────────────────────────────────── */} {boarders.length > 0 ? ( } title="Boarding at this yard" count={boarders.length} /> {!logged && pendingBoarders.length > 0 ? ( Log the pass first — the train must be at {station?.label} before cargo can be loaded. ) : null} Booking Customer Direction Status Loaded {boarders.map((row) => ( {row.reference ?? row.id.slice(0, 8)} {row.isGovernment ? ( GOV ) : null} {row.customer} {row.loadedAt ? ( {fmtDate(row.loadedAt)} ) : ( Not loaded )} {!row.loadedAt ? ( ) : null} ))}
) : null} )} {!logged ? ( setPassAt(v ? new Date(v) : null)} maxDate={new Date()} valueFormat="DD MMM YYYY HH:mm" clearable={false} radius="md" maw={320} /> ) : null} {logged && pendingBoarders.length > 0 ? `${pendingBoarders.length} booking${pendingBoarders.length === 1 ? "" : "s"} still to load before the next station.` : ""} {!logged ? ( ) : null}
); }