mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 01:20:55 +00:00
187 lines
5.4 KiB
TypeScript
187 lines
5.4 KiB
TypeScript
import { Freight } from "@edr/types";
|
|
import {
|
|
Button,
|
|
Checkbox,
|
|
Group,
|
|
ScrollArea,
|
|
Select,
|
|
Stack,
|
|
Text,
|
|
TextInput,
|
|
} from "@mantine/core";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { Plus, Search } from "lucide-react";
|
|
import { useMemo, useState } from "react";
|
|
|
|
import { api } from "@/services/api";
|
|
|
|
/**
|
|
* AVAILABLE wagons standing in the train's own yard — the only ones that can
|
|
* be coupled. Pick any number and append them to the consist.
|
|
*/
|
|
export default function AvailableWagonsPanel({
|
|
yardId,
|
|
yardLabel,
|
|
onAssign,
|
|
assigning,
|
|
}: AvailableWagonsPanelProps) {
|
|
const [search, setSearch] = useState("");
|
|
const [typeFilter, setTypeFilter] = useState<string>("ALL");
|
|
const [selected, setSelected] = useState<string[]>([]);
|
|
|
|
const wagonsQuery = useQuery(
|
|
api.wagons.list.queryOptions({
|
|
input: {
|
|
filters: { status: Freight.WagonStatus.Available, currentYardId: yardId },
|
|
},
|
|
enabled: Boolean(yardId),
|
|
}),
|
|
);
|
|
|
|
const wagons = useMemo(() => {
|
|
const q = search.trim().toLowerCase();
|
|
return (wagonsQuery.data ?? []).filter((wagon) => {
|
|
if (typeFilter !== "ALL" && wagon.wagonTypeId !== typeFilter) return false;
|
|
if (q && !wagon.wagonNumber.toLowerCase().includes(q)) return false;
|
|
return true;
|
|
});
|
|
}, [wagonsQuery.data, search, typeFilter]);
|
|
|
|
const typeOptions = useMemo(() => {
|
|
const byId = new Map<string, string>();
|
|
for (const wagon of wagonsQuery.data ?? []) {
|
|
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 [
|
|
{ value: "ALL", label: "All types" },
|
|
...[...byId.entries()].map(([value, label]) => ({ value, label })),
|
|
];
|
|
}, [wagonsQuery.data]);
|
|
|
|
const toggle = (wagonId: string, checked: boolean) => {
|
|
setSelected((prev) =>
|
|
checked ? [...prev, wagonId] : prev.filter((id) => id !== wagonId),
|
|
);
|
|
};
|
|
|
|
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 = () => {
|
|
if (!selected.length) return;
|
|
onAssign(selected);
|
|
setSelected([]);
|
|
};
|
|
|
|
return (
|
|
<Stack gap="sm">
|
|
<Group gap="xs" grow>
|
|
<TextInput
|
|
size="sm"
|
|
placeholder="Search wagon number…"
|
|
leftSection={<Search size={14} />}
|
|
value={search}
|
|
onChange={(e) => setSearch(e.currentTarget.value)}
|
|
/>
|
|
<Select
|
|
size="sm"
|
|
data={typeOptions}
|
|
value={typeFilter}
|
|
onChange={(v) => setTypeFilter(v ?? "ALL")}
|
|
/>
|
|
</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">
|
|
<Stack gap={6}>
|
|
{wagonsQuery.isLoading ? (
|
|
<Text py="md" ta="center" c="dimmed" size="sm">
|
|
Loading wagons…
|
|
</Text>
|
|
) : !wagons.length ? (
|
|
<Text py="md" ta="center" c="dimmed" size="sm">
|
|
No available wagons in {yardLabel ?? "this yard"}
|
|
</Text>
|
|
) : (
|
|
wagons.map((wagon) => (
|
|
<Group
|
|
key={wagon.id}
|
|
gap="sm"
|
|
wrap="nowrap"
|
|
p="xs"
|
|
style={{
|
|
border: "1px solid var(--mantine-color-gray-2)",
|
|
borderRadius: "var(--mantine-radius-md)",
|
|
}}
|
|
>
|
|
<Checkbox
|
|
size="sm"
|
|
checked={selected.includes(wagon.id)}
|
|
onChange={(e) => toggle(wagon.id, e.currentTarget.checked)}
|
|
aria-label={`Select wagon ${wagon.wagonNumber}`}
|
|
/>
|
|
<Stack gap={0} style={{ flex: 1, minWidth: 0 }}>
|
|
<Text size="sm" fw={600} ff="monospace" truncate>
|
|
{wagon.wagonNumber}
|
|
</Text>
|
|
<Text size="xs" c="dimmed" truncate>
|
|
{wagon.wagonType
|
|
? `${wagon.wagonType.name} · ${wagon.wagonType.capacityTons ?? "—"}T cap`
|
|
: "Unknown type"}
|
|
</Text>
|
|
</Stack>
|
|
</Group>
|
|
))
|
|
)}
|
|
</Stack>
|
|
</ScrollArea.Autosize>
|
|
|
|
<Button
|
|
leftSection={<Plus size={16} />}
|
|
disabled={!selected.length}
|
|
loading={assigning}
|
|
onClick={handleAssign}
|
|
>
|
|
Add {selected.length ? `${selected.length} wagon${selected.length > 1 ? "s" : ""}` : "wagons"} to consist
|
|
</Button>
|
|
</Stack>
|
|
);
|
|
}
|
|
|
|
export interface AvailableWagonsPanelProps {
|
|
yardId: string;
|
|
yardLabel?: string | null;
|
|
onAssign: (wagonIds: string[]) => void;
|
|
assigning: boolean;
|
|
}
|