import { FormEvent, useMemo, useState } from "react"; import { Edit, Eye, Trash2 } from "lucide-react"; import type { ColumnDef } from "@edr/ui-common"; import { ActionIcon, Badge, Box, Button, Card, Group, Modal, Select, SimpleGrid, Stack, Text, TextInput, Tooltip, } from "@mantine/core"; import FleetToolbar from "@/components/fleet/FleetToolbar"; import { useFleetViewMode } from "@/components/fleet/useFleetViewMode"; import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter"; import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles"; import { useCreateRoute, useDeactivateRoute, useRouteYards, useRoutes, useUpdateRoute, } from "@/hooks/useRoutes"; import { useToast } from "@/hooks/use-toast"; import type { RouteRecord, YardRef } from "@/services/routes.service"; import { DataTable, DataTableFooter, usePagination } from "@edr/ui-common"; type RouteFormState = { name: string; milestones: string[]; }; const emptyForm = (): RouteFormState => ({ name: "", milestones: ["", ""] }); const yardLabel = (yard?: YardRef | null) => (yard ? `${yard.label} (${yard.code})` : "—"); const routeStops = (route: RouteRecord) => (route.milestones ?? []) .sort((left, right) => left.sequenceNo - right.sequenceNo) .map((milestone) => milestone.yard?.label ?? milestone.yard?.code ?? milestone.yardId); const normalizeRouteError = (error: unknown) => { const responseData = (error as { response?: { data?: unknown } })?.response?.data; const data = responseData && typeof responseData === "object" ? (responseData as Record) : undefined; const rawMessage = data?.message ?? data?.error ?? (error as Error)?.message; return Array.isArray(rawMessage) ? rawMessage.join(", ") : rawMessage ? String(rawMessage) : "Save failed"; }; export default function RoutesPage() { const [search, setSearch] = useState(""); const [formOpen, setFormOpen] = useState(false); const [viewing, setViewing] = useState(null); const [editing, setEditing] = useState(null); const [form, setForm] = useState(emptyForm()); const { viewMode, setViewMode } = useFleetViewMode("routes"); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const { toast } = useToast(); const routesQuery = useRoutes(); const yardsQuery = useRouteYards(); const createMutation = useCreateRoute(); const updateMutation = useUpdateRoute(); const deactivateMutation = useDeactivateRoute(); const filteredRoutes = useMemo(() => { const query = search.trim().toLowerCase(); if (!query) return routesQuery.data ?? []; return (routesQuery.data ?? []).filter((route) => { const searchable = [ route.name, route.originYard?.label, route.originYard?.code, route.destinationYard?.label, route.destinationYard?.code, ...routeStops(route), ] .filter(Boolean) .join(" ") .toLowerCase(); return searchable.includes(query); }); }, [routesQuery.data, search]); const pageCount = Math.max(1, Math.ceil(filteredRoutes.length / pagination.pageSize)); const pagedRoutes = useMemo(() => { const start = pagination.pageIndex * pagination.pageSize; return filteredRoutes.slice(start, start + pagination.pageSize); }, [filteredRoutes, pagination.pageIndex, pagination.pageSize]); const yardOptions = useMemo( () => (yardsQuery.data ?? []).map((yard) => ({ value: yard.id, label: `${yard.label} (${yard.code})`, })), [yardsQuery.data], ); const resetForm = () => { setFormOpen(false); setEditing(null); setForm(emptyForm()); }; const openCreate = () => { setEditing(null); setForm(emptyForm()); setFormOpen(true); }; const openEdit = (route: RouteRecord) => { setEditing(route); setForm({ name: route.name, milestones: (route.milestones ?? []) .sort((left, right) => left.sequenceNo - right.sequenceNo) .map((milestone) => milestone.yardId), }); setFormOpen(true); }; const setMilestone = (index: number, yardId: string) => { setForm((current) => ({ ...current, milestones: current.milestones.map((value, currentIndex) => currentIndex === index ? yardId : value, ), })); }; const addMilestone = () => { setForm((current) => ({ ...current, milestones: [...current.milestones, ""] })); }; const removeMilestone = (index: number) => { setForm((current) => ({ ...current, milestones: current.milestones.filter((_, currentIndex) => currentIndex !== index), })); }; const handleSubmit = async (event: FormEvent) => { event.preventDefault(); if (!form.name.trim()) { toast({ title: "Save failed", description: "Route name is required", variant: "destructive" }); return; } if (form.milestones.length < 2 || form.milestones.some((yardId) => !yardId)) { toast({ title: "Save failed", description: "Select at least an origin and destination yard", variant: "destructive", }); return; } try { const payload = { name: form.name.trim(), milestones: form.milestones.map((yardId) => ({ yardId })), isActive: editing?.isActive ?? true, }; if (editing) { await updateMutation.mutateAsync({ id: editing.id, data: payload }); toast({ title: "Route updated" }); } else { await createMutation.mutateAsync(payload); toast({ title: "Route created" }); } resetForm(); } catch (error) { toast({ title: "Save failed", description: normalizeRouteError(error), variant: "destructive" }); } }; const handleDeactivate = async (route: RouteRecord) => { try { await deactivateMutation.mutateAsync(route.id); toast({ title: "Route deactivated" }); } catch { toast({ title: "Deactivate failed", description: "Could not deactivate route", variant: "destructive" }); } }; const isSaving = createMutation.isPending || updateMutation.isPending; const availableOptionsForIndex = (index: number) => { const selectedByOthers = new Set( form.milestones.filter((value, currentIndex) => currentIndex !== index && value), ); return yardOptions.filter( (option) => option.value === form.milestones[index] || !selectedByOthers.has(option.value), ); }; const tableStatus = routesQuery.isLoading ? "loading" : routesQuery.isError ? "error" : "success"; const columns = useMemo((): ColumnDef[] => { const headerClassName = ruleEngineTable.headerCell; const cellClassName = ruleEngineTable.bodyCell; return [ { id: "name", header: "Name", meta: { headerClassName, cellClassName }, cell: ({ row }) => row.original.name }, { id: "origin", header: "Origin", meta: { headerClassName, cellClassName }, cell: ({ row }) => yardLabel(row.original.originYard), }, { id: "destination", header: "Destination", meta: { headerClassName, cellClassName }, cell: ({ row }) => yardLabel(row.original.destinationYard), }, { id: "milestones", header: "Milestones", meta: { headerClassName, cellClassName }, cell: ({ row }) => Math.max((row.original.milestones?.length ?? 0) - 2, 0), }, { id: "status", header: "Status", meta: { headerClassName, cellClassName }, cell: ({ row }) => ( {row.original.isActive ? "Active" : "Inactive"} ), }, { id: "actions", header: "Actions", meta: { headerClassName, cellClassName: `${cellClassName} whitespace-nowrap` }, cell: ({ row }) => ( setViewing(row.original)}> openEdit(row.original)}> handleDeactivate(row.original)} > ), }, ]; }, [deactivateMutation.isPending]); return ( {viewMode === "table" ? ( ( )} /> ) : ( {tableStatus === "loading" ? ( Loading… ) : !pagedRoutes.length ? ( No routes found ) : ( {pagedRoutes.map((route) => ( {route.name} {route.isActive ? "Active" : "Inactive"} {yardLabel(route.originYard)} → {yardLabel(route.destinationYard)} {Math.max((route.milestones?.length ?? 0) - 2, 0)} intermediate milestones ))} )} )} {editing ? "Edit Route" : "Add Route"}} size="lg" radius="lg" centered >
setForm((current) => ({ ...current, name: e.currentTarget.value }))} /> Stops {form.milestones.map((yardId, index) => { const role = index === 0 ? "Origin" : index === form.milestones.length - 1 ? "Destination" : "Milestone"; return ( {role}