import { useCallback, useEffect, useMemo, useState } from "react"; import { Alert, Badge, Button, Group, Paper, Progress, Select, SimpleGrid, Stack, Text, ThemeIcon, } from "@mantine/core"; import { AlertTriangle, Link2, Wand2 } from "lucide-react"; import { Freight } from "@edr/types"; import type { PinWagonAssignment, TrainScheduleDetail } from "@/types/trainScheduling"; import type { Wagon } from "@/services/wagon.service"; import { wagonMatchesScheduleOrigin } from "@/utils/wagonAvailability"; import { autoFillWagonAssignments, countFilledSlots } from "./pinWagons.util"; export function PinWagonsForm({ schedule, availableWagons, isSubmitting, onSubmit, autoFillOnMount = true, }: { schedule: TrainScheduleDetail; availableWagons: Wagon[]; isSubmitting?: boolean; onSubmit: (assignments: PinWagonAssignment[]) => void; autoFillOnMount?: boolean; }) { const originYardId = schedule.originStation?.id; // Consist-only rows are the built train's coupled-but-empty wagons — display // entries with no TrainSetWagon slot behind them, so nothing can be pinned. const slots = (schedule.trainSet?.wagons ?? []).filter((w) => !w.consistOnly); const [assignments, setAssignments] = useState>({}); const wagonOptionsByType = useMemo(() => { const map = new Map>(); for (const wagon of availableWagons) { const isPinnedOnSlot = slots.some((s) => s.physicalWagonId === wagon.id); if ( !wagonMatchesScheduleOrigin(wagon, originYardId, { allowPinned: isPinnedOnSlot, }) ) { continue; } if (wagon.status !== Freight.WagonStatus.Available && !isPinnedOnSlot) { continue; } const typeId = wagon.wagonTypeId; const list = map.get(typeId) ?? []; list.push({ value: wagon.id, label: wagon.wagonNumber }); map.set(typeId, list); } return map; }, [availableWagons, originYardId, slots]); const runAutoFill = useCallback( (preserveManual = false) => { const existing = preserveManual ? assignments : {}; setAssignments(autoFillWagonAssignments(slots, wagonOptionsByType, existing)); }, [assignments, slots, wagonOptionsByType], ); useEffect(() => { if (!autoFillOnMount || !slots.length) return; setAssignments(autoFillWagonAssignments(slots, wagonOptionsByType)); }, [schedule.id, slots, wagonOptionsByType, autoFillOnMount]); const fillStats = useMemo( () => countFilledSlots(slots, assignments), [slots, assignments], ); const progress = fillStats.total > 0 ? Math.round((fillStats.filled / fillStats.total) * 100) : 0; const handleSubmit = () => { const payload: PinWagonAssignment[] = Object.entries(assignments) .filter(([, wagonId]) => Boolean(wagonId)) .map(([trainSetWagonId, physicalWagonId]) => ({ trainSetWagonId, physicalWagonId })); onSubmit(payload); }; if (!slots.length) { return ( Assign bookings first to create wagon slots. ); } return ( Pin physical wagons Match each train slot to a fleet wagon. Slots are auto-filled when possible. {fillStats.filled} of {fillStats.total} slots filled {progress}% {fillStats.unfilledSlotNumbers.length > 0 ? ( } title="Some slots could not be auto-filled" > No matching fleet wagon for slot {fillStats.unfilledSlotNumbers.length === 1 ? "" : "s"} # {fillStats.unfilledSlotNumbers.join(", #")}. Select manually or add wagons to the fleet. ) : null} {slots.map((slot) => { const typeId = slot.wagonType?.id ?? ""; const options = wagonOptionsByType.get(typeId) ?? availableWagons.map((w) => ({ value: w.id, label: w.wagonNumber, })); return ( {slot.sequenceNo} Slot #{slot.sequenceNo} {slot.wagonType?.code ?? "—"} · {slot.capacityTons}T