import { useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { Alert, Box, Button, Card, Container, Group, Paper, RingProgress, Select, SimpleGrid, Skeleton, Stack, Text, ThemeIcon, } from "@mantine/core"; import { AlertTriangle, ArrowRight, CalendarClock, CalendarDays, Inbox, Package, Ruler, Train, TrainFront, Weight, } from "lucide-react"; import type { ColumnDef } from "@edr/ui-common"; import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common"; import Breadcrumbs from "@/components/ui/Breadcrumbs"; import FleetToolbar from "@/components/fleet/FleetToolbar"; import { useFleetViewMode } from "@/components/fleet/useFleetViewMode"; import { BookingPipeline, HeroChip, totalBookingCount, WindowStatusPill, } from "@/components/trainScheduling/batchVisuals"; import { RouteCorridor, StatTile } from "@/components/trainScheduling/scheduleVisuals"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { FREIGHT_BRAND, FREIGHT_BRAND_DARK } from "@/theme/freight-brand"; import { useBatchBoard } from "@/hooks/trainScheduling/useTrainScheduling"; import type { BatchBoardSchedule } from "@/types/trainScheduling"; const fmtTons = (n: number) => `${n.toLocaleString(undefined, { maximumFractionDigits: 1 })} t`; const fmtMeters = (n: number) => `${n.toLocaleString(undefined, { maximumFractionDigits: 1 })} m`; const fmtScheduleDate = (iso: string | null) => iso ? new Intl.DateTimeFormat("en-GB", { weekday: "short", day: "2-digit", month: "short", hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Africa/Addis_Ababa", }).format(new Date(iso)) + " EAT" : "No date"; const splitDate = (iso: string | null) => { if (!iso) return { day: "—", time: "" }; const date = new Date(iso); if (Number.isNaN(date.getTime())) return { day: "—", time: "" }; return { day: new Intl.DateTimeFormat("en-GB", { day: "2-digit", month: "short", year: "numeric", timeZone: "Africa/Addis_Ababa", }).format(date), time: new Intl.DateTimeFormat("en-GB", { hour: "2-digit", minute: "2-digit", hour12: false, timeZone: "Africa/Addis_Ababa", }).format(date) + " EAT", }; }; /** Capacity ring color: gold normally, red once over capacity. */ function ringColor(pct: number) { if (pct >= 100) return "#fa5252"; return "#F2A516"; } /** Capacity ring that keeps the explicit allocated/max numbers underneath. */ function CapacityRing({ pct, label, current, max, }: { pct: number; label: string; current: string; max: string; }) { const clamped = Math.min(100, Math.max(0, pct)); const color = ringColor(pct); return ( {Math.round(pct)}% {label} } /> {current} of {max} ); } /** Small percent chip used in the table's capacity column. */ function CapacityChip({ icon: Icon, pct, text, }: { icon: typeof Weight; pct: number | null; text: string; }) { const over = pct != null && pct >= 100; return ( {pct != null ? `${Math.round(pct)}%` : "—"} {text} ); } function weightPctOf(s: BatchBoardSchedule) { return s.capacity.maxWeightTons && s.capacity.maxWeightTons > 0 ? (s.capacity.usedWeightTons / s.capacity.maxWeightTons) * 100 : null; } function lengthPctOf(s: BatchBoardSchedule) { return s.capacity.maxLengthMeters && s.capacity.maxLengthMeters > 0 ? (s.capacity.allocatedLengthMeters / s.capacity.maxLengthMeters) * 100 : null; } function ScheduleCard({ schedule }: { schedule: BatchBoardSchedule }) { const navigate = useNavigate(); const { capacity, counts, locomotive } = schedule; const lengthPct = lengthPctOf(schedule); const weightPct = weightPctOf(schedule); const totalBookings = totalBookingCount(counts); return ( navigate(`/dashboard/operations/batch-board/${schedule.scheduleId}`) } > {/* header */} {schedule.trainNumber ?? schedule.routeName ?? "Schedule"} Freight schedule · {schedule.status} }> {fmtScheduleDate(schedule.scheduleDate)} {locomotive ? ( }> {locomotive.code} · {fmtTons(locomotive.maxPullWeightTons)} ) : null} {!locomotive ? ( } py={6} styles={{ message: { fontSize: 12 } }} > No locomotive assigned — wagon allocation cannot run. ) : null} {/* capacity: weight + length rings + wagons (numbers preserved) */} {weightPct != null ? ( ) : null} {capacity.allocatedWagons} Wagons allocated {lengthPct != null ? ( ) : null} {/* booking pipeline */} Booking pipeline {totalBookings} booking{totalBookings === 1 ? "" : "s"} {/* CTA */} ); } function CardSkeleton() { return ( ); } export default function BatchBoardPage() { const navigate = useNavigate(); const { data, isLoading, isFetching, refetch } = useBatchBoard(); const { viewMode, setViewMode } = useFleetViewMode("batch-board"); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [search, setSearch] = useState(""); const [windowFilter, setWindowFilter] = useState("ALL"); const schedules = data ?? []; const summary = useMemo(() => { const openWindows = schedules.filter((s) => s.bookingWindowStatus === "OPEN").length; const totalBookings = schedules.reduce((sum, s) => sum + totalBookingCount(s.counts), 0); const totalWagons = schedules.reduce((sum, s) => sum + s.capacity.allocatedWagons, 0); return { openWindows, totalBookings, totalWagons }; }, [schedules]); const filtered = useMemo(() => { const query = search.trim().toLowerCase(); return schedules.filter((s) => { if (windowFilter !== "ALL" && s.bookingWindowStatus !== windowFilter) return false; if (!query) return true; const haystack = [ s.trainNumber, s.routeName, s.origin, s.destination, s.locomotive?.code, s.status, s.bookingWindowStatus, ] .filter(Boolean) .join(" ") .toLowerCase(); return haystack.includes(query); }); }, [schedules, search, windowFilter]); const pageCount = Math.max(1, Math.ceil(filtered.length / pagination.pageSize)); const paged = useMemo(() => { const start = pagination.pageIndex * pagination.pageSize; return filtered.slice(start, start + pagination.pageSize); }, [filtered, pagination]); const columns = useMemo((): ColumnDef[] => { const headerClassName = ruleEngineTable.headerCell; const cellClassName = ruleEngineTable.bodyCell; return [ { id: "train", header: "Train / Route", meta: { headerClassName, cellClassName }, cell: ({ row }) => ( {row.original.trainNumber ?? row.original.routeName ?? "Schedule"} ), }, { id: "date", header: "Departure", meta: { headerClassName, cellClassName }, cell: ({ row }) => { const { day, time } = splitDate(row.original.scheduleDate); return ( {day} {time || "—"} ); }, }, { id: "window", header: "Window", meta: { headerClassName, cellClassName }, cell: ({ row }) => , }, { id: "loco", header: "Locomotive", meta: { headerClassName, cellClassName }, cell: ({ row }) => row.original.locomotive ? ( {row.original.locomotive.code} {fmtTons(row.original.locomotive.maxPullWeightTons)} pull ) : ( No loco ), }, { id: "capacity", header: "Capacity", meta: { headerClassName, cellClassName }, cell: ({ row }) => ( {row.original.capacity.allocatedWagons} wgn ), }, { id: "bookings", header: "Bookings", meta: { headerClassName, cellClassName }, cell: ({ row }) => { const total = totalBookingCount(row.original.counts); return ( {total} booking{total === 1 ? "" : "s"} ); }, }, { id: "actions", header: "", meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` }, cell: ({ row }) => ( ), }, ]; }, [navigate]); const tableStatus = isLoading ? "loading" : "success"; return ( v && setWindowFilter(v)} data={[ { value: "ALL", label: "All windows" }, { value: "OPEN", label: "Open" }, { value: "FULL", label: "Full" }, { value: "CLOSED", label: "Closed" }, ]} w={150} styles={{ input: { borderColor: "var(--mantine-color-gray-3)" } }} /> } /> {viewMode === "table" ? ( ( )} /> ) : isLoading ? ( ) : filtered.length === 0 ? ( No active schedules Schedules with an open booking window appear here. Create or activate a schedule to get started. ) : ( {filtered.map((s) => ( ))} )} ); }