mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
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.
This commit is contained in:
@@ -40,17 +40,21 @@ export function PreviewSummary({
|
||||
summary?: {
|
||||
totalBookings: number;
|
||||
totalWeightTons: number;
|
||||
grossWeightTons?: number;
|
||||
totalTareTons?: number;
|
||||
wagonType: string;
|
||||
wagonsNeeded: number;
|
||||
totalLengthMeters: number;
|
||||
};
|
||||
}) {
|
||||
if (!summary) return null;
|
||||
// GROSS — the axis every train limit is spent against.
|
||||
const gross = summary.grossWeightTons ?? summary.totalWeightTons;
|
||||
const stats = [
|
||||
{ label: "Bookings", value: String(summary.totalBookings) },
|
||||
{ label: "Wagons", value: String(summary.wagonsNeeded) },
|
||||
{ label: "Wagon type", value: summary.wagonType },
|
||||
{ label: "Total weight", value: `${summary.totalWeightTons}T` },
|
||||
{ label: "Gross weight", value: `${gross}T` },
|
||||
{ label: "Train length", value: `${summary.totalLengthMeters}m` },
|
||||
];
|
||||
return (
|
||||
|
||||
@@ -99,9 +99,8 @@ function usedWeight(schedule: TrainScheduleDetail): number {
|
||||
/**
|
||||
* Pull capacity of the set = the WEAKEST locomotive's max pull weight (0 when
|
||||
* unknown). The API caps at the weakest loco, not the sum of all locos — a
|
||||
* consist can only pull as hard as its weakest engine. Note: the API also adds
|
||||
* the consist tare to the used weight when it checks this cap; tare isn't
|
||||
* available client-side, so this meter compares cargo-only load against pull.
|
||||
* consist can only pull as hard as its weakest engine. Both sides of this meter
|
||||
* are gross: `usedWeight` sums per-booking gross (cargo + wagon tare).
|
||||
*/
|
||||
function pullCapacity(schedule: TrainScheduleDetail): number {
|
||||
const set = schedule.trainSet;
|
||||
|
||||
@@ -46,6 +46,8 @@ type NormalizedWagon = {
|
||||
|
||||
const CAR_WIDTH = 150; // car body + coupler footprint
|
||||
|
||||
const round1 = (n: number) => Math.round(n * 10) / 10;
|
||||
|
||||
function normalizeWagon(w: DiagramWagonInput, freightType?: string | null): NormalizedWagon {
|
||||
const allocations = w.allocations ?? [];
|
||||
const firstLoad = (
|
||||
@@ -268,10 +270,11 @@ const CONTAINER_BORDERS = [
|
||||
];
|
||||
|
||||
function WagonCar({ wagon }: { wagon: NormalizedWagon }) {
|
||||
// GROSS on both sides: cargo + tare vs rated payload + tare.
|
||||
const grossTons = round1(wagon.assignedWeightTons + wagon.tareWeightTons);
|
||||
const maxGrossTons = round1(wagon.capacityTons + wagon.tareWeightTons);
|
||||
const utilization =
|
||||
wagon.capacityTons > 0
|
||||
? Math.min(100, Math.round((wagon.assignedWeightTons / wagon.capacityTons) * 100))
|
||||
: 0;
|
||||
maxGrossTons > 0 ? Math.min(100, Math.round((grossTons / maxGrossTons) * 100)) : 0;
|
||||
const accent = wagon.isEmpty ? "gray" : wagon.isBulk ? "orange" : "cyan";
|
||||
const accentVar = `var(--mantine-color-${accent}-6)`;
|
||||
|
||||
@@ -281,8 +284,8 @@ function WagonCar({ wagon }: { wagon: NormalizedWagon }) {
|
||||
wagon.bookingRefs.length ? wagon.bookingRefs.join(", ") : ""
|
||||
}${
|
||||
wagon.containerNumbers.length ? `\nContainers: ${wagon.containerNumbers.join(", ")}` : ""
|
||||
}${wagon.cargoDescription ? `\n${wagon.cargoDescription}` : ""}\nLoad: ${wagon.assignedWeightTons}/${wagon.capacityTons}T (${utilization}%)${
|
||||
wagon.tareWeightTons ? `\nTare: ${wagon.tareWeightTons}T` : ""
|
||||
}${wagon.cargoDescription ? `\n${wagon.cargoDescription}` : ""}\nGross: ${grossTons}/${maxGrossTons}T (${utilization}%)\nCargo: ${wagon.assignedWeightTons}T${
|
||||
wagon.tareWeightTons ? ` · Tare: ${wagon.tareWeightTons}T` : ""
|
||||
}`;
|
||||
|
||||
// container blocks: one per container number (cap visual at 2 = TEU per wagon)
|
||||
@@ -369,7 +372,7 @@ function WagonCar({ wagon }: { wagon: NormalizedWagon }) {
|
||||
/>
|
||||
</Box>
|
||||
<Text size="9px" c="dimmed" ta="center" fw={600}>
|
||||
{wagon.assignedWeightTons}/{wagon.capacityTons}T
|
||||
{grossTons}/{maxGrossTons}T
|
||||
</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
@@ -442,7 +445,7 @@ function WagonCar({ wagon }: { wagon: NormalizedWagon }) {
|
||||
</Text>
|
||||
{!wagon.isEmpty ? (
|
||||
<Text size="8px" c="gray.6" fw={700} style={{ whiteSpace: "nowrap" }}>
|
||||
{wagon.assignedWeightTons}T
|
||||
{grossTons}T
|
||||
</Text>
|
||||
) : null}
|
||||
</Group>
|
||||
|
||||
@@ -6,6 +6,7 @@ type WagonSlot = (WagonPlanRow & { physicalWagonNumber?: string | null }) | {
|
||||
sequenceNo: number;
|
||||
capacityTons: number;
|
||||
assignedWeightTons: number;
|
||||
tareWeightTons?: number | null;
|
||||
slotLoadType?: string;
|
||||
wagonType?: { code: string } | null;
|
||||
wagonTypeCode?: string;
|
||||
@@ -21,6 +22,8 @@ type WagonSlot = (WagonPlanRow & { physicalWagonNumber?: string | 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";
|
||||
@@ -68,8 +71,16 @@ export function WagonPlanGrid({
|
||||
);
|
||||
}
|
||||
|
||||
const totalCapacity = wagonPlan.reduce((sum, w) => sum + w.capacityTons, 0);
|
||||
const totalAssigned = wagonPlan.reduce((sum, w) => sum + w.assignedWeightTons, 0);
|
||||
// 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"));
|
||||
@@ -82,7 +93,7 @@ export function WagonPlanGrid({
|
||||
</Text>
|
||||
{isBulk ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
Load: <strong>{totalAssigned}</strong> / {totalCapacity}T
|
||||
Gross: <strong>{totalAssigned}</strong> / {totalCapacity}T
|
||||
</Text>
|
||||
) : null}
|
||||
</Group>
|
||||
@@ -90,8 +101,9 @@ export function WagonPlanGrid({
|
||||
<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 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);
|
||||
@@ -149,7 +161,7 @@ export function WagonPlanGrid({
|
||||
</Text>
|
||||
{label === "BULK" ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
{alloc.allocatedWeightTons}T
|
||||
{alloc.allocatedWeightTons}T cargo
|
||||
</Text>
|
||||
) : null}
|
||||
</Group>
|
||||
|
||||
@@ -132,7 +132,7 @@ export const BookingDetailModal = ({
|
||||
/>
|
||||
<InfoRow
|
||||
icon={<Weight size={15} />}
|
||||
label="Weight"
|
||||
label="Gross weight"
|
||||
value={
|
||||
<Text size="sm" fw={700}>
|
||||
{booking.weightTons != null ? `${booking.weightTons.toFixed(1)} T` : "—"}
|
||||
|
||||
@@ -161,8 +161,11 @@ function WagonCar({
|
||||
const allocation = wagon.allocations?.[0];
|
||||
const isEmpty = !allocation;
|
||||
const isBulk = (allocation?.loadType ?? "").toUpperCase().includes("BULK");
|
||||
const assigned = allocation?.allocatedWeightTons ?? wagon.assignedWeightTons ?? 0;
|
||||
const capacity = wagon.capacityTons ?? 0;
|
||||
// GROSS on both sides: cargo + tare vs rated payload + tare.
|
||||
const tare = wagon.tareWeightTons ?? 0;
|
||||
const assigned =
|
||||
(allocation?.allocatedWeightTons ?? wagon.assignedWeightTons ?? 0) + tare;
|
||||
const capacity = (wagon.capacityTons ?? 0) + tare;
|
||||
const utilization = capacity > 0 ? Math.min(100, Math.round((assigned / capacity) * 100)) : 0;
|
||||
const accent = isEmpty ? "gray" : isBulk ? "orange" : "cyan";
|
||||
const accentVar = `var(--mantine-color-${accent}-6)`;
|
||||
|
||||
@@ -21,6 +21,8 @@ export const RemoveBookingModal = ({
|
||||
if (!wagon || !wagon.allocations?.[0]) return null;
|
||||
|
||||
const allocation = wagon.allocations[0];
|
||||
// GROSS: allocated cargo + the tare of the wagon it sits on.
|
||||
const grossTons = (allocation.allocatedWeightTons ?? 0) + (wagon.tareWeightTons ?? 0);
|
||||
|
||||
return (
|
||||
<Modal opened={opened} onClose={onClose} title="Confirm Booking Removal" centered>
|
||||
@@ -40,7 +42,7 @@ export const RemoveBookingModal = ({
|
||||
</Badge>
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
<strong>Weight:</strong> {allocation.allocatedWeightTons?.toFixed(2) || 0} T
|
||||
<strong>Gross weight:</strong> {grossTons.toFixed(2)} T
|
||||
</Text>
|
||||
<Text size="sm">
|
||||
<strong>Wagon Slot:</strong> #{wagon.sequenceNo}
|
||||
|
||||
@@ -93,10 +93,14 @@ export const TrainConsistView = ({
|
||||
}
|
||||
};
|
||||
|
||||
const weightUsed = wagons.reduce(
|
||||
(sum, w) => sum + (w.allocations?.[0]?.allocatedWeightTons ?? 0),
|
||||
// GROSS: cargo on every allocation + the tare of every wagon in the consist.
|
||||
// maxPullWeightTons is a gross limit, so the numerator must be gross too.
|
||||
const cargoUsed = wagons.reduce(
|
||||
(sum, w) => sum + (w.allocations ?? []).reduce((a, al) => a + (al.allocatedWeightTons ?? 0), 0),
|
||||
0,
|
||||
);
|
||||
const tareUsed = wagons.reduce((sum, w) => sum + (w.tareWeightTons ?? 0), 0);
|
||||
const weightUsed = cargoUsed + tareUsed;
|
||||
const lengthUsed = wagons.reduce((sum, w) => sum + (w.lengthMeters ?? 0), 0);
|
||||
|
||||
return (
|
||||
|
||||
@@ -98,7 +98,7 @@ export const TrainStatsBar = ({
|
||||
<SimpleGrid cols={{ base: 1, xs: 3 }} spacing="lg">
|
||||
<StatTile
|
||||
icon={<Weight size={15} />}
|
||||
label="Weight"
|
||||
label="Gross weight"
|
||||
pct={weightPct}
|
||||
current={weightUsed.toFixed(1)}
|
||||
max={weightMax?.toFixed(1) ?? "∞"}
|
||||
|
||||
@@ -130,7 +130,9 @@ export const UnassignedBookingsPanel = ({
|
||||
|
||||
{bookings.map((booking) => {
|
||||
const isActive = selectedBookingId === booking.id;
|
||||
const weight = Number(booking.cargoTotalWeightVgm ?? 0);
|
||||
// GROSS (cargo + wagon tare) so this badge shares the axis every other
|
||||
// weight on the page uses — cargo-only here read ~25% light.
|
||||
const weight = Number(booking.grossWeightTons ?? booking.cargoTotalWeightVgm ?? 0);
|
||||
const fits = booking.canAssign;
|
||||
const blockReason = booking.blockReason;
|
||||
|
||||
|
||||
@@ -36,8 +36,12 @@ export const WagonCard = ({
|
||||
const hasAllocations = Boolean(allocation);
|
||||
const isBulk = (allocation?.loadType ?? "").toUpperCase().includes("BULK");
|
||||
|
||||
const weightUsed = allocation?.allocatedWeightTons ?? 0;
|
||||
const weightMax = wagon.capacityTons ?? 0;
|
||||
// GROSS on both sides: loaded cargo + wagon tare, against the wagon's max
|
||||
// gross (rated payload + tare). Keeps the wagon axis identical to the train
|
||||
// axis in TrainStatsBar.
|
||||
const tare = wagon.tareWeightTons ?? 0;
|
||||
const weightUsed = (allocation?.allocatedWeightTons ?? 0) + tare;
|
||||
const weightMax = (wagon.capacityTons ?? 0) + tare;
|
||||
const weightPercent = weightMax ? (weightUsed / weightMax) * 100 : 0;
|
||||
|
||||
const wagonType = wagon.wagonType?.code || "UNKNOWN";
|
||||
|
||||
Reference in New Issue
Block a user