leg-aware capacity and wagon sharing

This commit is contained in:
Marshal
2026-07-30 17:21:44 +00:00
parent 8f25fb3e8b
commit acef6870e9
15 changed files with 653 additions and 119 deletions

View File

@@ -26,6 +26,8 @@ type DragState = { sourceWagonId: string } | null;
interface InteractiveTrainConsistProps {
wagons: Wagon[];
locomotive: Locomotive | null | undefined;
/** Full locomotive set (built trains, ≥2). Takes precedence over `locomotive`. */
locomotives?: NonNullable<TrainScheduleDetail["trainSet"]>["locomotives"] | null;
/** Resolve the customer/company name for a booking id (joined from schedule bookings). */
getCompany: (bookingId: string | undefined) => string | null;
selectedWagonId: string | null;
@@ -557,6 +559,7 @@ function WagonCar({
export const InteractiveTrainConsist = ({
wagons,
locomotive,
locomotives,
getCompany,
selectedWagonId,
onSelectWagon,
@@ -565,6 +568,7 @@ export const InteractiveTrainConsist = ({
onMoveLoad,
}: InteractiveTrainConsistProps) => {
const [drag, setDrag] = useState<DragState>(null);
const locos = locomotives?.length ? locomotives : locomotive ? [locomotive] : [];
return (
<Box
style={{
@@ -589,7 +593,12 @@ export const InteractiveTrainConsist = ({
) : null}
<Group gap={0} wrap="nowrap" align="flex-start" style={{ minWidth: "min-content" }}>
{locomotive ? <LocomotiveCar locomotive={locomotive} /> : null}
{locos.map((loco, i) => (
<Group key={loco.code ?? i} gap={0} wrap="nowrap" align="flex-start">
{i > 0 ? <Coupler /> : null}
<LocomotiveCar locomotive={loco} />
</Group>
))}
{wagons.length === 0 ? (
<Text size="sm" c="dimmed" pl="md" pt="lg">
No wagons assigned
@@ -599,7 +608,7 @@ export const InteractiveTrainConsist = ({
const bookingId = wagon.allocations?.[0]?.bookingId;
return (
<Group key={wagon.id} gap={0} wrap="nowrap" align="flex-start">
{i > 0 || locomotive ? <Coupler /> : null}
{i > 0 || locos.length ? <Coupler /> : null}
<WagonCar
wagon={wagon}
company={getCompany(bookingId)}

View File

@@ -135,13 +135,27 @@ export const TrainConsistView = ({
const weightUsed = cargoUsed + tareUsed;
const lengthUsed = wagons.reduce((sum, w) => sum + (w.lengthMeters ?? 0), 0);
// Weakest locomotive caps the set — same rule the allocation engine applies.
const locos = trainSet?.locomotives?.length
? trainSet.locomotives
: trainSet?.locomotive
? [trainSet.locomotive]
: [];
const weightMax = locos.length
? Math.min(...locos.map((l) => l.maxPullWeightTons))
: null;
const lengthCaps = locos
.map((l) => l.maxTrainLengthMeters)
.filter((v): v is number => v != null);
const lengthMax = lengthCaps.length ? Math.min(...lengthCaps) : null;
return (
<Stack gap="md" style={{ width: "100%" }}>
<TrainStatsBar
weightUsed={weightUsed}
weightMax={trainSet?.locomotive?.maxPullWeightTons ?? null}
weightMax={weightMax}
lengthUsed={lengthUsed}
lengthMax={trainSet?.locomotive?.maxTrainLengthMeters ?? null}
lengthMax={lengthMax}
wagonCount={loadedCount}
wagonMax={maxWagons}
/>
@@ -202,6 +216,7 @@ export const TrainConsistView = ({
<InteractiveTrainConsist
wagons={wagons}
locomotive={trainSet?.locomotive}
locomotives={trainSet?.locomotives}
getCompany={(bookingId) => (bookingId ? companyByBooking.get(bookingId) ?? null : null)}
selectedWagonId={selectedWagonId}
onSelectWagon={(w) => setSelectedWagonId((prev) => (prev === w.id ? null : w.id))}