added ewti landing pae

This commit is contained in:
Marshal
2026-07-31 11:23:00 +00:00
parent a64af98205
commit 091d655baa

View File

@@ -34,6 +34,10 @@ interface LegBookingUsage {
reference: string;
wagons: number;
grossTons: number;
/** Linked to the schedule but has NO wagon allocation — its weight is on no consist slot. */
unallocated?: boolean;
/** The booking's own origin → destination, so a sub-leg booking reads as such. */
route?: string | null;
}
interface EdgeUsage {
@@ -45,6 +49,8 @@ interface EdgeUsage {
lengthMeters: number;
bookingRefs: string[];
bookings: LegBookingUsage[];
/** Gross tons of linked-but-unallocated bookings riding this leg — not yet on any slot. */
pendingTons: number;
}
const round1 = (n: number) => Math.round(n * 10) / 10;
@@ -106,9 +112,33 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
if (stops.length < 2) return [];
const indexOf = new Map(stops.map((s, i) => [s.yardId, i]));
const lastIdx = stops.length - 1;
const spans = wagons.map((w) =>
spanOf(w.boardYardId, w.alightYardId, indexOf, lastIdx),
);
// Booking legs, for slot-span fallback and for labelling dropdown rows.
const bookingById = new Map((schedule.bookings ?? []).map((b) => [b.id, b]));
const bookingSpan = (bookingId: string) => {
const b = bookingById.get(bookingId);
if (!b) return null;
const from = (b.originYardId ? indexOf.get(b.originYardId) : undefined) ?? 0;
const toRaw = b.destinationYardId ? indexOf.get(b.destinationYardId) : lastIdx;
const to = toRaw != null && toRaw > from ? toRaw : lastIdx;
return { from, to };
};
// A slot rides its stamped board→alight span. Slots without a stamp (an
// API build predating the fields, or plans written before spans existed)
// fall back to the union of their OWN bookings' legs — an a→b-only wagon
// must not count on the b→c leg. Empty stamped-less wagons ride everything.
const spans = wagons.map((w) => {
if (w.boardYardId || w.alightYardId) {
return spanOf(w.boardYardId, w.alightYardId, indexOf, lastIdx);
}
const legs = (w.allocations ?? [])
.map((a) => bookingSpan(a.bookingId))
.filter((s): s is { from: number; to: number } => s != null);
if (!legs.length) return { from: 0, to: lastIdx };
return {
from: Math.min(...legs.map((s) => s.from)),
to: Math.max(...legs.map((s) => s.to)),
};
});
return stops.slice(0, -1).map((from, edge) => {
const active = wagons.filter(
(_, i) => spans[i].from <= edge && edge < spans[i].to,
@@ -128,11 +158,16 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
for (const a of w.allocations ?? []) {
if (!a.bookingReference) continue;
refs.add(a.bookingReference);
const linked = bookingById.get(a.bookingId);
const row = byBooking.get(a.bookingId) ?? {
bookingId: a.bookingId,
reference: a.bookingReference,
wagons: 0,
grossTons: 0,
route:
linked?.origin && linked?.destination
? `${linked.origin}${linked.destination}`
: null,
};
row.grossTons += Number(a.allocatedWeightTons) || 0;
byBooking.set(a.bookingId, row);
@@ -143,9 +178,35 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
if (row) row.wagons += 1;
}
}
// Linked bookings with NO wagon allocation ride their leg too — without
// this they vanish from the tab entirely (two Dire→DCT bookings hidden
// while a through booking showed alone). Flagged so staff see the gap;
// their tonnage is deliberately NOT in the leg totals, which reflect
// what is physically on consist slots.
for (const b of schedule.bookings ?? []) {
if (byBooking.has(b.id)) continue;
const bFrom = (b.originYardId ? indexOf.get(b.originYardId) : undefined) ?? 0;
const bToRaw = b.destinationYardId ? indexOf.get(b.destinationYardId) : lastIdx;
const bTo = bToRaw != null && bToRaw > bFrom ? bToRaw : lastIdx;
if (!(bFrom <= edge && edge < bTo)) continue;
const reference = b.reference ?? b.id;
refs.add(reference);
byBooking.set(b.id, {
bookingId: b.id,
reference,
wagons: Number(b.wagonsRequired) || 0,
grossTons: Number(b.weightTons) || 0,
unallocated: true,
route:
b.origin && b.destination ? `${b.origin}${b.destination}` : null,
});
}
const bookings = [...byBooking.values()]
.map((b) => ({ ...b, grossTons: round1(b.grossTons) }))
.sort((a, b) => b.grossTons - a.grossTons);
const pendingTons = round1(
bookings.filter((b) => b.unallocated).reduce((sum, b) => sum + b.grossTons, 0),
);
return {
edge,
from,
@@ -155,9 +216,21 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
lengthMeters: round1(lengthMeters),
bookingRefs: [...refs],
bookings,
pendingTons,
};
});
}, [stops, wagons]);
}, [stops, wagons, schedule.bookings]);
const unallocatedRefs = useMemo(
() => [
...new Set(
edges.flatMap((e) =>
e.bookings.filter((b) => b.unallocated).map((b) => b.reference),
),
),
],
[edges],
);
if (stops.length < 2) {
return (
@@ -210,6 +283,14 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
return (
<Stack gap="lg" mt="lg">
{unallocatedRefs.length ? (
<Alert radius="lg" color="orange" icon={<Info size={16} />}>
{unallocatedRefs.join(", ")}{" "}
{unallocatedRefs.length === 1 ? "is" : "are"} linked to this train but
have no wagons allocated their weight is not on any leg yet. Re-run
allocation (or add them from the workspace) to place them.
</Alert>
) : null}
<Paper radius="xl" p="lg" withBorder>
<Stack gap="md">
<Stack gap={2}>
@@ -267,6 +348,11 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
</Table.Td>
<Table.Td>
<UsageCell used={e.grossTons} cap={weightCap} unit="T" />
{e.pendingTons > 0 ? (
<Text size="xs" c="orange.8" fw={600}>
+{e.pendingTons}T unallocated
</Text>
) : null}
</Table.Td>
<Table.Td>
<UsageCell used={e.lengthMeters} cap={lengthCap} unit="m" />
@@ -303,21 +389,35 @@ export function LegCapacityPanel({ schedule }: { schedule: TrainScheduleDetail }
<Table.Tbody>
{e.bookings.map((b) => (
<Table.Tr key={b.bookingId}>
<Table.Td w="40%">
<Table.Td w="34%">
<Group gap={6}>
<Text size="sm" fw={500}>
{b.reference}
</Text>
{b.unallocated ? (
<Badge size="xs" color="orange" variant="filled">
no wagons
</Badge>
) : null}
</Group>
</Table.Td>
<Table.Td w="30%">
<Group gap={4}>
<Table.Td w="26%">
<Text size="xs" c="dimmed" style={{ whiteSpace: "nowrap" }}>
{b.route ?? "—"}
</Text>
</Table.Td>
<Table.Td w="20%">
<Group gap={4} wrap="nowrap">
<Train size={12} />
<Text size="xs" c="dimmed">
{b.wagons} wagon{b.wagons === 1 ? "" : "s"}
{b.wagons > 0 ? b.wagons : ""} wagon
{b.wagons === 1 ? "" : "s"}
{b.unallocated && b.wagons > 0 ? " needed" : ""}
</Text>
</Group>
</Table.Td>
<Table.Td w="30%">
<Group gap={4}>
<Table.Td w="20%">
<Group gap={4} wrap="nowrap">
<Weight size={12} />
<Text size="xs" c="dimmed">
{b.grossTons}T