Merge pull request #1030 from Tria-plc/freight_feature/usermanagement

leg-aware capacity and wagon sharing
This commit is contained in:
marshal
2026-07-30 20:22:44 +03:00
committed by GitHub
15 changed files with 653 additions and 119 deletions

View File

@@ -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}

View File

@@ -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,