import { useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; import { isAxiosError } from "axios"; import { Badge, Box, Button, Checkbox, Group, Modal, MultiSelect, Paper, Radio, RingProgress, Select, SimpleGrid, Stack, Text, ThemeIcon, Title, } from "@mantine/core"; import { CheckCircle2, Container as ContainerIcon, Eye, Flame, LayoutGrid, Package, Route as RouteIcon, Train, Wallet, Weight, } from "lucide-react"; import { useMutation, useQuery } from "@tanstack/react-query"; import { api } from "@/services/api"; import { formatRouteLabel } from "@/services/routes.service"; import { useToast } from "@/hooks/use-toast"; import { trainSchedulingService } from "@/services/trainScheduling.service"; import type { BookingDetail } from "@/types/booking"; import { cargoTonsAndItems } from "@/utils/cargoWeight"; import type { ContainerPlacement, FreightType, ReschedulePlan, TrainScheduleDetail, TrainScheduleListItem, TrainSchedulePreviewResponse, } from "@/types/trainScheduling"; import { ContainerPlacementGrid } from "./ContainerPlacementGrid"; import { locomotiveOption, showScheduleWarnings } from "./locomotiveOptions"; import { autoFillPlacements, mergePlacementsWithSaved, placementsFromScheduleWagons, validateLocalPlacements, } from "./containerPlacement.util"; import { shouldShowContainerPlacementStep } from "./schedulingContainerStep.util"; import { FleetAvailabilitySummary } from "./FleetAvailabilitySummary"; import { ScheduleBookingsStep } from "./ScheduleBookingsStep"; import { PreviewSummary, ScheduleWarningsAlert } from "./ScheduleWarningsAlert"; import { FreightTypeBadge, SchedulingStatusBadge } from "./ScheduleStatusBadge"; import { RouteCorridor, StatTile, StatusPill, scheduleBrand, } from "./scheduleVisuals"; import { TrainCompositionDiagram } from "./TrainCompositionDiagram"; import { WagonPlanGrid } from "./WagonPlanGrid"; import { WorkflowRail, WorkflowStep } from "./WorkflowStep"; 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; const violations = data?.violations; if (Array.isArray(violations)) return violations.join(", "); } return fallback; }; const formatCountdown = (expiresAt?: string | null) => { if (!expiresAt) return null; const diff = new Date(expiresAt).getTime() - Date.now(); if (diff <= 0) return "Hold expired"; const hours = Math.floor(diff / 3600000); const mins = Math.floor((diff % 3600000) / 60000); return `${hours}h ${mins}m remaining`; }; export function AllocateBookingWizard({ booking, opened, onClose, initialBookingIds, }: { booking: BookingDetail; opened: boolean; onClose: () => void; initialBookingIds?: string[]; }) { const navigate = useNavigate(); const { toast } = useToast(); const bookingFreightType = booking.freightType as FreightType; const [activeStep, setActiveStep] = useState(0); const [scheduleMode, setScheduleMode] = useState<"existing" | "new">("existing"); const [selectedScheduleId, setSelectedScheduleId] = useState(null); const [routeId, setRouteId] = useState(""); const scheduleDate = booking.scheduledDate; const [locomotiveIds, setLocomotiveIds] = useState([]); const [extraBookingIds, setExtraBookingIds] = useState([]); const [forceAssign, setForceAssign] = useState(false); const [previewResult, setPreviewResult] = useState(null); const [containerPlacements, setContainerPlacements] = useState([]); const [assignedSchedule, setAssignedSchedule] = useState(null); const [reschedulePlan, setReschedulePlan] = useState(null); const [confirmPreempt, setConfirmPreempt] = useState(false); const [allocationComplete, setAllocationComplete] = useState(false); const originId = booking.originYard?.id; const destinationId = booking.destinationYard?.id; const eligibleFilters = useMemo( () => ({ originStationId: originId, destinationStationId: destinationId, }), [originId, destinationId], ); const eligibleQuery = useQuery( api.trainScheduling.eligibleBookings.queryOptions({ input: { filters: eligibleFilters }, enabled: opened, }), ); // Paginated {items, meta} list; the newest 100 schedules comfortably cover // every DRAFT schedule the wizard can attach to. const schedulesQuery = useQuery( api.trainScheduling.scheduleList.queryOptions({ input: { filters: { pageSize: 100 } }, }), ); const routesQuery = useQuery( api.routes.list.queryOptions({ input: { status: "AVAILABLE" } }), ); const locomotivesQuery = useQuery( api.trainScheduling.availableLocomotives.queryOptions({ input: { routeId: scheduleMode === "new" && routeId ? routeId : undefined, }, }), ); const create = useMutation(api.trainScheduling.createSchedule.mutationOptions()); const preview = useMutation(api.trainScheduling.preview.mutationOptions()); const assign = useMutation(api.trainScheduling.assignBookings.mutationOptions()); const finalize = useMutation(api.trainScheduling.finalizeSchedule.mutationOptions()); useEffect(() => { if (scheduleMode === "new") { setLocomotiveIds([]); } }, [routeId, scheduleMode]); const matchingSchedules = useMemo( () => (schedulesQuery.data?.items ?? []).filter( (s: TrainScheduleListItem) => s.status === "DRAFT" && (!s.freightType || s.freightType === "MIXED" || s.freightType === bookingFreightType), ), [schedulesQuery.data, bookingFreightType], ); const allBookingIds = useMemo( () => [booking.id, ...extraBookingIds.filter((id) => id !== booking.id)], [booking.id, extraBookingIds], ); const containerUnits = previewResult?.containerUnits ?? []; const containerSlots = previewResult?.containerSlotSequenceNos ?? []; const hasContainerStep = useMemo( () => shouldShowContainerPlacementStep({ containerUnitCount: containerUnits.length, scheduleFreightType: booking.freightType, bookingFreightTypes: [ booking.freightType, ...(eligibleQuery.data?.items ?? []) .filter((item) => allBookingIds.includes(item.id)) .map((item) => item.freightType), ], }), [ allBookingIds, booking.freightType, containerUnits.length, eligibleQuery.data?.items, ], ); const previewFreightType = previewResult?.summary?.freightMode as FreightType | undefined; const finalizeStep = hasContainerStep ? 3 : 2; useEffect(() => { if (!opened) { setActiveStep(0); setPreviewResult(null); setAssignedSchedule(null); setExtraBookingIds([]); setContainerPlacements([]); setReschedulePlan(null); setConfirmPreempt(false); setAllocationComplete(false); return; } if (initialBookingIds?.length) { setExtraBookingIds(initialBookingIds.filter((id) => id !== booking.id)); } }, [opened, booking.id, initialBookingIds]); useEffect(() => { if (matchingSchedules.length && !selectedScheduleId) { setSelectedScheduleId(matchingSchedules[0].id); } }, [matchingSchedules, selectedScheduleId]); const savedPlacementsFromSchedule = useMemo( () => assignedSchedule?.trainSet?.wagons ? placementsFromScheduleWagons(assignedSchedule.trainSet.wagons) : [], [assignedSchedule?.trainSet?.wagons], ); useEffect(() => { if (!containerUnits.length || !containerSlots.length) return; setContainerPlacements((current) => { if (current.length && current.some((p) => p.containerNumber?.trim())) { return current; } const autoFilled = autoFillPlacements(containerUnits, containerSlots); if (savedPlacementsFromSchedule.length) { return mergePlacementsWithSaved(autoFilled, savedPlacementsFromSchedule); } if (current.length) return current; return autoFilled; }); }, [containerUnits, containerSlots, savedPlacementsFromSchedule]); const activeRoutes = useMemo( () => routesQuery.data ?? [], [routesQuery.data], ); const displayWagonPlan = useMemo(() => { const savedWagons = assignedSchedule?.trainSet?.wagons ?? []; const physicalBySeq = new Map( savedWagons.map((w) => [w.sequenceNo, w.physicalWagonNumber ?? null]), ); if (previewResult?.wagonPlan?.length) { return previewResult.wagonPlan.map((slot) => ({ ...slot, physicalWagonNumber: physicalBySeq.get(slot.sequenceNo) ?? null, })); } if (savedWagons.length) return savedWagons; return []; }, [previewResult?.wagonPlan, assignedSchedule?.trainSet?.wagons]); const ensureSchedule = async (): Promise => { if (scheduleMode === "existing" && selectedScheduleId) return selectedScheduleId; if (!routeId || !scheduleDate || locomotiveIds.length < 2) { throw new Error("Select route, date, and at least two locomotives"); } const created = await create.mutateAsync({ payload: { routeId, scheduleDate, locomotiveIds }, }); showScheduleWarnings(created.warnings); setSelectedScheduleId(created.id); return created.id; }; const handlePreview = async () => { if (!originId || !destinationId) { toast({ title: "Booking missing origin or destination", variant: "destructive" }); return; } try { const targetScheduleId = scheduleMode === "existing" ? (selectedScheduleId ?? undefined) : undefined; const result = await preview.mutateAsync({ payload: { bookingIds: allBookingIds, scheduleDate, originStationId: originId, destinationStationId: destinationId, targetScheduleId, }, }); setPreviewResult(result); if (booking.isGovernment && targetScheduleId) { const plan = (await trainSchedulingService.previewReschedule(targetScheduleId, { incomingBookingIds: allBookingIds, trigger: "GOVERNMENT_PREEMPT", })) as ReschedulePlan; setReschedulePlan(plan); } else { setReschedulePlan(null); } if (result.containerUnits?.length && result.containerSlotSequenceNos?.length) { const autoFilled = autoFillPlacements( result.containerUnits, result.containerSlotSequenceNos, ); setContainerPlacements(autoFilled); } setActiveStep(1); } catch (err) { toast({ title: "Preview failed", description: parseError(err, "Could not preview"), variant: "destructive", }); } }; const handleAssign = async () => { if (hasContainerStep) { const issues = validateLocalPlacements(containerUnits, containerPlacements); if (issues.length) { toast({ title: "Complete container assignments", description: issues.join(", "), variant: "destructive", }); return; } } if (reschedulePlan?.displaced.length && !confirmPreempt) { toast({ title: "Confirm displacement", description: "Acknowledge displaced bookings before assigning", variant: "destructive", }); return; } try { const scheduleId = await ensureSchedule(); let result: TrainScheduleDetail; if (reschedulePlan?.displaced.length) { const executed = await trainSchedulingService.executeReschedule(scheduleId, { incomingBookingIds: allBookingIds, trigger: "GOVERNMENT_PREEMPT", finalBookingIds: reschedulePlan.finalBookingIds, displacedBookingIds: reschedulePlan.displaced.map((b) => b.id), }); result = (executed as { schedule: TrainScheduleDetail }).schedule; } else { result = await assign.mutateAsync({ id: scheduleId, freightType: previewFreightType, payload: { bookingIds: allBookingIds, forceAssign, containerPlacements: hasContainerStep ? containerPlacements : undefined, }, }); } setAssignedSchedule(result); const saved = result.trainSet?.wagons ? placementsFromScheduleWagons(result.trainSet.wagons) : []; if (saved.length) { setContainerPlacements(saved); } setActiveStep(finalizeStep); toast({ title: "Bookings assigned — wagons auto-pinned" }); if (result.deferredBookings?.length) { toast({ title: "Partial assignment", description: `${result.deferredBookings.length} booking(s) deferred to next train`, }); } } catch (err) { toast({ title: "Assign failed", description: parseError(err, "Could not assign"), variant: "destructive", }); } }; const handleFinalize = async () => { const scheduleId = assignedSchedule?.id ?? selectedScheduleId; if (!scheduleId) return; try { const finalized = await finalize.mutateAsync(scheduleId); setAssignedSchedule(finalized); setAllocationComplete(true); toast({ title: "Schedule finalized — booking allocated" }); } catch (err) { toast({ title: "Finalize failed", description: parseError(err, "Could not finalize schedule"), variant: "destructive", }); } }; const amount = Number(booking.totalAmount); const containers = booking.bookingContainers ?? []; const containerCount = containers.reduce((sum, c) => sum + Number(c.quantity ?? 0), 0); const { tons: weight, items: itemCount } = cargoTonsAndItems(booking); const holdCountdown = formatCountdown(booking.holdExpiresAt); const containerComplete = hasContainerStep && containerUnits.length > 0 && validateLocalPlacements(containerUnits, containerPlacements).length === 0; const stepsMeta = [ { key: "bookings", icon: Package, title: "Bookings", subtitle: "Select cargo & preview the plan", complete: Boolean(previewResult) || Boolean(assignedSchedule), }, { key: "wagon", icon: LayoutGrid, title: "Wagon plan", subtitle: "Review generated allocations", complete: displayWagonPlan.length > 0, }, ...(hasContainerStep ? [ { key: "container", icon: ContainerIcon, title: "Containers", subtitle: "Map units to wagon slots", complete: containerComplete, }, ] : []), { key: "finalize", icon: CheckCircle2, title: "Finalize", subtitle: "Lock the plan & dispatch", complete: allocationComplete, }, ]; const completedCount = stepsMeta.filter((s) => s.complete).length; const progressPct = Math.round((completedCount / stepsMeta.length) * 100); const toggleStep = (i: number) => setActiveStep((cur) => (cur === i ? -1 : i)); const renderStepRightSlot = (key: string) => { if (key === "bookings") { if (previewResult) { return ( {previewResult.valid ? "Plan valid" : "Has issues"} ); } return allBookingIds.length ? ( {allBookingIds.length} selected ) : null; } if (key === "wagon" && displayWagonPlan.length) { return ( {displayWagonPlan.length} wagons ); } if (key === "container" && containerUnits.length) { return ( {containerUnits.length} units ); } if (key === "finalize" && allocationComplete) { return ; } return null; }; const renderStepBody = (key: string) => { if (key === "bookings") { return ( Train schedule setScheduleMode(v as "existing" | "new")} > {scheduleMode === "existing" ? ( ({ value: r.id, label: formatRouteLabel(r) }))} value={routeId || null} onChange={(v) => setRouteId(v ?? "")} searchable /> locomotiveOption(l, " · "), )} value={locomotiveIds} onChange={setLocomotiveIds} searchable disabled={!routeId} error={ locomotiveIds.length > 0 && locomotiveIds.length < 2 ? "Select at least two locomotives" : undefined } nothingFoundMessage={ routeId ? "No available locomotives for this corridor" : "Select a route first" } /> )} {holdCountdown ? ( Hold window: {holdCountdown} ) : null} ({ id: b.id, reference: b.reference ?? b.id.slice(0, 8), weightTons: b.weightTons, isGovernment: b.isGovernment, wagonsRequired: b.wagonsRequired, contractReference: b.contractReference, origin: b.origin, destination: b.destination, }))} eligibleItems={eligibleQuery.data?.items ?? []} eligibleLoading={eligibleQuery.isLoading} selectedIds={allBookingIds} onSelectionChange={(ids) => { setExtraBookingIds(ids.filter((id) => id !== booking.id)); }} freightType={bookingFreightType} /> setForceAssign(e.currentTarget.checked)} size="sm" /> {previewResult ? ( ) : null} ); } if (key === "wagon") { return ( {!displayWagonPlan.length && !previewResult ? ( Run a preview from the Bookings step to generate the wagon plan. ) : null} {reschedulePlan?.displaced.length ? ( Government preempt — bookings to displace {reschedulePlan.displaced.map((b) => ( {b.reference} (priority {b.priorityScore}) ))} setConfirmPreempt(e.currentTarget.checked)} /> ) : null} {!hasContainerStep ? ( ) : ( )} ); } if (key === "container") { return ( {!containerUnits.length ? ( Run preview from the Bookings step to load container units for numbering. ) : ( )} ); } // finalize return ( {displayWagonPlan.length || assignedSchedule?.trainSet?.wagons?.length ? ( ) : null} {allocationComplete ? ( Allocation complete Booking {booking.reference} is scheduled on train{" "} {assignedSchedule?.trainSet?.locomotive?.code ?? "—"} . ) : ( <> Ready to finalize Finalizing locks the plan, moves the schedule to{" "} SCHEDULED , and completes the booking allocation. )} ); }; return ( {/* Hero */} Allocate {booking.reference} {booking.schedulingStatus ? ( ) : null} {previewResult ? ( } > Preview {previewResult.valid ? "valid" : "has issues"} ) : null} {/* Workflow */} Allocation workflow {completedCount} of {stepsMeta.length} steps complete · expand any step to edit {progressPct}% } /> {stepsMeta.map((step, index) => ( toggleStep(index)} rightSlot={renderStepRightSlot(step.key)} > {renderStepBody(step.key)} ))} ); }