mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
implement wagon maintenance feature: add functionality to detach wagons and move them to maintenance status
This commit is contained in:
@@ -10,11 +10,6 @@ import {
|
|||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
|
|
||||||
export class BuildTrainDto {
|
export class BuildTrainDto {
|
||||||
@ApiProperty({ example: '81001', description: 'Operator-assigned train code (unique)' })
|
|
||||||
@IsString()
|
|
||||||
@MaxLength(32)
|
|
||||||
code!: string;
|
|
||||||
|
|
||||||
@ApiProperty({ example: '8001', description: 'EXPORT run number (odd, unique across trains)' })
|
@ApiProperty({ example: '8001', description: 'EXPORT run number (odd, unique across trains)' })
|
||||||
@IsString()
|
@IsString()
|
||||||
@MaxLength(20)
|
@MaxLength(20)
|
||||||
|
|||||||
@@ -85,6 +85,16 @@ export class TrainBuilderController {
|
|||||||
return this.trainBuilderService.removeWagon(id, wagonId);
|
return this.trainBuilderService.removeWagon(id, wagonId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post(':id/wagons/:wagonId/maintenance')
|
||||||
|
@FleetManage()
|
||||||
|
@ApiOperation({ summary: 'Detach one wagon and move it to MAINTENANCE status' })
|
||||||
|
sendWagonToMaintenance(
|
||||||
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
|
@Param('wagonId', ParseUUIDPipe) wagonId: string,
|
||||||
|
) {
|
||||||
|
return this.trainBuilderService.sendWagonToMaintenance(id, wagonId);
|
||||||
|
}
|
||||||
|
|
||||||
@Post(':id/reorder-wagons')
|
@Post(':id/reorder-wagons')
|
||||||
@FleetManage()
|
@FleetManage()
|
||||||
@ApiOperation({ summary: 'Persist a drag-reorder of the full consist' })
|
@ApiOperation({ summary: 'Persist a drag-reorder of the full consist' })
|
||||||
|
|||||||
@@ -59,11 +59,7 @@ export class TrainBuilderService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const trainId = await this.dataSource.transaction(async (manager) => {
|
const trainId = await this.dataSource.transaction(async (manager) => {
|
||||||
const code = dto.code.trim();
|
const code = await this.generateTrainCode(manager);
|
||||||
const existing = await manager.getRepository(Train).findOne({ where: { code } });
|
|
||||||
if (existing) {
|
|
||||||
throw new ConflictException(`Train code ${code} is already in use`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Friendly 409 before the partial unique indexes (the race-proof backstop):
|
// Friendly 409 before the partial unique indexes (the race-proof backstop):
|
||||||
// the typed pair may not collide with any train's pair or legacy number.
|
// the typed pair may not collide with any train's pair or legacy number.
|
||||||
@@ -418,6 +414,33 @@ export class TrainBuilderService {
|
|||||||
return this.getComposition(id);
|
return this.getComposition(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach one wagon AND flag it for maintenance: it leaves the consist and
|
||||||
|
* moves to MAINTENANCE status (not AVAILABLE), so it is not re-coupled until
|
||||||
|
* it clears maintenance. The freed sequence gap is closed.
|
||||||
|
*/
|
||||||
|
async sendWagonToMaintenance(id: string, wagonId: string) {
|
||||||
|
await this.dataSource.transaction(async (manager) => {
|
||||||
|
const train = await this.getEditableTrain(manager, id);
|
||||||
|
const wagon = await manager.getRepository(Wagon).findOne({ where: { id: wagonId } });
|
||||||
|
if (!wagon || wagon.trainId !== train.id) {
|
||||||
|
throw new NotFoundException(`Wagon ${wagonId} is not part of this train`);
|
||||||
|
}
|
||||||
|
if (wagon.currentTrainScheduleId) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Wagon ${wagon.wagonNumber} is pinned to an active schedule and cannot be removed`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await manager.getRepository(Wagon).update(wagon.id, {
|
||||||
|
trainId: null,
|
||||||
|
sequenceNumber: null,
|
||||||
|
status: WagonStatus.Maintenance,
|
||||||
|
});
|
||||||
|
await this.resequenceWagons(manager, train.id);
|
||||||
|
});
|
||||||
|
return this.getComposition(id);
|
||||||
|
}
|
||||||
|
|
||||||
/** Persist a drag-reorder: `wagonIds` is the full consist in its new order. */
|
/** Persist a drag-reorder: `wagonIds` is the full consist in its new order. */
|
||||||
async reorderWagons(id: string, dto: ReorderTrainWagonsDto) {
|
async reorderWagons(id: string, dto: ReorderTrainWagonsDto) {
|
||||||
await this.dataSource.transaction(async (manager) => {
|
await this.dataSource.transaction(async (manager) => {
|
||||||
@@ -469,6 +492,29 @@ export class TrainBuilderService {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- internals
|
// ---------------------------------------------------------------- internals
|
||||||
|
|
||||||
|
/**
|
||||||
|
* System-assigned train code `TR-NNNNN`. Draws the next number from the
|
||||||
|
* highest existing `TR-` code and probes past any manual collision so the
|
||||||
|
* unique constraint never rejects the build.
|
||||||
|
*/
|
||||||
|
private async generateTrainCode(manager: EntityManager): Promise<string> {
|
||||||
|
const [row]: { max_seq: string | null }[] = await manager.query(
|
||||||
|
`SELECT MAX(CAST(SUBSTRING(code FROM '^TR-([0-9]+)$') AS INTEGER)) AS max_seq
|
||||||
|
FROM freight.trains
|
||||||
|
WHERE code ~ '^TR-[0-9]+$'`,
|
||||||
|
);
|
||||||
|
let seq = Number(row?.max_seq ?? 0) + 1;
|
||||||
|
for (let attempt = 0; attempt < 50; attempt += 1) {
|
||||||
|
const code = `TR-${String(seq).padStart(5, '0')}`;
|
||||||
|
const exists = await manager
|
||||||
|
.getRepository(Train)
|
||||||
|
.findOne({ where: { code }, withDeleted: true });
|
||||||
|
if (!exists) return code;
|
||||||
|
seq += 1;
|
||||||
|
}
|
||||||
|
throw new ConflictException('Could not allocate a unique train code');
|
||||||
|
}
|
||||||
|
|
||||||
private mapSummary(train: Train, activeSchedule: ActiveScheduleRef | null) {
|
private mapSummary(train: Train, activeSchedule: ActiveScheduleRef | null) {
|
||||||
const locomotives = [...(train.locomotives ?? [])]
|
const locomotives = [...(train.locomotives ?? [])]
|
||||||
.sort((a, b) => a.sequenceNo - b.sequenceNo)
|
.sort((a, b) => a.sequenceNo - b.sequenceNo)
|
||||||
|
|||||||
@@ -50,7 +50,15 @@ export default function AvailableWagonsPanel({
|
|||||||
const typeOptions = useMemo(() => {
|
const typeOptions = useMemo(() => {
|
||||||
const byId = new Map<string, string>();
|
const byId = new Map<string, string>();
|
||||||
for (const wagon of wagonsQuery.data ?? []) {
|
for (const wagon of wagonsQuery.data ?? []) {
|
||||||
if (wagon.wagonType) byId.set(wagon.wagonType.id, wagon.wagonType.name);
|
if (wagon.wagonType) {
|
||||||
|
// e.g. "Flat wagon (NW5)" — name with its type code.
|
||||||
|
byId.set(
|
||||||
|
wagon.wagonType.id,
|
||||||
|
wagon.wagonType.code
|
||||||
|
? `${wagon.wagonType.name} (${wagon.wagonType.code})`
|
||||||
|
: wagon.wagonType.name,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
{ value: "ALL", label: "All types" },
|
{ value: "ALL", label: "All types" },
|
||||||
@@ -64,6 +72,22 @@ export default function AvailableWagonsPanel({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const allSelected =
|
||||||
|
wagons.length > 0 && wagons.every((w) => selected.includes(w.id));
|
||||||
|
const someSelected = wagons.some((w) => selected.includes(w.id));
|
||||||
|
|
||||||
|
const toggleAll = (checked: boolean) => {
|
||||||
|
setSelected((prev) => {
|
||||||
|
if (checked) {
|
||||||
|
const ids = new Set(prev);
|
||||||
|
wagons.forEach((w) => ids.add(w.id));
|
||||||
|
return [...ids];
|
||||||
|
}
|
||||||
|
const visible = new Set(wagons.map((w) => w.id));
|
||||||
|
return prev.filter((id) => !visible.has(id));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleAssign = () => {
|
const handleAssign = () => {
|
||||||
if (!selected.length) return;
|
if (!selected.length) return;
|
||||||
onAssign(selected);
|
onAssign(selected);
|
||||||
@@ -88,6 +112,16 @@ export default function AvailableWagonsPanel({
|
|||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
{wagons.length ? (
|
||||||
|
<Checkbox
|
||||||
|
size="sm"
|
||||||
|
label={`Select all (${wagons.length})`}
|
||||||
|
checked={allSelected}
|
||||||
|
indeterminate={!allSelected && someSelected}
|
||||||
|
onChange={(e) => toggleAll(e.currentTarget.checked)}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<ScrollArea.Autosize mah={380} type="auto">
|
<ScrollArea.Autosize mah={380} type="auto">
|
||||||
<Stack gap={6}>
|
<Stack gap={6}>
|
||||||
{wagonsQuery.isLoading ? (
|
{wagonsQuery.isLoading ? (
|
||||||
|
|||||||
@@ -31,13 +31,12 @@ const isOddNumber = (value: string) => /^\d*[13579]$/.test(value.trim());
|
|||||||
const isEvenNumber = (value: string) => /^\d*[02468]$/.test(value.trim());
|
const isEvenNumber = (value: string) => /^\d*[02468]$/.test(value.trim());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Step one of the Train Builder: give the train its operator code, pick the
|
* Step one of the Train Builder: pick the yard it is being assembled in and
|
||||||
* yard it is being assembled in, and couple at least two locomotives from that
|
* couple at least two locomotives from that yard. The train code is assigned by
|
||||||
* yard. Wagons are attached afterwards on the composition page.
|
* the system. Wagons are attached afterwards on the composition page.
|
||||||
*/
|
*/
|
||||||
export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrainModalProps) {
|
export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrainModalProps) {
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const [code, setCode] = useState("");
|
|
||||||
const [exportTrainNumber, setExportTrainNumber] = useState("");
|
const [exportTrainNumber, setExportTrainNumber] = useState("");
|
||||||
const [importTrainNumber, setImportTrainNumber] = useState("");
|
const [importTrainNumber, setImportTrainNumber] = useState("");
|
||||||
const [trainName, setTrainName] = useState("");
|
const [trainName, setTrainName] = useState("");
|
||||||
@@ -62,7 +61,6 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!opened) {
|
if (!opened) {
|
||||||
setCode("");
|
|
||||||
setExportTrainNumber("");
|
setExportTrainNumber("");
|
||||||
setImportTrainNumber("");
|
setImportTrainNumber("");
|
||||||
setTrainName("");
|
setTrainName("");
|
||||||
@@ -73,9 +71,9 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
|||||||
}, [opened]);
|
}, [opened]);
|
||||||
|
|
||||||
const handleBuild = async () => {
|
const handleBuild = async () => {
|
||||||
if (!code.trim() || !yardId || locomotiveIds.length < 2) {
|
if (!yardId || locomotiveIds.length < 2) {
|
||||||
toast({
|
toast({
|
||||||
title: "Enter a train code, pick a yard, and couple at least two locomotives",
|
title: "Pick a yard and couple at least two locomotives",
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -89,7 +87,6 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const composition = await build.mutateAsync({
|
const composition = await build.mutateAsync({
|
||||||
code: code.trim(),
|
|
||||||
exportTrainNumber: exportTrainNumber.trim(),
|
exportTrainNumber: exportTrainNumber.trim(),
|
||||||
importTrainNumber: importTrainNumber.trim(),
|
importTrainNumber: importTrainNumber.trim(),
|
||||||
currentYardId: yardId,
|
currentYardId: yardId,
|
||||||
@@ -125,24 +122,16 @@ export default function BuildTrainModal({ opened, onClose, onBuilt }: BuildTrain
|
|||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
A train is assembled in one yard: two or more locomotives plus wagons
|
A train is assembled in one yard: two or more locomotives plus wagons
|
||||||
standing in that same yard. Wagons are attached on the next screen.
|
standing in that same yard. The train code is assigned automatically;
|
||||||
|
wagons are attached on the next screen.
|
||||||
</Text>
|
</Text>
|
||||||
<Group grow>
|
<TextInput
|
||||||
<TextInput
|
label="Name (optional)"
|
||||||
label="Train code"
|
placeholder="e.g. Fertilizer block"
|
||||||
placeholder="e.g. 81001"
|
value={trainName}
|
||||||
value={code}
|
onChange={(e) => setTrainName(e.currentTarget.value)}
|
||||||
onChange={(e) => setCode(e.currentTarget.value)}
|
maxLength={100}
|
||||||
maxLength={32}
|
/>
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Name (optional)"
|
|
||||||
placeholder="e.g. Fertilizer block"
|
|
||||||
value={trainName}
|
|
||||||
onChange={(e) => setTrainName(e.currentTarget.value)}
|
|
||||||
maxLength={100}
|
|
||||||
/>
|
|
||||||
</Group>
|
|
||||||
<Group grow>
|
<Group grow>
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Export train number"
|
label="Export train number"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
type DropResult,
|
type DropResult,
|
||||||
} from "@hello-pangea/dnd";
|
} from "@hello-pangea/dnd";
|
||||||
import { ActionIcon, Badge, Box, Group, Stack, Text, Tooltip } from "@mantine/core";
|
import { ActionIcon, Badge, Box, Group, Stack, Text, Tooltip } from "@mantine/core";
|
||||||
import { GripVertical, Trash2 } from "lucide-react";
|
import { GripVertical, Trash2, Wrench } from "lucide-react";
|
||||||
import { type ReactNode } from "react";
|
import { type ReactNode } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ export default function ConsistWagonList({
|
|||||||
editable,
|
editable,
|
||||||
onReorder,
|
onReorder,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
onMaintenance,
|
||||||
busy = false,
|
busy = false,
|
||||||
}: ConsistWagonListProps) {
|
}: ConsistWagonListProps) {
|
||||||
const onDragEnd = (result: DropResult) => {
|
const onDragEnd = (result: DropResult) => {
|
||||||
@@ -78,6 +79,7 @@ export default function ConsistWagonList({
|
|||||||
editable={editable}
|
editable={editable}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
onRemove={onRemove}
|
onRemove={onRemove}
|
||||||
|
onMaintenance={onMaintenance}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
@@ -95,6 +97,8 @@ export interface ConsistWagonListProps {
|
|||||||
editable: boolean;
|
editable: boolean;
|
||||||
onReorder: (wagonIds: string[]) => void;
|
onReorder: (wagonIds: string[]) => void;
|
||||||
onRemove: (wagonId: string) => void;
|
onRemove: (wagonId: string) => void;
|
||||||
|
/** Detach the wagon and move it to MAINTENANCE status. */
|
||||||
|
onMaintenance: (wagonId: string) => void;
|
||||||
busy?: boolean;
|
busy?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +110,7 @@ function WagonRow({
|
|||||||
editable,
|
editable,
|
||||||
busy,
|
busy,
|
||||||
onRemove,
|
onRemove,
|
||||||
|
onMaintenance,
|
||||||
}: {
|
}: {
|
||||||
wagon: TrainCompositionWagon;
|
wagon: TrainCompositionWagon;
|
||||||
index: number;
|
index: number;
|
||||||
@@ -114,6 +119,7 @@ function WagonRow({
|
|||||||
editable: boolean;
|
editable: boolean;
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
onRemove: (wagonId: string) => void;
|
onRemove: (wagonId: string) => void;
|
||||||
|
onMaintenance: (wagonId: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<PortalAwareRow snapshot={snapshot}>
|
<PortalAwareRow snapshot={snapshot}>
|
||||||
@@ -153,17 +159,30 @@ function WagonRow({
|
|||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
{editable ? (
|
{editable ? (
|
||||||
<Tooltip label="Detach wagon" withArrow>
|
<Group gap={4} wrap="nowrap">
|
||||||
<ActionIcon
|
<Tooltip label="Send to maintenance (detaches)" withArrow>
|
||||||
variant="subtle"
|
<ActionIcon
|
||||||
color="red"
|
variant="subtle"
|
||||||
disabled={busy}
|
color="orange"
|
||||||
onClick={() => onRemove(wagon.id)}
|
disabled={busy}
|
||||||
aria-label={`Detach wagon ${wagon.wagonNumber}`}
|
onClick={() => onMaintenance(wagon.id)}
|
||||||
>
|
aria-label={`Send wagon ${wagon.wagonNumber} to maintenance`}
|
||||||
<Trash2 size={16} />
|
>
|
||||||
</ActionIcon>
|
<Wrench size={16} />
|
||||||
</Tooltip>
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="Detach wagon" withArrow>
|
||||||
|
<ActionIcon
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => onRemove(wagon.id)}
|
||||||
|
aria-label={`Detach wagon ${wagon.wagonNumber}`}
|
||||||
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
) : null}
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
</PortalAwareRow>
|
</PortalAwareRow>
|
||||||
|
|||||||
@@ -76,12 +76,18 @@ export default function TrainBuilderDetailPage() {
|
|||||||
);
|
);
|
||||||
const assignWagons = useMutation(api.trainBuilder.assignWagons.mutationOptions());
|
const assignWagons = useMutation(api.trainBuilder.assignWagons.mutationOptions());
|
||||||
const removeWagon = useMutation(api.trainBuilder.removeWagon.mutationOptions());
|
const removeWagon = useMutation(api.trainBuilder.removeWagon.mutationOptions());
|
||||||
|
const maintenanceWagon = useMutation(
|
||||||
|
api.trainBuilder.sendWagonToMaintenance.mutationOptions(),
|
||||||
|
);
|
||||||
const reorderWagons = useMutation(api.trainBuilder.reorderWagons.mutationOptions());
|
const reorderWagons = useMutation(api.trainBuilder.reorderWagons.mutationOptions());
|
||||||
const disband = useMutation(api.trainBuilder.disband.mutationOptions());
|
const disband = useMutation(api.trainBuilder.disband.mutationOptions());
|
||||||
|
|
||||||
const composition = compositionQuery.data;
|
const composition = compositionQuery.data;
|
||||||
const busy =
|
const busy =
|
||||||
assignWagons.isPending || removeWagon.isPending || reorderWagons.isPending;
|
assignWagons.isPending ||
|
||||||
|
removeWagon.isPending ||
|
||||||
|
maintenanceWagon.isPending ||
|
||||||
|
reorderWagons.isPending;
|
||||||
|
|
||||||
const withToast = async (action: () => Promise<unknown>, failTitle: string) => {
|
const withToast = async (action: () => Promise<unknown>, failTitle: string) => {
|
||||||
try {
|
try {
|
||||||
@@ -284,6 +290,12 @@ export default function TrainBuilderDetailPage() {
|
|||||||
"Could not detach wagon",
|
"Could not detach wagon",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onMaintenance={(wagonId) =>
|
||||||
|
void withToast(
|
||||||
|
() => maintenanceWagon.mutateAsync({ id: composition.id, wagonId }),
|
||||||
|
"Could not send wagon to maintenance",
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1850,6 +1850,15 @@ export const api = {
|
|||||||
() => TRAIN_BUILDER_INVALIDATIONS,
|
() => TRAIN_BUILDER_INVALIDATIONS,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
sendWagonToMaintenance: endpoint<{ id: string; wagonId: string }, TrainComposition>(
|
||||||
|
"train-builder",
|
||||||
|
"sendWagonToMaintenance",
|
||||||
|
({ id, wagonId }) =>
|
||||||
|
trainBuilderService.sendWagonToMaintenance(id, wagonId).then((r) => r.data),
|
||||||
|
undefined,
|
||||||
|
() => TRAIN_BUILDER_INVALIDATIONS,
|
||||||
|
),
|
||||||
|
|
||||||
reorderWagons: endpoint<{ id: string; wagonIds: string[] }, TrainComposition>(
|
reorderWagons: endpoint<{ id: string; wagonIds: string[] }, TrainComposition>(
|
||||||
"train-builder",
|
"train-builder",
|
||||||
"reorderWagons",
|
"reorderWagons",
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ export interface BuiltTrainListResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface BuildTrainPayload {
|
export interface BuildTrainPayload {
|
||||||
code: string;
|
|
||||||
/** EXPORT run number — odd, unique across trains (e.g. 8001). */
|
/** EXPORT run number — odd, unique across trains (e.g. 8001). */
|
||||||
exportTrainNumber: string;
|
exportTrainNumber: string;
|
||||||
/** IMPORT run number — even, unique across trains (e.g. 8002). */
|
/** IMPORT run number — even, unique across trains (e.g. 8002). */
|
||||||
@@ -249,6 +248,9 @@ export const trainBuilderService = {
|
|||||||
apiClient.post<TrainComposition>(`${BASE}/${id}/wagons`, { wagonIds }),
|
apiClient.post<TrainComposition>(`${BASE}/${id}/wagons`, { wagonIds }),
|
||||||
removeWagon: (id: string, wagonId: string) =>
|
removeWagon: (id: string, wagonId: string) =>
|
||||||
apiClient.delete<TrainComposition>(`${BASE}/${id}/wagons/${wagonId}`),
|
apiClient.delete<TrainComposition>(`${BASE}/${id}/wagons/${wagonId}`),
|
||||||
|
/** Detach a wagon and move it to MAINTENANCE status. */
|
||||||
|
sendWagonToMaintenance: (id: string, wagonId: string) =>
|
||||||
|
apiClient.post<TrainComposition>(`${BASE}/${id}/wagons/${wagonId}/maintenance`),
|
||||||
reorderWagons: (id: string, wagonIds: string[]) =>
|
reorderWagons: (id: string, wagonIds: string[]) =>
|
||||||
apiClient.post<TrainComposition>(`${BASE}/${id}/reorder-wagons`, { wagonIds }),
|
apiClient.post<TrainComposition>(`${BASE}/${id}/reorder-wagons`, { wagonIds }),
|
||||||
disband: (id: string) => apiClient.delete<void>(`${BASE}/${id}`),
|
disband: (id: string) => apiClient.delete<void>(`${BASE}/${id}`),
|
||||||
|
|||||||
Reference in New Issue
Block a user