mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
feat: enhance train scheduling and contract management features
- Added StationWorkControls to manage loading/unloading phases in TrainScheduleV2DetailPage. - Implemented API endpoints for recording station work and managing wagon detach requests. - Updated contract templates to include Ethiopian customs handling options. - Enhanced shipment forms to collect customs clearing agent details for without-customs bookings. - Introduced NUMBER_OF_WAGONS as a unit of measure for bulk cargo, allowing customers to specify wagon counts. - Improved validation for customs clearing agent information in shipment forms. - Updated various components and services to accommodate new features and ensure data integrity.
This commit is contained in:
@@ -5,18 +5,30 @@ import {
|
||||
Group,
|
||||
Pagination,
|
||||
Paper,
|
||||
Select,
|
||||
Stack,
|
||||
Table,
|
||||
Text,
|
||||
ThemeIcon,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Link2, MapPin, PackageOpen, User } from "lucide-react";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { isAxiosError } from "axios";
|
||||
import { ArrowRightLeft, Link2, MapPin, PackageOpen, User } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { api } from "@/services/api";
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
trainId: string;
|
||||
/** Staff may attach and the train is editable (not out on a run). */
|
||||
@@ -50,6 +62,47 @@ export default function DetachedWagonsPanel({
|
||||
// Selection is page-scoped in the header checkbox but survives paging, so
|
||||
// staff can gather wagons across pages into one attach.
|
||||
const [selected, setSelected] = useState<ReadonlySet<string>>(new Set());
|
||||
|
||||
// Attach the selection to a DIFFERENT built train: pick a target, reuse the
|
||||
// same assign endpoint with that train's id. The builder attach is
|
||||
// yard-agnostic, so any loose AVAILABLE wagon qualifies; a train that is
|
||||
// out on a run rejects server-side and is disabled here too.
|
||||
const { toast } = useToast();
|
||||
const [targetTrainId, setTargetTrainId] = useState<string | null>(null);
|
||||
const trainsQuery = useQuery(
|
||||
api.trainBuilder.list.queryOptions({
|
||||
input: { filters: { pageSize: 200, sortBy: "code", sortOrder: "ASC" } },
|
||||
enabled: canAttach,
|
||||
staleTime: 60_000,
|
||||
}),
|
||||
);
|
||||
const trainOptions = (trainsQuery.data?.items ?? [])
|
||||
.filter((t) => t.id !== trainId)
|
||||
.map((t) => ({
|
||||
value: t.id,
|
||||
label: `${t.code}${t.trainName ? ` · ${t.trainName}` : ""} — ${t.wagonCount} wagon${t.wagonCount === 1 ? "" : "s"}${t.status === "IN_SERVICE" ? " (in service)" : ""}`,
|
||||
disabled: t.status === "IN_SERVICE",
|
||||
}));
|
||||
const attachOther = useMutation(api.trainBuilder.assignWagons.mutationOptions());
|
||||
const handleAttachOther = async () => {
|
||||
if (!targetTrainId || !selected.size) return;
|
||||
const target = trainsQuery.data?.items.find((t) => t.id === targetTrainId);
|
||||
try {
|
||||
await attachOther.mutateAsync({ id: targetTrainId, wagonIds: [...selected] });
|
||||
toast({
|
||||
title: `${selected.size} wagon(s) attached to ${target?.code ?? "the selected train"}`,
|
||||
});
|
||||
setSelected(new Set());
|
||||
setTargetTrainId(null);
|
||||
void query.refetch();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Could not attach to the other train",
|
||||
description: parseError(error, "The target train may be out on a run."),
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.wagonId));
|
||||
|
||||
const toggle = (wagonId: string, checked: boolean) =>
|
||||
@@ -79,17 +132,40 @@ export default function DetachedWagonsPanel({
|
||||
</Stack>
|
||||
</Group>
|
||||
{canAttach ? (
|
||||
<Button
|
||||
leftSection={<Link2 size={16} />}
|
||||
disabled={selected.size === 0}
|
||||
loading={attachPending}
|
||||
onClick={() => {
|
||||
onAttach([...selected]);
|
||||
setSelected(new Set());
|
||||
}}
|
||||
>
|
||||
Attach {selected.size || ""} wagon{selected.size === 1 ? "" : "s"}
|
||||
</Button>
|
||||
<Group gap="sm" align="flex-end" wrap="wrap">
|
||||
<Button
|
||||
leftSection={<Link2 size={16} />}
|
||||
disabled={selected.size === 0}
|
||||
loading={attachPending}
|
||||
onClick={() => {
|
||||
onAttach([...selected]);
|
||||
setSelected(new Set());
|
||||
}}
|
||||
>
|
||||
Attach {selected.size || ""} wagon{selected.size === 1 ? "" : "s"}
|
||||
</Button>
|
||||
<Select
|
||||
size="sm"
|
||||
w={280}
|
||||
searchable
|
||||
clearable
|
||||
placeholder="Or pick another train…"
|
||||
maxDropdownHeight={350}
|
||||
data={trainOptions}
|
||||
value={targetTrainId}
|
||||
onChange={setTargetTrainId}
|
||||
nothingFoundMessage="No other built trains"
|
||||
/>
|
||||
<Button
|
||||
variant="light"
|
||||
leftSection={<ArrowRightLeft size={16} />}
|
||||
disabled={selected.size === 0 || !targetTrainId}
|
||||
loading={attachOther.isPending}
|
||||
onClick={() => void handleAttachOther()}
|
||||
>
|
||||
Attach to that train
|
||||
</Button>
|
||||
</Group>
|
||||
) : null}
|
||||
</Group>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user