booking operations and trains scheduling also allocations

This commit is contained in:
marshal
2026-06-10 00:48:32 +03:00
parent 675975bc08
commit 5774d7db9d
180 changed files with 13423 additions and 3877 deletions

View File

@@ -0,0 +1,202 @@
import { useMemo } from "react";
import {
Badge,
Button,
Card,
Group,
Paper,
Progress,
Select,
SimpleGrid,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { CheckCircle2, Container } from "lucide-react";
import type { ContainerPlacement, ContainerUnitRow } from "@/types/trainScheduling";
import { autoFillPlacements, unitKey, validateLocalPlacements } from "./containerPlacement.util";
export function ContainerPlacementGrid({
units,
containerSlots,
placements,
onChange,
}: {
units: ContainerUnitRow[];
containerSlots: number[];
placements: ContainerPlacement[];
onChange: (placements: ContainerPlacement[]) => void;
}) {
const placementMap = useMemo(() => {
const map = new Map<string, ContainerPlacement>();
for (const placement of placements) {
map.set(unitKey(placement.bookingContainerId, placement.unitIndex), placement);
}
return map;
}, [placements]);
const issues = useMemo(() => validateLocalPlacements(units, placements), [units, placements]);
const completedCount = useMemo(
() =>
units.filter((unit) => {
const placement = placementMap.get(unitKey(unit.bookingContainerId, unit.unitIndex));
return placement?.sequenceNo && placement.containerNumber?.trim();
}).length,
[units, placementMap],
);
const slotOptions = containerSlots.map((seq) => ({
value: String(seq),
label: `Wagon #${seq}`,
}));
const updatePlacement = (unit: ContainerUnitRow, patch: Partial<ContainerPlacement>) => {
const key = unitKey(unit.bookingContainerId, unit.unitIndex);
const existing = placementMap.get(key);
const next: ContainerPlacement = {
bookingContainerId: unit.bookingContainerId,
unitIndex: unit.unitIndex,
sequenceNo: existing?.sequenceNo ?? containerSlots[0] ?? 1,
containerNumber: existing?.containerNumber,
sealNumber: existing?.sealNumber,
...patch,
};
onChange([
...placements.filter(
(p) => !(p.bookingContainerId === unit.bookingContainerId && p.unitIndex === unit.unitIndex),
),
next,
]);
};
if (!units.length) {
return (
<Text size="sm" c="dimmed">
No container units in this selection.
</Text>
);
}
const progress = units.length ? Math.round((completedCount / units.length) * 100) : 0;
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">
<Container size={18} />
<Text fw={600} size="sm">
Container assignment
</Text>
</Group>
<Text size="xs" c="dimmed">
Map each booking unit to a wagon slot and enter the container number. One wagon fits
either 1×40ft or 2×20ft containers.
</Text>
</Stack>
<Button
variant="light"
size="compact-sm"
onClick={() => onChange(autoFillPlacements(units, containerSlots))}
>
Auto-fill slots
</Button>
</Group>
<Stack gap={6} mt="md">
<Group justify="space-between">
<Text size="xs" c="dimmed">
{completedCount} of {units.length} units complete
</Text>
<Text size="xs" fw={500}>
{progress}%
</Text>
</Group>
<Progress
value={progress}
size="sm"
radius="xl"
color={issues.length ? "yellow" : "teal"}
/>
</Stack>
</Paper>
{issues.length ? (
<Stack gap={6}>
{issues.map((issue) => (
<Badge key={issue} color="red" variant="light" size="sm" w="fit-content">
{issue}
</Badge>
))}
</Stack>
) : (
<Badge
color="teal"
variant="light"
size="sm"
w="fit-content"
leftSection={<CheckCircle2 size={12} />}
>
All units mapped
</Badge>
)}
<SimpleGrid cols={{ base: 1, lg: 2 }} spacing="md">
{units.map((unit) => {
const key = unitKey(unit.bookingContainerId, unit.unitIndex);
const placement = placementMap.get(key);
const isComplete = placement?.sequenceNo && placement.containerNumber?.trim();
return (
<Card key={key} radius="xl" padding="md" withBorder>
<Stack gap="md">
<Group justify="space-between" align="flex-start">
<Stack gap={2}>
<Text size="sm" fw={600}>
{unit.bookingReference}
</Text>
<Text size="xs" c="dimmed">
{unit.label} · {unit.containerTypeCode} · {unit.grossWeightTons}T
</Text>
</Stack>
<Badge size="sm" variant="light" color={isComplete ? "teal" : "gray"}>
{isComplete ? "Ready" : "Pending"}
</Badge>
</Group>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="sm">
<Select
label="Wagon slot"
size="sm"
data={slotOptions}
value={placement?.sequenceNo ? String(placement.sequenceNo) : null}
onChange={(value) =>
updatePlacement(unit, { sequenceNo: Number(value ?? containerSlots[0]) })
}
placeholder="Select wagon"
searchable
/>
<TextInput
label="Container number"
size="sm"
placeholder="e.g. MSCU1234567"
value={placement?.containerNumber ?? ""}
onChange={(e) =>
updatePlacement(unit, {
containerNumber: e.currentTarget.value,
})
}
/>
</SimpleGrid>
</Stack>
</Card>
);
})}
</SimpleGrid>
</Stack>
);
}