mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 02:28:18 +00:00
train
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user