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"; import { useImportTrainNumberOptions } from "@/hooks/useImportTrainNumberOptions"; import { exportRunFor } from "@/constants/trainRuns"; 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; }; /** * Step one of the Train Builder: pick the yard it is being assembled in and * couple at least one locomotive 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 })); // Admin-managed run list (dropdown settings); numbers already on a train // come back disabled so they cannot be picked twice. const importNumbers = useImportTrainNumberOptions(); // Only serviceable locomotives standing in the selected yard, and not already // coupled to another built train, can be picked. A new train owns none yet, so // no train to keep-exclude. const locomotivesQuery = useQuery( api.locomotives.listFiltered.queryOptions({ input: { filters: { status: "AVAILABLE", currentYardId: yardId, excludeCoupled: true, }, }, enabled: Boolean(yardId), }), ); const build = useMutation(api.trainBuilder.build.mutationOptions()); // A locomotive belongs to one yard — switching yards invalidates the pick. useEffect(() => { setLocomotiveIds([]); }, [yardId]); // The export run is fixed by the import run, so it tracks it rather than // being entered by hand (and clears back to empty when the import is cleared). useEffect(() => { setExportTrainNumber(exportRunFor(importTrainNumber)); }, [importTrainNumber]); useEffect(() => { if (!opened) { setExportTrainNumber(""); setImportTrainNumber(""); setTrainName(""); setYardId(""); setLocomotiveIds([]); setNotes(""); } }, [opened]); const handleBuild = async () => { if (!trainName.trim()) { toast({ title: "Enter the voyage number", variant: "destructive", }); return; } if (!yardId || locomotiveIds.length < 1) { toast({ title: "Pick a yard and couple at least one locomotive", variant: "destructive", }); return; } // Both numbers come from the fixed run pairs, so parity cannot be wrong — // only "nothing picked" is reachable here. if (!exportTrainNumber || !importTrainNumber) { toast({ title: "Pick an import train number (e.g. 8002) — the export run follows it", variant: "destructive", }); return; } try { const composition = await build.mutateAsync({ exportTrainNumber: exportTrainNumber.trim(), importTrainNumber: importTrainNumber.trim(), currentYardId: yardId, locomotiveIds, 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} required /> {/* Fixed by the import run — derived, never typed. */} ({ value: y.id, label: y.label ?? y.code, }))} value={yardId || null} onChange={(v) => setYardId(v ?? "")} searchable />