mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 02:58:11 +00:00
booking operations and trains scheduling also allocations
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
import { Badge, Card, Group, Progress, SimpleGrid, Stack, Text, ThemeIcon } from "@mantine/core";
|
||||
import { Box, Package } from "lucide-react";
|
||||
import type { TrainScheduleWagonAllocation, WagonPlanRow } from "@/types/trainScheduling";
|
||||
|
||||
type WagonSlot = WagonPlanRow | {
|
||||
sequenceNo: number;
|
||||
capacityTons: number;
|
||||
assignedWeightTons: number;
|
||||
slotLoadType?: string;
|
||||
wagonType?: { code: string } | null;
|
||||
wagonTypeCode?: string;
|
||||
physicalWagonNumber?: string | null;
|
||||
allocations?: TrainScheduleWagonAllocation[] | Array<{
|
||||
id?: string;
|
||||
bookingId: string;
|
||||
bookingReference?: string | null;
|
||||
allocatedWeightTons: number;
|
||||
loadType?: string | null;
|
||||
containerItems?: Array<{ containerNumber: string | null; grossWeightTons?: number | null }>;
|
||||
bulkLoad?: { weightTons: number; cargoDescription: string | null } | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
function loadTypeColor(loadType: string | undefined, freightType?: string | null) {
|
||||
const normalized = loadType?.toUpperCase() ?? "";
|
||||
if (normalized.includes("BULK")) return "orange";
|
||||
if (normalized.includes("CONTAINER")) return "cyan";
|
||||
return freightType === "BULK" ? "orange" : "cyan";
|
||||
}
|
||||
|
||||
function slotLabel(slot: WagonSlot, freightType?: string | null) {
|
||||
if ("slotLoadType" in slot && slot.slotLoadType) return slot.slotLoadType;
|
||||
const fromAlloc = slot.allocations?.[0]?.loadType?.toString().toUpperCase();
|
||||
if (fromAlloc) return fromAlloc;
|
||||
if (freightType === "MIXED") return "MIXED";
|
||||
return freightType ?? "SLOT";
|
||||
}
|
||||
|
||||
function wagonTypeLabel(slot: WagonSlot) {
|
||||
if ("wagonType" in slot && slot.wagonType?.code) return slot.wagonType.code;
|
||||
if ("wagonTypeCode" in slot && slot.wagonTypeCode) return slot.wagonTypeCode;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function WagonPlanGrid({
|
||||
wagonPlan,
|
||||
freightType,
|
||||
}: {
|
||||
wagonPlan: WagonSlot[];
|
||||
freightType?: string | null;
|
||||
}) {
|
||||
if (!wagonPlan?.length) {
|
||||
return (
|
||||
<Card radius="lg" padding="xl" withBorder bg="gray.0">
|
||||
<Stack align="center" gap="sm">
|
||||
<ThemeIcon size="lg" radius="xl" variant="light" color="gray">
|
||||
<Package size={20} />
|
||||
</ThemeIcon>
|
||||
<Text size="sm" fw={500}>
|
||||
No wagon plan yet
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" ta="center" maw={360}>
|
||||
Select bookings and run <strong>Preview plan</strong> to generate wagon slots and
|
||||
allocations.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const totalCapacity = wagonPlan.reduce((sum, w) => sum + w.capacityTons, 0);
|
||||
const totalAssigned = wagonPlan.reduce((sum, w) => sum + w.assignedWeightTons, 0);
|
||||
const usedSlots = wagonPlan.filter((w) => (w.allocations?.length ?? 0) > 0).length;
|
||||
|
||||
const isBulk = freightType === "BULK" || wagonPlan.every((w) => w.slotLoadType === "BULK" || (!w.slotLoadType && w.allocations?.[0]?.loadType === "Bulk"));
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
<Group gap="lg">
|
||||
<Text size="sm" c="dimmed">
|
||||
<strong>{wagonPlan.length}</strong> wagons · <strong>{usedSlots}</strong> in use
|
||||
</Text>
|
||||
{isBulk ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
Load: <strong>{totalAssigned}</strong> / {totalCapacity}T
|
||||
</Text>
|
||||
) : null}
|
||||
</Group>
|
||||
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, xl: 3 }} spacing="md">
|
||||
{wagonPlan.map((wagon) => {
|
||||
const seq = wagon.sequenceNo;
|
||||
const capacity = wagon.capacityTons;
|
||||
const assigned = wagon.assignedWeightTons;
|
||||
const allocations = wagon.allocations ?? [];
|
||||
const utilization = capacity > 0 ? Math.min(100, Math.round((assigned / capacity) * 100)) : 0;
|
||||
const label = slotLabel(wagon, freightType);
|
||||
const typeCode = wagonTypeLabel(wagon);
|
||||
|
||||
return (
|
||||
<Card key={seq} radius="lg" padding="md" withBorder>
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="flex-start" wrap="nowrap">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<ThemeIcon size="md" radius="md" variant="light" color={loadTypeColor(label, freightType)}>
|
||||
<Box size={16} />
|
||||
</ThemeIcon>
|
||||
<Stack gap={0}>
|
||||
<Text fw={600} size="sm">
|
||||
Wagon #{seq}
|
||||
</Text>
|
||||
{typeCode ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
{typeCode}
|
||||
{wagon.physicalWagonNumber ? ` · ${wagon.physicalWagonNumber}` : ""}
|
||||
</Text>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Group>
|
||||
<Badge variant="light" size="sm" color={loadTypeColor(label, freightType)}>
|
||||
{label}
|
||||
</Badge>
|
||||
</Group>
|
||||
|
||||
{label === "BULK" ? (
|
||||
<Stack gap={4}>
|
||||
<Group justify="space-between">
|
||||
<Text size="xs" c="dimmed">
|
||||
Capacity
|
||||
</Text>
|
||||
<Text size="xs" fw={500}>
|
||||
{assigned} / {capacity}T
|
||||
</Text>
|
||||
</Group>
|
||||
<Progress
|
||||
value={utilization}
|
||||
size="sm"
|
||||
radius="xl"
|
||||
color={utilization > 95 ? "red" : utilization > 80 ? "yellow" : "green"}
|
||||
/>
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
<Stack gap={6}>
|
||||
{allocations.length ? (
|
||||
allocations.map((alloc, index) => (
|
||||
<Card key={`${alloc.bookingId}-${index}`} padding="xs" radius="md" bg="gray.0">
|
||||
<Stack gap={2}>
|
||||
<Group justify="space-between" gap="xs">
|
||||
<Text size="xs" fw={500} lineClamp={1}>
|
||||
{alloc.bookingReference ?? alloc.bookingId}
|
||||
</Text>
|
||||
{label === "BULK" ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
{alloc.allocatedWeightTons}T
|
||||
</Text>
|
||||
) : null}
|
||||
</Group>
|
||||
{"containerItems" in alloc && alloc.containerItems?.length ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
{alloc.containerItems.length} container{alloc.containerItems.length > 1 ? "s" : ""}
|
||||
</Text>
|
||||
) : null}
|
||||
{"bulkLoad" in alloc && alloc.bulkLoad ? (
|
||||
<Text size="xs" c="dimmed" lineClamp={2}>
|
||||
Bulk · {alloc.bulkLoad.weightTons}T
|
||||
{alloc.bulkLoad.cargoDescription
|
||||
? ` — ${alloc.bulkLoad.cargoDescription}`
|
||||
: ""}
|
||||
</Text>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Text size="xs" c="dimmed" fs="italic">
|
||||
Empty slot
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user