mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 14:20:58 +00:00
- Introduced new boolean fields (isHazardous, isReefer, isReturn) in UnitDraft and related interfaces to allow individual container handling options. - Updated emptyUnit function to initialize these new fields. - Modified GlCreateBookingForm to handle and display these options for each container. - Adjusted calculations for hazardous, reefer, and return quantities based on the new handling options. - Updated the schema for container units and booking container lines to include handling options. - Added migration to support the new return flag in the database. - Enhanced various components to reflect gross weight calculations, ensuring consistency across the application.
198 lines
7.7 KiB
TypeScript
198 lines
7.7 KiB
TypeScript
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 & { physicalWagonNumber?: string | null }) | {
|
|
sequenceNo: number;
|
|
capacityTons: number;
|
|
assignedWeightTons: number;
|
|
tareWeightTons?: number | null;
|
|
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;
|
|
}>;
|
|
};
|
|
|
|
const round1 = (n: number) => Math.round(n * 10) / 10;
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
// GROSS on both sides: cargo + tare vs rated payload + tare.
|
|
const totalTare = round1(
|
|
wagonPlan.reduce((sum, w) => sum + (Number(w.tareWeightTons) || 0), 0),
|
|
);
|
|
const totalCapacity = round1(
|
|
wagonPlan.reduce((sum, w) => sum + w.capacityTons, 0) + totalTare,
|
|
);
|
|
const totalAssigned = round1(
|
|
wagonPlan.reduce((sum, w) => sum + w.assignedWeightTons, 0) + totalTare,
|
|
);
|
|
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">
|
|
Gross: <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 tare = Number(wagon.tareWeightTons) || 0;
|
|
const capacity = round1(wagon.capacityTons + tare);
|
|
const assigned = round1(wagon.assignedWeightTons + tare);
|
|
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.physicalWagonNumber ?? `Wagon #${seq}`}
|
|
</Text>
|
|
<Text size="xs" c="dimmed">
|
|
{[typeCode, `Pos ${seq}`].filter(Boolean).join(" · ")}
|
|
</Text>
|
|
</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" : "edr-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 cargo
|
|
</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>
|
|
);
|
|
}
|