mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 00:38:11 +00:00
- Introduced new yard distances resource with CRUD operations. - Created migration for yard distances table with necessary constraints. - Implemented service and repository for yard distances handling. - Added controller for API endpoints to manage yard distances. - Updated rule engine configuration to include yard distances. - Enhanced rule engine resource page to support yard distance selection. - Updated contracts and train builder pages to handle new yard distance logic. - Added error handling utility for better error message extraction.
139 lines
4.4 KiB
TypeScript
139 lines
4.4 KiB
TypeScript
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<string[]>([]);
|
|
|
|
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<string>();
|
|
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 (
|
|
<Modal
|
|
opened={opened}
|
|
onClose={onClose}
|
|
title={<Text fw={600}>Change locomotives</Text>}
|
|
radius="lg"
|
|
centered
|
|
>
|
|
<Stack gap="md">
|
|
<Text size="sm" c="dimmed">
|
|
Only available locomotives standing in{" "}
|
|
{composition?.currentYard?.label ?? "the train's yard"} can be coupled.
|
|
The first pick is the lead locomotive.
|
|
</Text>
|
|
<MultiSelect
|
|
label="Locomotives"
|
|
data={options}
|
|
value={locomotiveIds}
|
|
onChange={setLocomotiveIds}
|
|
searchable
|
|
error={
|
|
locomotiveIds.length > 0 && locomotiveIds.length < 2
|
|
? "Select at least two locomotives"
|
|
: undefined
|
|
}
|
|
nothingFoundMessage="No available locomotives in this yard"
|
|
/>
|
|
<Group justify="flex-end">
|
|
<Button variant="default" onClick={onClose}>
|
|
Cancel
|
|
</Button>
|
|
<Button loading={setLocomotives.isPending} onClick={handleSave}>
|
|
Save
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
</Modal>
|
|
);
|
|
}
|
|
|
|
export interface ChangeLocomotivesModalProps {
|
|
composition: TrainComposition | null;
|
|
opened: boolean;
|
|
onClose: () => void;
|
|
}
|