mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
- planned couples: loose wagons join the train at a route stop, added from the schedule yards tab; capacity credits them per corridor edge and coupling validates locomotive weight/length caps per leg - real-cut toggle: a cut wagon permanently leaves the train build at its cut yard (soft cut still sits out one trip only) - fix heaviest-leg display counting a shared slot's full cargo on every spanned edge (phantom pull-weight overload on S-2026-00045) - confirmation dialogs for workspace add/load/unload/remove actions - train-builder History and Detached-wagons tabs, backed by paginated endpoints; builder detaches now always write adjustment-log rows Migrations 3660 (planned_wagon_couples, planned_wagon_real_cuts) and 3670 (adjustment log train_schedule_id nullable) — both applied to the dev DB by hand; watch mode does not run migrations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
195 lines
6.6 KiB
TypeScript
195 lines
6.6 KiB
TypeScript
import {
|
|
Badge,
|
|
Button,
|
|
Checkbox,
|
|
Group,
|
|
Pagination,
|
|
Paper,
|
|
Stack,
|
|
Table,
|
|
Text,
|
|
ThemeIcon,
|
|
Tooltip,
|
|
} from "@mantine/core";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { Link2, MapPin, PackageOpen, User } from "lucide-react";
|
|
import { useState } from "react";
|
|
|
|
import { api } from "@/services/api";
|
|
|
|
interface Props {
|
|
trainId: string;
|
|
/** Staff may attach and the train is editable (not out on a run). */
|
|
canAttach: boolean;
|
|
attachPending: boolean;
|
|
onAttach: (wagonIds: string[]) => void;
|
|
}
|
|
|
|
/**
|
|
* "Detached wagons" tab: wagons last detached from THIS train that are still
|
|
* loose — with when, where and by whom they were detached — so staff can pick
|
|
* them straight back onto the consist without hunting through the global pool.
|
|
*/
|
|
export default function DetachedWagonsPanel({
|
|
trainId,
|
|
canAttach,
|
|
attachPending,
|
|
onAttach,
|
|
}: Props) {
|
|
const [page, setPage] = useState(1);
|
|
const query = useQuery(
|
|
api.trainBuilder.detachedWagons.queryOptions({
|
|
input: { id: trainId, page, pageSize: 20 },
|
|
enabled: Boolean(trainId),
|
|
// Keep the previous page on screen while the next one loads.
|
|
placeholderData: (prev) => prev,
|
|
}),
|
|
);
|
|
const rows = query.data?.items ?? [];
|
|
const totalPages = Math.max(1, query.data?.meta.totalPages ?? 1);
|
|
// 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());
|
|
const allSelected = rows.length > 0 && rows.every((r) => selected.has(r.wagonId));
|
|
|
|
const toggle = (wagonId: string, checked: boolean) =>
|
|
setSelected((prev) => {
|
|
const next = new Set(prev);
|
|
if (checked) next.add(wagonId);
|
|
else next.delete(wagonId);
|
|
return next;
|
|
});
|
|
|
|
return (
|
|
<Paper radius="xl" p="lg" withBorder>
|
|
<Stack gap="lg">
|
|
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
|
<Group gap="md" align="center" wrap="nowrap">
|
|
<ThemeIcon size={44} radius="md" variant="light" color="orange">
|
|
<PackageOpen size={22} />
|
|
</ThemeIcon>
|
|
<Stack gap={2}>
|
|
<Text fw={700} fz="lg">
|
|
Detached wagons
|
|
</Text>
|
|
<Text size="sm" c="dimmed">
|
|
Wagons that left this train and are still loose — select and
|
|
attach them back in one click.
|
|
</Text>
|
|
</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>
|
|
) : null}
|
|
</Group>
|
|
|
|
{query.isLoading ? (
|
|
<Text size="sm" c="dimmed" ta="center" py="md">
|
|
Loading detached wagons…
|
|
</Text>
|
|
) : rows.length === 0 ? (
|
|
<Text size="sm" c="dimmed" ta="center" py="md">
|
|
No loose wagons were detached from this train — detach history starts
|
|
being recorded from now on.
|
|
</Text>
|
|
) : (
|
|
<Table striped highlightOnHover withTableBorder>
|
|
<Table.Thead>
|
|
<Table.Tr>
|
|
{canAttach ? (
|
|
<Table.Th w={36}>
|
|
<Checkbox
|
|
checked={allSelected}
|
|
indeterminate={selected.size > 0 && !allSelected}
|
|
onChange={(e) =>
|
|
setSelected(
|
|
e.currentTarget.checked
|
|
? new Set(rows.map((r) => r.wagonId))
|
|
: new Set(),
|
|
)
|
|
}
|
|
/>
|
|
</Table.Th>
|
|
) : null}
|
|
<Table.Th>Wagon</Table.Th>
|
|
<Table.Th>Type</Table.Th>
|
|
<Table.Th>Now standing at</Table.Th>
|
|
<Table.Th>Last detached</Table.Th>
|
|
</Table.Tr>
|
|
</Table.Thead>
|
|
<Table.Tbody>
|
|
{rows.map((r) => (
|
|
<Table.Tr key={r.wagonId}>
|
|
{canAttach ? (
|
|
<Table.Td>
|
|
<Checkbox
|
|
checked={selected.has(r.wagonId)}
|
|
onChange={(e) => toggle(r.wagonId, e.currentTarget.checked)}
|
|
/>
|
|
</Table.Td>
|
|
) : null}
|
|
<Table.Td>
|
|
<Text fw={600} size="sm" ff="monospace">
|
|
{r.wagonNumber}
|
|
</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Badge size="sm" variant="light" color="gray">
|
|
{r.wagonTypeCode ?? "—"}
|
|
</Badge>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Text size="sm">{r.currentYardLabel ?? "No yard"}</Text>
|
|
</Table.Td>
|
|
<Table.Td>
|
|
<Group gap="md" wrap="wrap">
|
|
<Tooltip label={new Date(r.detachedAt).toLocaleString()}>
|
|
<Text size="sm">{new Date(r.detachedAt).toLocaleDateString()}</Text>
|
|
</Tooltip>
|
|
{r.detachedYardLabel ? (
|
|
<Group gap={4} wrap="nowrap">
|
|
<MapPin size={12} />
|
|
<Text size="xs" c="dimmed">
|
|
at {r.detachedYardLabel}
|
|
</Text>
|
|
</Group>
|
|
) : null}
|
|
{r.detachedBy ? (
|
|
<Group gap={4} wrap="nowrap">
|
|
<User size={12} />
|
|
<Text size="xs" c="dimmed">
|
|
by {r.detachedBy}
|
|
</Text>
|
|
</Group>
|
|
) : null}
|
|
</Group>
|
|
</Table.Td>
|
|
</Table.Tr>
|
|
))}
|
|
</Table.Tbody>
|
|
</Table>
|
|
)}
|
|
|
|
{totalPages > 1 ? (
|
|
<Group justify="space-between">
|
|
<Text size="xs" c="dimmed">
|
|
{query.data?.meta.total ?? 0} wagon(s) · selection carries across pages
|
|
</Text>
|
|
<Pagination size="sm" value={page} onChange={setPage} total={totalPages} />
|
|
</Group>
|
|
) : null}
|
|
</Stack>
|
|
</Paper>
|
|
);
|
|
}
|