mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 21:15:41 +00:00
leg-aware capacity and wagon sharing
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
Group,
|
||||
Loader,
|
||||
Paper,
|
||||
Progress,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
@@ -923,9 +924,19 @@ export default function BatchScheduleDetailPage() {
|
||||
items={[
|
||||
{
|
||||
label: "Train length",
|
||||
value: data.capacity.maxLengthMeters
|
||||
? `${fmtMeters(data.capacity.allocatedLengthMeters)} / ${fmtMeters(data.capacity.maxLengthMeters)}`
|
||||
: fmtMeters(data.capacity.allocatedLengthMeters),
|
||||
// A built train's length is its marshalled consist — always
|
||||
// the same figure the Train Builder shows.
|
||||
value: (() => {
|
||||
const length =
|
||||
data.capacity.trainLengthMeters ??
|
||||
data.capacity.allocatedLengthMeters;
|
||||
return data.capacity.maxLengthMeters
|
||||
? `${fmtMeters(length)} / ${fmtMeters(data.capacity.maxLengthMeters)}`
|
||||
: fmtMeters(length);
|
||||
})(),
|
||||
hint: data.capacity.trainLengthMeters
|
||||
? "built consist — matches Train Builder"
|
||||
: undefined,
|
||||
icon: Ruler,
|
||||
},
|
||||
{
|
||||
@@ -933,9 +944,24 @@ export default function BatchScheduleDetailPage() {
|
||||
value: data.capacity.maxWeightTons
|
||||
? `${fmtTons(data.capacity.usedWeightTons)} / ${fmtTons(data.capacity.maxWeightTons)}`
|
||||
: fmtTons(data.capacity.usedWeightTons),
|
||||
hint: "wagon tare + cargo",
|
||||
hint: (() => {
|
||||
const legs = data.capacity.legUsage;
|
||||
if (!legs?.length) return "wagon tare + cargo";
|
||||
const peak = legs.reduce((a, b) =>
|
||||
b.usedWeightTons > a.usedWeightTons ? b : a,
|
||||
);
|
||||
return `peak leg ${peak.from} → ${peak.to} · wagon tare + cargo`;
|
||||
})(),
|
||||
icon: Weight,
|
||||
},
|
||||
{
|
||||
label: "Wagons",
|
||||
value: data.capacity.maxWagons
|
||||
? `${data.capacity.allocatedWagons} / ${data.capacity.maxWagons}`
|
||||
: data.capacity.allocatedWagons,
|
||||
hint: "allocated wagon slots",
|
||||
icon: Layers,
|
||||
},
|
||||
{
|
||||
label: "Bookings",
|
||||
value: totalBookings,
|
||||
@@ -945,6 +971,92 @@ export default function BatchScheduleDetailPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Per-leg load — only multi-stop corridors have distinct legs */}
|
||||
{data.capacity.legUsage && data.capacity.legUsage.length > 1 ? (
|
||||
<Paper
|
||||
radius="lg"
|
||||
withBorder
|
||||
p="lg"
|
||||
mt="lg"
|
||||
style={{ borderColor: "var(--mantine-color-gray-2)" }}
|
||||
>
|
||||
<Group gap={8} mb="sm" wrap="nowrap">
|
||||
<ThemeIcon size={32} radius="md" variant="light" color="#F2A516">
|
||||
<Weight size={16} />
|
||||
</ThemeIcon>
|
||||
<div>
|
||||
<Text fw={700}>Load per leg</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Each leg carries only the bookings riding it — the heaviest
|
||||
leg is what the locomotive actually pulls.
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Group gap="md" align="stretch" wrap="wrap">
|
||||
{(() => {
|
||||
const legs = data.capacity.legUsage;
|
||||
const max = data.capacity.maxWeightTons;
|
||||
const peakTons = Math.max(
|
||||
...legs.map((l) => l.usedWeightTons),
|
||||
);
|
||||
return legs.map((leg, i) => {
|
||||
const pct = max
|
||||
? Math.round((leg.usedWeightTons / max) * 100)
|
||||
: null;
|
||||
const over = pct != null && pct > 100;
|
||||
const isPeak =
|
||||
peakTons > 0 && leg.usedWeightTons === peakTons;
|
||||
return (
|
||||
<Paper
|
||||
key={i}
|
||||
radius="md"
|
||||
withBorder
|
||||
p="sm"
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 210,
|
||||
borderColor: isPeak
|
||||
? "var(--mantine-color-yellow-4)"
|
||||
: "var(--mantine-color-gray-2)",
|
||||
background: isPeak
|
||||
? "var(--mantine-color-yellow-0)"
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap" mb={6}>
|
||||
<Text size="sm" fw={700} truncate>
|
||||
{leg.from} → {leg.to}
|
||||
</Text>
|
||||
{isPeak ? (
|
||||
<Badge size="xs" color="yellow" variant="filled">
|
||||
peak
|
||||
</Badge>
|
||||
) : null}
|
||||
</Group>
|
||||
<Text size="sm" fw={800} c={over ? "red.7" : "dark.5"}>
|
||||
{fmtTons(leg.usedWeightTons)}
|
||||
{max ? ` / ${fmtTons(max)}` : ""}
|
||||
{pct != null ? ` · ${pct}%` : ""}
|
||||
</Text>
|
||||
{pct != null ? (
|
||||
<Progress
|
||||
mt={6}
|
||||
value={Math.min(100, pct)}
|
||||
color={over ? "red" : pct > 90 ? "yellow" : "edr-green"}
|
||||
size="sm"
|
||||
radius="xl"
|
||||
striped={over}
|
||||
animated={over}
|
||||
/>
|
||||
) : null}
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
})()}
|
||||
</Group>
|
||||
</Paper>
|
||||
) : null}
|
||||
|
||||
{/* Booking pipeline */}
|
||||
<Paper
|
||||
radius="lg"
|
||||
@@ -1133,6 +1245,7 @@ export default function BatchScheduleDetailPage() {
|
||||
<Box mt="lg">
|
||||
<TrainCompositionDiagram
|
||||
locomotive={scheduleDetailQuery.data.trainSet?.locomotive}
|
||||
locomotives={scheduleDetailQuery.data.trainSet?.locomotives}
|
||||
wagons={scheduleDetailQuery.data.trainSet?.wagons ?? []}
|
||||
freightType={scheduleDetailQuery.data.freightType ?? null}
|
||||
trainNumber={scheduleDetailQuery.data.trainNumber}
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
Package,
|
||||
PackageCheck,
|
||||
Route as RouteIcon,
|
||||
Ruler,
|
||||
Send,
|
||||
Train,
|
||||
Weight,
|
||||
@@ -1076,6 +1077,17 @@ export default function TrainScheduleV2DetailPage() {
|
||||
: "No locomotives assigned",
|
||||
icon: Train,
|
||||
},
|
||||
...(schedule.trainSet?.totalLengthMeters
|
||||
? [
|
||||
{
|
||||
label: "Train length",
|
||||
// Physical consist length — the same figure Train Builder shows.
|
||||
value: `${schedule.trainSet.totalLengthMeters}m`,
|
||||
hint: "built consist — matches Train Builder",
|
||||
icon: Ruler,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: "Bookings",
|
||||
value: schedule.bookings?.length ?? 0,
|
||||
|
||||
Reference in New Issue
Block a user