Files
edr-platform/apps/edr-freight-web/backoffice/src/components/trainScheduling/WagonPlanGrid.tsx
Marshal 0dead281ce add per-container handling options for hazardous, reefer, and return services
- 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.
2026-07-18 19:20:45 +00:00

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>
);
}