mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
153 lines
4.5 KiB
TypeScript
153 lines
4.5 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) byId.set(wagon.wagonType.id, 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 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>
|
|
|
|
<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;
|
|
}
|