mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 07:38:10 +00:00
leg-aware capacity and wagon sharing
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { memo, useMemo, useState } from "react";
|
||||
import { memo, useMemo, useState, type ReactNode } from "react";
|
||||
import {
|
||||
Box,
|
||||
Group,
|
||||
@@ -248,6 +248,96 @@ function RankedCard({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Consecutive-run grouping of an already-ranked lane by boarding class:
|
||||
* government first, then each booking-window cycle (windowCycleNo is 0-based,
|
||||
* so cycle 0 renders as "1st cycle window"). rankBookings sorts gov → cycle
|
||||
* asc, so consecutive runs are exactly the cycle groups.
|
||||
*/
|
||||
type CycleGroup = {
|
||||
key: string;
|
||||
color: string;
|
||||
label: string;
|
||||
sub: string | null;
|
||||
items: BatchBoardBookingDetail[];
|
||||
};
|
||||
|
||||
const CYCLE_COLORS = ["indigo", "cyan", "teal"];
|
||||
|
||||
const ordinal = (n: number) =>
|
||||
n === 1 ? "1st" : n === 2 ? "2nd" : n === 3 ? "3rd" : `${n}th`;
|
||||
|
||||
function groupMeta(b: BatchBoardBookingDetail): Omit<CycleGroup, "items"> {
|
||||
if (b.isGovernment)
|
||||
return { key: "gov", color: "grape", label: "Government", sub: "boards first" };
|
||||
if (b.windowCycleNo == null)
|
||||
return {
|
||||
key: "none",
|
||||
color: "gray",
|
||||
label: "No cycle yet",
|
||||
sub: "contract not signed",
|
||||
};
|
||||
const n = b.windowCycleNo + 1;
|
||||
return {
|
||||
key: `c${b.windowCycleNo}`,
|
||||
color: CYCLE_COLORS[b.windowCycleNo % CYCLE_COLORS.length],
|
||||
label: `${ordinal(n)} cycle window`,
|
||||
sub: n === 1 ? "booked in the first window" : "boards after earlier cycles",
|
||||
};
|
||||
}
|
||||
|
||||
function groupByCycle(items: BatchBoardBookingDetail[]): CycleGroup[] {
|
||||
const groups: CycleGroup[] = [];
|
||||
for (const b of items) {
|
||||
const meta = groupMeta(b);
|
||||
const last = groups[groups.length - 1];
|
||||
if (last && last.key === meta.key) last.items.push(b);
|
||||
else groups.push({ ...meta, items: [b] });
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** Tinted wrapper card holding one cycle's ranked bookings. */
|
||||
function CycleSection({
|
||||
group,
|
||||
children,
|
||||
}: {
|
||||
group: CycleGroup;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
const wagons = group.items.reduce((s, b) => s + b.wagons, 0);
|
||||
return (
|
||||
<Paper
|
||||
radius="md"
|
||||
p="sm"
|
||||
withBorder
|
||||
style={{
|
||||
borderColor: cardVar(group.color, 2),
|
||||
background: cardVar(group.color, 0),
|
||||
}}
|
||||
>
|
||||
<Group gap={8} mb={8} wrap="nowrap">
|
||||
<ThemeIcon size="sm" radius="xl" variant="light" color={group.color}>
|
||||
{group.key === "gov" ? <Crown size={12} /> : <Layers size={12} />}
|
||||
</ThemeIcon>
|
||||
<Text size="xs" fw={700} c={`${group.color}.8`}>
|
||||
{group.label}
|
||||
</Text>
|
||||
{group.sub ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
— {group.sub}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text size="xs" fw={600} c="dimmed" ml="auto" style={{ flexShrink: 0 }}>
|
||||
{group.items.length} booking{group.items.length === 1 ? "" : "s"} ·{" "}
|
||||
{wagons}w
|
||||
</Text>
|
||||
</Group>
|
||||
<Stack gap={6}>{children}</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
/** The capacity cut line drawn between "in the batch" and "waiting list". */
|
||||
function CapacityDivider({ used, max }: { used: number; max: number | null }) {
|
||||
const full = max != null && used >= max;
|
||||
@@ -482,19 +572,23 @@ export const PriorityTrackingTab = memo(function PriorityTrackingTab({
|
||||
</Text>
|
||||
</Text>
|
||||
</Group>
|
||||
{lanes.inBatch.map((b) => {
|
||||
rankNo += 1;
|
||||
return (
|
||||
<RankedCard
|
||||
key={b.id}
|
||||
rank={rankNo}
|
||||
booking={b}
|
||||
scoreMax={scoreMax}
|
||||
phase={phase}
|
||||
isPayPhase={isPayPhase}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{groupByCycle(lanes.inBatch).map((g) => (
|
||||
<CycleSection key={g.key} group={g}>
|
||||
{g.items.map((b) => {
|
||||
rankNo += 1;
|
||||
return (
|
||||
<RankedCard
|
||||
key={b.id}
|
||||
rank={rankNo}
|
||||
booking={b}
|
||||
scoreMax={scoreMax}
|
||||
phase={phase}
|
||||
isPayPhase={isPayPhase}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CycleSection>
|
||||
))}
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
@@ -514,19 +608,23 @@ export const PriorityTrackingTab = memo(function PriorityTrackingTab({
|
||||
</Text>
|
||||
</Text>
|
||||
</Group>
|
||||
{lanes.waiting.map((b) => {
|
||||
rankNo += 1;
|
||||
return (
|
||||
<RankedCard
|
||||
key={b.id}
|
||||
rank={rankNo}
|
||||
booking={b}
|
||||
scoreMax={scoreMax}
|
||||
phase={phase}
|
||||
isPayPhase={isPayPhase}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{groupByCycle(lanes.waiting).map((g) => (
|
||||
<CycleSection key={g.key} group={g}>
|
||||
{g.items.map((b) => {
|
||||
rankNo += 1;
|
||||
return (
|
||||
<RankedCard
|
||||
key={b.id}
|
||||
rank={rankNo}
|
||||
booking={b}
|
||||
scoreMax={scoreMax}
|
||||
phase={phase}
|
||||
isPayPhase={isPayPhase}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</CycleSection>
|
||||
))}
|
||||
</Stack>
|
||||
) : null}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user