import { Button, Group, Modal, MultiSelect, Stack, Text } from "@mantine/core"; import { useMutation, useQuery } from "@tanstack/react-query"; import { isAxiosError } from "axios"; import { useEffect, useMemo, useState } from "react"; import { api } from "@/services/api"; import type { TrainComposition } from "@/services/trainBuilder.service"; import { useToast } from "@/hooks/use-toast"; const parseError = (error: unknown, fallback: string) => { if (isAxiosError(error)) { const message = error.response?.data?.message; if (Array.isArray(message)) return message.join(", "); if (typeof message === "string") return message; } return fallback; }; /** Swap the locomotive set of a built train (minimum 2, same-yard rule). */ export default function ChangeLocomotivesModal({ composition, opened, onClose, }: ChangeLocomotivesModalProps) { const { toast } = useToast(); const [locomotiveIds, setLocomotiveIds] = useState([]); const yardId = composition?.currentYard?.id ?? ""; // A locomotive already coupled to ANOTHER built train is not a valid pick — // the API rejects it on save. Exclude those here (keeping this train's own // ones, which are re-listed below as "(coupled)"). const availableQuery = useQuery( api.locomotives.listFiltered.queryOptions({ input: { filters: { status: "AVAILABLE", currentYardId: yardId, excludeCoupled: true, excludeTrainId: composition?.id, }, }, enabled: opened && Boolean(yardId), }), ); const setLocomotives = useMutation(api.trainBuilder.setLocomotives.mutationOptions()); useEffect(() => { if (opened && composition) { setLocomotiveIds(composition.locomotives.map((l) => l.id)); } }, [opened, composition]); // Pickable = available locomotives in the yard + the ones already coupled // to this train (valid to keep even though they are not "loose" anymore). const options = useMemo(() => { const seen = new Set(); const rows: Array<{ value: string; label: string }> = []; for (const loco of composition?.locomotives ?? []) { seen.add(loco.id); rows.push({ value: loco.id, label: `${loco.code}${loco.name ? ` — ${loco.name}` : ""} · pulls ${loco.maxPullWeightTons}T (coupled)`, }); } for (const loco of availableQuery.data ?? []) { if (seen.has(loco.id)) continue; rows.push({ value: loco.id, label: `${loco.code}${loco.name ? ` — ${loco.name}` : ""} · pulls ${loco.maxPullWeightTons}T`, }); } return rows; }, [composition, availableQuery.data]); const handleSave = async () => { if (!composition) return; if (locomotiveIds.length < 2) { toast({ title: "A train needs at least two locomotives", variant: "destructive" }); return; } try { await setLocomotives.mutateAsync({ id: composition.id, locomotiveIds }); toast({ title: "Locomotives updated" }); onClose(); } catch (err) { toast({ title: "Update failed", description: parseError(err, "Could not update locomotives"), variant: "destructive", }); } }; return ( Change locomotives} radius="lg" centered > Only available locomotives standing in{" "} {composition?.currentYard?.label ?? "the train's yard"} can be coupled. The first pick is the lead locomotive. 0 && locomotiveIds.length < 2 ? "Select at least two locomotives" : undefined } nothingFoundMessage="No available locomotives in this yard" /> ); } export interface ChangeLocomotivesModalProps { composition: TrainComposition | null; opened: boolean; onClose: () => void; }