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 */}
}
onClick={(e) => {
e.stopPropagation();
navigate(`/dashboard/operations/batch-board/${schedule.scheduleId}`);
}}
>
View batch windows
);
}
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 }) => (
}
onClick={() =>
navigate(`/dashboard/operations/batch-board/${row.original.scheduleId}`)
}
>
View windows
),
},
];
}, [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) => (
))}
)}
);
}