Files
edr-platform/apps/edr-freight-web/backoffice/src/components/trainBuilder/AvailableWagonsPanel.tsx
Marshal 6d0cf50b4d train
2026-07-14 11:06:49 +00:00

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;
}