mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
Merge pull request #1387 from Tria-plc/freight_feature/usermanagement
fix issue
This commit is contained in:
@@ -22,6 +22,15 @@ type Slot = NonNullable<TrainScheduleDetail["trainSet"]>["wagons"][number];
|
||||
type Stop = { yardId: string; label: string };
|
||||
type Span = [number, number];
|
||||
|
||||
/** The allocations of one slot that ride the same corridor — one drawn bar. */
|
||||
type SlotPart = {
|
||||
slot: Slot;
|
||||
span: Span;
|
||||
loaded: boolean;
|
||||
/** Allocations riding THIS span (all of the slot's when it is not split). */
|
||||
allocations: NonNullable<Slot["allocations"]>;
|
||||
};
|
||||
|
||||
/** One physical wagon of the consist with every slot (leg load) pinned to it. */
|
||||
interface WagonRow {
|
||||
key: string;
|
||||
@@ -30,12 +39,57 @@ interface WagonRow {
|
||||
position: number;
|
||||
typeCode: string | null;
|
||||
capacityTons: number;
|
||||
slots: Array<{ slot: Slot; span: Span; loaded: boolean }>;
|
||||
slots: SlotPart[];
|
||||
}
|
||||
|
||||
const round1 = (n: number) => Math.round(n * 10) / 10;
|
||||
const overlaps = (a: Span, b: Span) => a[0] < b[1] && b[0] < a[1];
|
||||
|
||||
/**
|
||||
* One drawn bar per corridor a slot actually serves.
|
||||
*
|
||||
* A wagon reused across disjoint legs (containers Doraleh→Dire Dawa, bulk
|
||||
* Dire Dawa→Gelan) is ONE slot whose stored board/alight yards are the UNION
|
||||
* of its loads. Drawing that union as a single bar claims both loads ride the
|
||||
* whole way and hides where each one actually sits. Each allocation carries
|
||||
* its own booking yards, so group by corridor and draw one bar per group —
|
||||
* the board then reads "containers on leg 1, bulk on leg 2" truthfully.
|
||||
*
|
||||
* Falls back to the slot's own span whenever the yards are missing or not on
|
||||
* the stop list, which is exactly the previous behaviour.
|
||||
*/
|
||||
function splitByCorridor(slot: Slot, slotSpan: Span, stops: Stop[]): SlotPart[] {
|
||||
const allocations = slot.allocations ?? [];
|
||||
const whole: SlotPart[] = [
|
||||
{ slot, span: slotSpan, loaded: allocations.length > 0, allocations },
|
||||
];
|
||||
if (allocations.length < 2) return whole;
|
||||
|
||||
const idx = (yardId?: string | null) =>
|
||||
yardId ? stops.findIndex((s) => s.yardId === yardId) : -1;
|
||||
const byCorridor = new Map<string, { span: Span; allocations: typeof allocations }>();
|
||||
for (const allocation of allocations) {
|
||||
const from = idx(allocation.originYardId);
|
||||
const to = idx(allocation.destinationYardId);
|
||||
// Any allocation without a usable corridor → keep the old single bar.
|
||||
if (from < 0 || to <= from) return whole;
|
||||
const key = `${from}-${to}`;
|
||||
const entry = byCorridor.get(key);
|
||||
if (entry) entry.allocations.push(allocation);
|
||||
else byCorridor.set(key, { span: [from, to], allocations: [allocation] });
|
||||
}
|
||||
if (byCorridor.size < 2) return whole;
|
||||
|
||||
return [...byCorridor.values()]
|
||||
.sort((a, b) => a.span[0] - b.span[0])
|
||||
.map((part) => ({
|
||||
slot,
|
||||
span: part.span,
|
||||
loaded: true,
|
||||
allocations: part.allocations,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Leg board: rows = physical wagons in coupling order, columns = corridor legs
|
||||
* (A→B, B→C, …). A wagon reused on disjoint legs shows one load per leg on the
|
||||
@@ -86,11 +140,7 @@ export function LegLoadBoardPanel({
|
||||
row.position = Math.min(row.position, slot.position ?? slot.sequenceNo);
|
||||
// Coupled-but-empty consist wagons carry no slot row: they are a target only.
|
||||
if (!slot.consistOnly) {
|
||||
row.slots.push({
|
||||
slot,
|
||||
span: spanOf(slot),
|
||||
loaded: (slot.allocations?.length ?? 0) > 0,
|
||||
});
|
||||
row.slots.push(...splitByCorridor(slot, spanOf(slot), stops));
|
||||
}
|
||||
}
|
||||
return [...byKey.values()].sort((a, b) => a.position - b.position);
|
||||
@@ -211,15 +261,16 @@ export function LegLoadBoardPanel({
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{rows.map((row) => {
|
||||
const cargoTons = row.slots.reduce(
|
||||
(s, x) =>
|
||||
s +
|
||||
((x.slot.allocations ?? []).reduce(
|
||||
(a, al) => a + (al.allocatedWeightTons ?? 0),
|
||||
0,
|
||||
) || x.slot.assignedWeightTons || 0),
|
||||
0,
|
||||
);
|
||||
// Heaviest single leg, not the sum of every bar: one slot may be
|
||||
// drawn as several corridor bars, and a wagon reused on disjoint
|
||||
// legs never carries both loads at once. Summing them reported a
|
||||
// 60T wagon as 120T loaded and painted the capacity red.
|
||||
const cargoTons = row.slots.reduce((max, part) => {
|
||||
const tons =
|
||||
part.allocations.reduce((a, al) => a + (al.allocatedWeightTons ?? 0), 0) ||
|
||||
(row.slots.length === 1 ? part.slot.assignedWeightTons || 0 : 0);
|
||||
return Math.max(max, tons);
|
||||
}, 0);
|
||||
const isPickedRow = picked?.rowKey === row.key;
|
||||
// A row can take the picked load when nothing loaded on it rides
|
||||
// any of the picked load's legs.
|
||||
@@ -279,12 +330,14 @@ export function LegLoadBoardPanel({
|
||||
const isPicked = picked?.slotId === s.slot.id;
|
||||
const swappable =
|
||||
!!picked && !isPicked && !isPickedRow && s.loaded && canRearrange;
|
||||
const allocs = s.slot.allocations ?? [];
|
||||
// The allocations riding THIS bar's corridor — not the whole
|
||||
// slot's, so a leg-shared wagon labels each leg with its own load.
|
||||
const allocs = s.allocations;
|
||||
const bulk = allocs.some((a) => (a.loadType ?? "CONTAINER").toUpperCase() === "BULK");
|
||||
const containers = allocs.flatMap((a) => a.containerItems ?? []);
|
||||
cells.push(
|
||||
<Table.Td
|
||||
key={s.slot.id}
|
||||
key={`${s.slot.id}-${s.span[0]}-${s.span[1]}`}
|
||||
colSpan={Math.max(1, s.span[1] - s.span[0])}
|
||||
onClick={
|
||||
!canRearrange
|
||||
|
||||
@@ -39,11 +39,6 @@ interface InteractiveTrainConsistProps {
|
||||
onMoveLoad?: (move: WagonLoadMove) => void;
|
||||
}
|
||||
|
||||
const wagonItems = (wagon: Wagon) =>
|
||||
(wagon.allocations ?? [])
|
||||
.flatMap((a) => a.containerItems ?? [])
|
||||
.sort((a, b) => (a.positionOnWagon ?? 99) - (b.positionOnWagon ?? 99));
|
||||
|
||||
const CONTAINER_GRADIENTS = [
|
||||
"linear-gradient(180deg, var(--mantine-color-cyan-5), var(--mantine-color-cyan-7))",
|
||||
"linear-gradient(180deg, var(--mantine-color-blue-5), var(--mantine-color-blue-7))",
|
||||
@@ -192,7 +187,31 @@ function WagonCar({
|
||||
}) {
|
||||
const [dropHover, setDropHover] = useState(false);
|
||||
const wagon = slots[0]!;
|
||||
const loaded = slots.filter((s) => (s.allocations?.length ?? 0) > 0);
|
||||
const loadedSlots = slots.filter((s) => (s.allocations?.length ?? 0) > 0);
|
||||
// One drawn row per LOAD, not per slot. A wagon reused across disjoint legs
|
||||
// (containers to Dire Dawa, bulk onward) is ONE slot holding two allocations
|
||||
// with different corridors — counting slots drew that as a single row and
|
||||
// hid the second load entirely. Group the slot's allocations by their own
|
||||
// booking corridor so each load gets its own row, stacked top/bottom.
|
||||
const loaded = loadedSlots.flatMap((slot) => {
|
||||
const allocations = slot.allocations ?? [];
|
||||
const byCorridor = new Map<string, typeof allocations>();
|
||||
for (const allocation of allocations) {
|
||||
const key =
|
||||
allocation.originYardId && allocation.destinationYardId
|
||||
? `${allocation.originYardId}->${allocation.destinationYardId}`
|
||||
: "whole-route";
|
||||
byCorridor.set(key, [...(byCorridor.get(key) ?? []), allocation]);
|
||||
}
|
||||
if (byCorridor.size < 2) {
|
||||
return [{ slot, allocations, corridorKey: null as string | null }];
|
||||
}
|
||||
return [...byCorridor.entries()].map(([key, group]) => ({
|
||||
slot,
|
||||
allocations: group,
|
||||
corridorKey: key as string | null,
|
||||
}));
|
||||
});
|
||||
const shared = loaded.length > 1;
|
||||
const isEmpty = !loaded.length;
|
||||
const isBulk = loaded.some((s) =>
|
||||
@@ -201,12 +220,15 @@ function WagonCar({
|
||||
// GROSS on both sides: cargo across every slot + tare (counted ONCE — the
|
||||
// slots share the same physical wagon) vs rated payload + tare.
|
||||
const tare = wagon.tareWeightTons ?? 0;
|
||||
// Heaviest single load, not the sum: rows on disjoint legs never ride at the
|
||||
// same time, so summing them would over-report what the wagon carries.
|
||||
const cargo = loaded.reduce(
|
||||
(sum, s) =>
|
||||
sum +
|
||||
((s.allocations ?? []).reduce((a, al) => a + (al.allocatedWeightTons ?? 0), 0) ||
|
||||
s.assignedWeightTons ||
|
||||
0),
|
||||
(max, row) =>
|
||||
Math.max(
|
||||
max,
|
||||
row.allocations.reduce((a, al) => a + (al.allocatedWeightTons ?? 0), 0) ||
|
||||
(loaded.length === 1 ? row.slot.assignedWeightTons || 0 : 0),
|
||||
),
|
||||
0,
|
||||
);
|
||||
const assigned = cargo + tare;
|
||||
@@ -249,7 +271,7 @@ function WagonCar({
|
||||
<HoverCard width={280} shadow="lg" radius="md" position="top" withArrow openDelay={120}>
|
||||
<HoverCard.Target>
|
||||
<Box
|
||||
onClick={() => onSelectSlot(loaded[0] ?? wagon)}
|
||||
onClick={() => onSelectSlot(loaded[0]?.slot ?? wagon)}
|
||||
style={{ width: 148, flexShrink: 0, cursor: "pointer" }}
|
||||
>
|
||||
<Box
|
||||
@@ -389,16 +411,25 @@ function WagonCar({
|
||||
// two side by side. A shared wagon stacks its slots top/bottom
|
||||
// (intercity above, export below); each row selects ITS slot.
|
||||
<Stack gap={3} style={{ width: "100%" }}>
|
||||
{loaded.map((slot, r) => {
|
||||
const rowBulk = (slot.allocations ?? []).some((a) =>
|
||||
{loaded.map((row, r) => {
|
||||
const slot = row.slot;
|
||||
const rowBulk = row.allocations.some((a) =>
|
||||
(a.loadType ?? "").toUpperCase().includes("BULK"),
|
||||
);
|
||||
const rowBlocks = wagonItems(slot).slice(0, 2);
|
||||
// Container blocks of THIS row's allocations only, so a
|
||||
// leg-shared wagon shows each leg's own boxes.
|
||||
const rowBlocks = row.allocations
|
||||
.flatMap((a) => a.containerItems ?? [])
|
||||
.slice()
|
||||
.sort(
|
||||
(a, b) => (a.positionOnWagon ?? 0) - (b.positionOnWagon ?? 0),
|
||||
)
|
||||
.slice(0, 2);
|
||||
const rowSelected = shared && slot.id === selectedWagonId;
|
||||
const rowHeight = shared ? 20 : 26;
|
||||
return (
|
||||
<Group
|
||||
key={slot.id}
|
||||
key={`${slot.id}-${row.corridorKey ?? "all"}`}
|
||||
gap={3}
|
||||
justify="center"
|
||||
wrap="nowrap"
|
||||
@@ -549,15 +580,18 @@ function WagonCar({
|
||||
</Text>
|
||||
) : (
|
||||
<Stack gap={6}>
|
||||
{loaded.map((slot) => {
|
||||
const slotAllocation = slot.allocations?.[0];
|
||||
{loaded.map((row) => {
|
||||
const slot = row.slot;
|
||||
const slotAllocation = row.allocations[0];
|
||||
const slotCompany = getCompany(slotAllocation?.bookingId);
|
||||
const slotContainers = wagonItems(slot).map(
|
||||
(c) => c.containerNumber?.trim() || "—",
|
||||
);
|
||||
// This row's own containers, so a leg-shared wagon lists each
|
||||
// leg's boxes under its own load rather than all of them twice.
|
||||
const slotContainers = row.allocations
|
||||
.flatMap((a) => a.containerItems ?? [])
|
||||
.map((c) => c.containerNumber?.trim() || "—");
|
||||
return (
|
||||
<Stack
|
||||
key={slot.id}
|
||||
key={`${slot.id}-${row.corridorKey ?? "all"}`}
|
||||
gap={4}
|
||||
style={
|
||||
shared
|
||||
|
||||
Reference in New Issue
Block a user