Files
edr-platform/apps/edr-freight-web/backoffice/src/components/trainBuilder/ChangeLocomotivesModal.tsx
Marshal 603537a20b add yard distances management to rule engine
- 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.
2026-07-21 08:50:50 +00:00

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;
}