feat(train-scheduling): add showWagonStat prop to control wagon stats visibility

This commit is contained in:
Marshal
2026-08-21 08:59:44 +00:00
parent 24f5d94709
commit a339ea620e
4 changed files with 40 additions and 37 deletions

View File

@@ -18,6 +18,8 @@ interface TrainConsistViewProps {
scheduleDetail: TrainScheduleDetail;
scheduleId: string;
maxWagons: number;
/** Hide the consist-wide Wagons stat tile (dispatch shows leg capacity instead). */
showWagonStat?: boolean;
/** Booking id selected in the side panel — highlights its wagons in the consist. */
highlightBookingId?: string | null;
}
@@ -45,6 +47,7 @@ export const TrainConsistView = ({
scheduleDetail,
scheduleId,
maxWagons,
showWagonStat = true,
highlightBookingId,
}: TrainConsistViewProps) => {
const [selectedWagonId, setSelectedWagonId] = useState<string | null>(null);
@@ -172,6 +175,7 @@ export const TrainConsistView = ({
lengthMax={lengthMax}
wagonCount={wagonsUsed}
wagonMax={maxWagons}
showWagons={showWagonStat}
/>
{/* Consist panel */}

View File

@@ -9,6 +9,12 @@ interface TrainStatsBarProps {
lengthMax: number | null;
wagonCount: number;
wagonMax: number;
/**
* The wagon tile is a consist-wide count, which reads as wrong on a
* multi-leg schedule where per-leg capacity is the real number. Dispatch
* hides it (leg capacity is shown there instead); the batch board keeps it.
*/
showWagons?: boolean;
}
function pctColor(pct: number) {
@@ -83,6 +89,7 @@ export const TrainStatsBar = ({
lengthMax,
wagonCount,
wagonMax,
showWagons = true,
}: TrainStatsBarProps) => {
const weightPct = weightMax ? (weightUsed / weightMax) * 100 : null;
const lengthPct = lengthMax ? (lengthUsed / lengthMax) * 100 : null;
@@ -95,7 +102,7 @@ export const TrainStatsBar = ({
withBorder
style={{ borderColor: "var(--mantine-color-gray-2)", background: "white" }}
>
<SimpleGrid cols={{ base: 1, xs: 3 }} spacing="lg">
<SimpleGrid cols={{ base: 1, xs: showWagons ? 3 : 2 }} spacing="lg">
<StatTile
icon={<Weight size={15} />}
label="Gross weight"
@@ -108,7 +115,7 @@ export const TrainStatsBar = ({
px={{ base: 0, xs: "lg" }}
style={{
borderLeft: "1px solid var(--mantine-color-gray-2)",
borderRight: "1px solid var(--mantine-color-gray-2)",
borderRight: showWagons ? "1px solid var(--mantine-color-gray-2)" : undefined,
}}
>
<StatTile
@@ -120,14 +127,16 @@ export const TrainStatsBar = ({
unit="m"
/>
</Box>
<StatTile
icon={<Train size={15} />}
label="Wagons"
pct={wagonPct}
current={String(wagonCount)}
max={String(wagonMax)}
unit=""
/>
{showWagons ? (
<StatTile
icon={<Train size={15} />}
label="Wagons"
pct={wagonPct}
current={String(wagonCount)}
max={String(wagonMax)}
unit=""
/>
) : null}
</SimpleGrid>
</Paper>
);

View File

@@ -848,6 +848,7 @@ export default function TrainScheduleV2DetailPage() {
scheduleDetail={schedule}
scheduleId={scheduleId ?? ""}
maxWagons={schedule.maxWagons ?? 53}
showWagonStat={false}
/>
) : (
<TrainCompositionDiagram

View File

@@ -330,17 +330,6 @@ export default function TrainScheduleV2ListPage() {
meta: { headerClassName, cellClassName },
cell: ({ row }) => (
<Stack gap={6}>
<Group gap={6} wrap="nowrap">
<Text size="sm" fw={600} lh={1.2}>
{row.original.routeName ?? "—"}
</Text>
{row.original.direction ? (
<Badge size="xs" variant="light" color={directionColor(row.original.direction)}>
{row.original.direction}
</Badge>
) : null}
<ShippingLineBadge schedule={row.original} />
</Group>
<Box maw={260}>
<RouteCorridor
origin={row.original.origin}
@@ -349,6 +338,14 @@ export default function TrainScheduleV2ListPage() {
orientation="vertical"
/>
</Box>
<Group gap={6} wrap="nowrap">
{row.original.direction ? (
<Badge size="xs" variant="light" color={directionColor(row.original.direction)}>
{row.original.direction}
</Badge>
) : null}
<ShippingLineBadge schedule={row.original} />
</Group>
</Stack>
),
},
@@ -888,12 +885,12 @@ export default function TrainScheduleV2ListPage() {
}
/**
* The row's wagon chips, matching the detail page's wagon plan: used is slots
* carrying a booking allocation, the denominator is the schedule's capacity
* (API-computed: the larger of coupled consist and planned `maxWagons`, since
* wagons are coupled on demand), and remaining excludes wagons reserved by
* bookings that have not paid yet — that space is claimed, so it is not
* bookable.
* The row's wagon chips: used is slots carrying a booking allocation, the
* denominator is the schedule's capacity (API-computed: the larger of coupled
* consist and planned `maxWagons`, since wagons are coupled on demand). Both
* are consist-wide totals, so on a multi-leg schedule they do not describe any
* single leg — the bookable/planned counts were dropped for that reason; the
* detail page's wagon plan is the per-leg source of truth.
*/
/** Green tint for departures dedicated to a shipping line (overrides direction tint). */
const SHIPPING_LINE_ROW_STYLE = {
@@ -965,20 +962,15 @@ function WagonChips({ schedule }: { schedule: TrainScheduleListItem }) {
const total = schedule.wagonsTotal ?? schedule.wagonCount;
const used = schedule.wagonsUsed;
const reserved = schedule.wagonsReserved ?? 0;
const remaining = schedule.wagonsRemaining;
if (used == null) {
if (used == null || schedule.wagonCount === 0) {
return <MetricChip value={total} label="wgn" subtle />;
}
return (
<>
<MetricChip
value={`${used}/${total}`}
label={schedule.wagonCount === 0 ? "wgn planned" : "wgn used"}
/>
<MetricChip value={`${used}/${total}`} label="wgn used" />
{reserved > used ? <MetricChip value={reserved} label="reserved" subtle /> : null}
{remaining != null ? <MetricChip value={remaining} label="bookable" subtle /> : null}
</>
);
}
@@ -1049,9 +1041,6 @@ function ScheduleCard({
{schedule.reference}
</Text>
) : null}
<Text fw={600} size="sm" lineClamp={1}>
{schedule.routeName ?? "Train schedule"}
</Text>
</Group>
<Text size="xs" c="dimmed">
{day} · {time}