mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 12:00:59 +00:00
211 lines
6.7 KiB
TypeScript
211 lines
6.7 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
import {
|
|
Alert,
|
|
Badge,
|
|
Button,
|
|
Group,
|
|
Paper,
|
|
Progress,
|
|
Select,
|
|
SimpleGrid,
|
|
Stack,
|
|
Text,
|
|
ThemeIcon,
|
|
} from "@mantine/core";
|
|
import { AlertTriangle, Link2, Wand2 } from "lucide-react";
|
|
|
|
import { Freight } from "@edr/types";
|
|
|
|
import type { PinWagonAssignment, TrainScheduleDetail } from "@/types/trainScheduling";
|
|
import type { Wagon } from "@/services/wagon.service";
|
|
import { wagonMatchesScheduleOrigin } from "@/utils/wagonAvailability";
|
|
|
|
import { autoFillWagonAssignments, countFilledSlots } from "./pinWagons.util";
|
|
|
|
export function PinWagonsForm({
|
|
schedule,
|
|
availableWagons,
|
|
isSubmitting,
|
|
onSubmit,
|
|
autoFillOnMount = true,
|
|
}: {
|
|
schedule: TrainScheduleDetail;
|
|
availableWagons: Wagon[];
|
|
isSubmitting?: boolean;
|
|
onSubmit: (assignments: PinWagonAssignment[]) => void;
|
|
autoFillOnMount?: boolean;
|
|
}) {
|
|
const originYardId = schedule.originStation?.id;
|
|
// Consist-only rows are the built train's coupled-but-empty wagons — display
|
|
// entries with no TrainSetWagon slot behind them, so nothing can be pinned.
|
|
const slots = (schedule.trainSet?.wagons ?? []).filter((w) => !w.consistOnly);
|
|
const [assignments, setAssignments] = useState<Record<string, string>>({});
|
|
|
|
const wagonOptionsByType = useMemo(() => {
|
|
const map = new Map<string, Array<{ value: string; label: string }>>();
|
|
for (const wagon of availableWagons) {
|
|
const isPinnedOnSlot = slots.some((s) => s.physicalWagonId === wagon.id);
|
|
if (
|
|
!wagonMatchesScheduleOrigin(wagon, originYardId, {
|
|
allowPinned: isPinnedOnSlot,
|
|
})
|
|
) {
|
|
continue;
|
|
}
|
|
if (wagon.status !== Freight.WagonStatus.Available && !isPinnedOnSlot) {
|
|
continue;
|
|
}
|
|
const typeId = wagon.wagonTypeId;
|
|
const list = map.get(typeId) ?? [];
|
|
list.push({ value: wagon.id, label: wagon.wagonNumber });
|
|
map.set(typeId, list);
|
|
}
|
|
return map;
|
|
}, [availableWagons, originYardId, slots]);
|
|
|
|
const runAutoFill = useCallback(
|
|
(preserveManual = false) => {
|
|
const existing = preserveManual ? assignments : {};
|
|
setAssignments(autoFillWagonAssignments(slots, wagonOptionsByType, existing));
|
|
},
|
|
[assignments, slots, wagonOptionsByType],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!autoFillOnMount || !slots.length) return;
|
|
setAssignments(autoFillWagonAssignments(slots, wagonOptionsByType));
|
|
}, [schedule.id, slots, wagonOptionsByType, autoFillOnMount]);
|
|
|
|
const fillStats = useMemo(
|
|
() => countFilledSlots(slots, assignments),
|
|
[slots, assignments],
|
|
);
|
|
|
|
const progress =
|
|
fillStats.total > 0 ? Math.round((fillStats.filled / fillStats.total) * 100) : 0;
|
|
|
|
const handleSubmit = () => {
|
|
const payload: PinWagonAssignment[] = Object.entries(assignments)
|
|
.filter(([, wagonId]) => Boolean(wagonId))
|
|
.map(([trainSetWagonId, physicalWagonId]) => ({ trainSetWagonId, physicalWagonId }));
|
|
onSubmit(payload);
|
|
};
|
|
|
|
if (!slots.length) {
|
|
return (
|
|
<Text size="sm" c="dimmed">
|
|
Assign bookings first to create wagon slots.
|
|
</Text>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Stack gap="md">
|
|
<Paper p="md" radius="xl" withBorder>
|
|
<Group justify="space-between" align="flex-start" wrap="wrap" gap="md">
|
|
<Stack gap={4}>
|
|
<Group gap="xs">
|
|
<Link2 size={18} />
|
|
<Text fw={600} size="sm">
|
|
Pin physical wagons
|
|
</Text>
|
|
</Group>
|
|
<Text size="xs" c="dimmed">
|
|
Match each train slot to a fleet wagon. Slots are auto-filled when possible.
|
|
</Text>
|
|
</Stack>
|
|
<Button
|
|
variant="light"
|
|
size="compact-sm"
|
|
leftSection={<Wand2 size={14} />}
|
|
onClick={() => runAutoFill(false)}
|
|
>
|
|
Auto-fill all slots
|
|
</Button>
|
|
</Group>
|
|
|
|
<Stack gap={6} mt="md">
|
|
<Group justify="space-between">
|
|
<Text size="xs" c="dimmed">
|
|
{fillStats.filled} of {fillStats.total} slots filled
|
|
</Text>
|
|
<Badge variant="light" color={progress === 100 ? "teal" : "yellow"}>
|
|
{progress}%
|
|
</Badge>
|
|
</Group>
|
|
<Progress value={progress} size="sm" radius="xl" color={progress === 100 ? "teal" : "yellow"} />
|
|
</Stack>
|
|
</Paper>
|
|
|
|
{fillStats.unfilledSlotNumbers.length > 0 ? (
|
|
<Alert
|
|
color="yellow"
|
|
variant="light"
|
|
radius="lg"
|
|
icon={<AlertTriangle size={16} />}
|
|
title="Some slots could not be auto-filled"
|
|
>
|
|
<Text size="sm">
|
|
No matching fleet wagon for slot
|
|
{fillStats.unfilledSlotNumbers.length === 1 ? "" : "s"} #
|
|
{fillStats.unfilledSlotNumbers.join(", #")}. Select manually or add wagons to the fleet.
|
|
</Text>
|
|
</Alert>
|
|
) : null}
|
|
|
|
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
|
|
{slots.map((slot) => {
|
|
const typeId = slot.wagonType?.id ?? "";
|
|
const options =
|
|
wagonOptionsByType.get(typeId) ??
|
|
availableWagons.map((w) => ({
|
|
value: w.id,
|
|
label: w.wagonNumber,
|
|
}));
|
|
|
|
return (
|
|
<Paper key={slot.id} p="md" radius="lg" withBorder>
|
|
<Group align="flex-end" wrap="nowrap" gap="md">
|
|
<Stack gap={2} style={{ minWidth: 90 }}>
|
|
<Group gap={6}>
|
|
<ThemeIcon size="sm" radius="md" variant="light" color="teal">
|
|
<Text size="xs" fw={700}>
|
|
{slot.sequenceNo}
|
|
</Text>
|
|
</ThemeIcon>
|
|
<Text size="sm" fw={600}>
|
|
Slot #{slot.sequenceNo}
|
|
</Text>
|
|
</Group>
|
|
<Text size="xs" c="dimmed">
|
|
{slot.wagonType?.code ?? "—"} · {slot.capacityTons}T
|
|
</Text>
|
|
</Stack>
|
|
<Select
|
|
style={{ flex: 1 }}
|
|
placeholder="Select physical wagon"
|
|
data={options}
|
|
value={assignments[slot.id] ?? null}
|
|
onChange={(value) =>
|
|
setAssignments((current) => ({
|
|
...current,
|
|
[slot.id]: value ?? "",
|
|
}))
|
|
}
|
|
searchable
|
|
/>
|
|
</Group>
|
|
</Paper>
|
|
);
|
|
})}
|
|
</SimpleGrid>
|
|
|
|
<Group justify="flex-end">
|
|
<Button color="teal" loading={isSubmitting} onClick={handleSubmit}>
|
|
Pin wagons
|
|
</Button>
|
|
</Group>
|
|
</Stack>
|
|
);
|
|
}
|