Merge pull request #762 from Tria-plc/freight/feature/user_management_UI

Freight/feature/user management UI
This commit is contained in:
yaschalew10
2026-07-17 13:38:55 +03:00
committed by GitHub
3 changed files with 60 additions and 4 deletions

View File

@@ -1,5 +1,6 @@
import { Freight } from "@edr/types";
import {
Badge,
Button,
Checkbox,
Group,
@@ -24,11 +25,19 @@ export default function AvailableWagonsPanel({
yardLabel,
onAssign,
assigning,
exportTrainNumber,
importTrainNumber,
}: AvailableWagonsPanelProps) {
const [search, setSearch] = useState("");
const [typeFilter, setTypeFilter] = useState<string>("ALL");
const [runOnly, setRunOnly] = useState(false);
const [selected, setSelected] = useState<string[]>([]);
// The train's own run, e.g. "8001-8002" — only offered when the train has one.
const runLabel = exportTrainNumber
? `${exportTrainNumber}${importTrainNumber ? `-${importTrainNumber}` : ""}`
: null;
const wagonsQuery = useQuery(
api.wagons.list.queryOptions({
input: {
@@ -42,10 +51,23 @@ export default function AvailableWagonsPanel({
const q = search.trim().toLowerCase();
return (wagonsQuery.data ?? []).filter((wagon) => {
if (typeFilter !== "ALL" && wagon.wagonTypeId !== typeFilter) return false;
// Rostered to this train's run — match on the export run, which fixes the
// import run anyway.
if (runOnly && wagon.exportTrainNumber !== exportTrainNumber) return false;
if (q && !wagon.wagonNumber.toLowerCase().includes(q)) return false;
return true;
});
}, [wagonsQuery.data, search, typeFilter]);
}, [wagonsQuery.data, search, typeFilter, runOnly, exportTrainNumber]);
const runMatchCount = useMemo(
() =>
exportTrainNumber
? (wagonsQuery.data ?? []).filter(
(w) => w.exportTrainNumber === exportTrainNumber,
).length
: 0,
[wagonsQuery.data, exportTrainNumber],
);
const typeOptions = useMemo(() => {
const byId = new Map<string, string>();
@@ -112,6 +134,15 @@ export default function AvailableWagonsPanel({
/>
</Group>
{runLabel ? (
<Checkbox
size="sm"
label={`Only wagons on this train's run (${runLabel}) — ${runMatchCount} here`}
checked={runOnly}
onChange={(e) => setRunOnly(e.currentTarget.checked)}
/>
) : null}
{wagons.length ? (
<Checkbox
size="sm"
@@ -151,9 +182,24 @@ export default function AvailableWagonsPanel({
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>
<Group gap={6} wrap="nowrap">
<Text size="sm" fw={600} ff="monospace" truncate>
{wagon.wagonNumber}
</Text>
{wagon.exportTrainNumber ? (
<Badge
size="xs"
radius="sm"
variant="light"
color={
wagon.exportTrainNumber === exportTrainNumber ? "edr-green" : "gray"
}
>
{wagon.exportTrainNumber}
{wagon.importTrainNumber ? `-${wagon.importTrainNumber}` : ""}
</Badge>
) : null}
</Group>
<Text size="xs" c="dimmed" truncate>
{wagon.wagonType
? `${wagon.wagonType.name} · ${wagon.wagonType.capacityTons ?? "—"}T cap`
@@ -183,4 +229,8 @@ export interface AvailableWagonsPanelProps {
yardLabel?: string | null;
onAssign: (wagonIds: string[]) => void;
assigning: boolean;
/** This train's odd EXPORT run — drives the "only this run" filter. */
exportTrainNumber?: string | null;
/** This train's even IMPORT run — label only; the export run does the matching. */
importTrainNumber?: string | null;
}

View File

@@ -255,6 +255,8 @@ export default function TrainBuilderDetailPage() {
<AvailableWagonsPanel
yardId={yard?.id ?? ""}
yardLabel={yard?.label}
exportTrainNumber={composition.exportTrainNumber}
importTrainNumber={composition.importTrainNumber}
assigning={assignWagons.isPending}
onAssign={(wagonIds) =>
void withToast(

View File

@@ -28,6 +28,10 @@ export interface Wagon {
status: Freight.WagonStatus;
currentYardId: string | null;
currentYard?: { id: string; label?: string; code?: string } | null;
/** Odd EXPORT run (Ethiopia → Djibouti); null when the wagon is not on a run. */
exportTrainNumber?: string | null;
/** Even IMPORT run (Djibouti → Ethiopia); always the export run + 1. */
importTrainNumber?: string | null;
notes?: string;
}