mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
booking operations and trains scheduling also allocations
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
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 { wagonMatchesScheduleDirection } 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 slots = schedule.trainSet?.wagons ?? [];
|
||||
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 (
|
||||
!wagonMatchesScheduleDirection(wagon, schedule.direction, {
|
||||
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, schedule.direction, 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user