import { Button, Group, Modal, MultiSelect, Select, Stack, Text, TextInput, Textarea, } from "@mantine/core"; import { useMutation, useQuery } from "@tanstack/react-query"; import { isAxiosError } from "axios"; import { useEffect, 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; }; // Run-number parity carries the trade direction: odd = export, even = import. const isOddNumber = (value: string) => /^\d*[13579]$/.test(value.trim()); const isEvenNumber = (value: string) => /^\d*[02468]$/.test(value.trim()); /** * Step one of the Train Builder: pick the yard it is being assembled in and * couple at least two locomotives from that yard. The train code is assigned by * the system. Wagons are attached afterwards on the composition page. */ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrainModalProps) { const { toast } = useToast(); const [exportTrainNumber, setExportTrainNumber] = useState(""); const [importTrainNumber, setImportTrainNumber] = useState(""); const [trainName, setTrainName] = useState(""); const [yardId, setYardId] = useState(""); const [locomotiveIds, setLocomotiveIds] = useState([]); const [notes, setNotes] = useState(""); const yardsQuery = useQuery(api.routes.yards.queryOptions({ staleTime: 5 * 60_000 })); // Only serviceable locomotives standing in the selected yard can be coupled. const locomotivesQuery = useQuery( api.locomotives.listFiltered.queryOptions({ input: { filters: { status: "AVAILABLE", currentYardId: yardId } }, enabled: Boolean(yardId), }), ); const build = useMutation(api.trainBuilder.build.mutationOptions()); // A locomotive belongs to one yard — switching yards invalidates the pick. useEffect(() => { setLocomotiveIds([]); }, [yardId]); useEffect(() => { if (!opened) { setExportTrainNumber(""); setImportTrainNumber(""); setTrainName(""); setYardId(""); setLocomotiveIds([]); setNotes(""); } }, [opened]); const handleBuild = async () => { if (!yardId || locomotiveIds.length < 2) { toast({ title: "Pick a yard and couple at least two locomotives", variant: "destructive", }); return; } if (!isOddNumber(exportTrainNumber) || !isEvenNumber(importTrainNumber)) { toast({ title: "Enter both run numbers — export must be odd (e.g. 8001), import even (e.g. 8002)", variant: "destructive", }); return; } try { const composition = await build.mutateAsync({ exportTrainNumber: exportTrainNumber.trim(), importTrainNumber: importTrainNumber.trim(), currentYardId: yardId, locomotiveIds, ...(trainName.trim() ? { trainName: trainName.trim() } : {}), ...(notes.trim() ? { notes: notes.trim() } : {}), }); toast({ title: `Train ${composition.code} built` }); onClose(); onBuilt(composition); } catch (err) { toast({ title: "Build failed", description: parseError(err, "Could not build the train"), variant: "destructive", }); } }; const locomotiveOptions = (locomotivesQuery.data ?? []).map((loco) => ({ value: loco.id, label: `${loco.code}${loco.name ? ` — ${loco.name}` : ""} · pulls ${loco.maxPullWeightTons}T`, })); return ( Build a train} radius="lg" centered > A train is assembled in one yard: two or more locomotives plus wagons standing in that same yard. The train code is assigned automatically; wagons are attached on the next screen. setTrainName(e.currentTarget.value)} maxLength={100} /> setExportTrainNumber(e.currentTarget.value)} maxLength={20} error={ exportTrainNumber && !isOddNumber(exportTrainNumber) ? "Must be numeric and odd" : undefined } /> setImportTrainNumber(e.currentTarget.value)} maxLength={20} error={ importTrainNumber && !isEvenNumber(importTrainNumber) ? "Must be numeric and even" : undefined } />