add goverment booking

This commit is contained in:
Marshal
2026-07-31 00:41:51 +00:00
parent 34e4a7d13f
commit 65f18b4796
35 changed files with 913 additions and 88 deletions

View File

@@ -1475,7 +1475,9 @@ export class BookingBatchService implements OnModuleInit {
const [schedules, total] = await this.trainSchedulesRepository.findAndCount({
where,
relations: {
trainSet: { locomotive: true, train: true },
// locomotives (plural) too — the caps SUM the whole set's pull; the
// single legacy column alone under-reports a two-loco train by half.
trainSet: { locomotive: true, locomotives: { locomotive: true }, train: true },
originStation: true,
destinationStation: true,
// Yards supply the route's display name for `routeName` below;
@@ -1538,7 +1540,21 @@ export class BookingBatchService implements OnModuleInit {
};
});
board.push(this.buildScheduleSummary(s, items));
board.push(
this.buildScheduleSummary(
s,
items,
new Map(
bookings.map((b) => [
b.id,
{
originYardId: b.originYardId ?? null,
destinationYardId: b.destinationYardId ?? null,
},
]),
),
),
);
}
return { items: board, meta: buildPaginationMeta(total, page, pageSize) };
@@ -1853,47 +1869,63 @@ export class BookingBatchService implements OnModuleInit {
: null;
const round2 = (value: number) => Math.round(value * 100) / 100;
// Per-leg committed weight: a booking holds weight only on the edges it
// rides, so the meter compares the HEAVIEST single edge against the pull
// limit. Whole-route bookings (or yards missing from the stop list) load
// every edge — never under-reported.
// Per-leg committed usage: a booking holds capacity only on the edges it
// rides, so every meter compares the HEAVIEST single edge against its cap
// — weight, wagons and length alike. Whole-route bookings (or yards
// missing from the stop list) load every edge — never under-reported.
const stops = legCtx?.stops ?? [];
let usedWeightTons = round2(
committed.reduce((sum, i) => sum + i.weightTons, 0),
);
let allocatedWagons = allocated.reduce((sum, i) => sum + i.wagons, 0);
let allocatedLengthMeters = round2(
allocated.reduce((sum, i) => sum + i.lengthMeters, 0),
);
let legUsage: BatchBoardSchedule["capacity"]["legUsage"] = null;
if (legCtx && stops.length > 2) {
const stopIndex = new Map(stops.map((yardId, i) => [yardId, i]));
const edges = new Array<number>(stops.length - 1).fill(0);
for (const item of committed) {
const yards = legCtx.yardsByBookingId.get(item.id);
const edgeCount = stops.length - 1;
const legOf = (bookingId: string): { from: number; to: number } => {
const yards = legCtx.yardsByBookingId.get(bookingId);
const from = yards?.originYardId
? stopIndex.get(yards.originYardId)
: undefined;
const to = yards?.destinationYardId
? stopIndex.get(yards.destinationYardId)
: undefined;
const leg =
from != null && to != null && from < to
? { from, to }
: { from: 0, to: edges.length };
for (let e = leg.from; e < leg.to; e += 1) edges[e] += item.weightTons;
return from != null && to != null && from < to
? { from, to }
: { from: 0, to: edgeCount };
};
const weightEdges = new Array<number>(edgeCount).fill(0);
for (const item of committed) {
const leg = legOf(item.id);
for (let e = leg.from; e < leg.to; e += 1) weightEdges[e] += item.weightTons;
}
const wagonEdges = new Array<number>(edgeCount).fill(0);
const lengthEdges = new Array<number>(edgeCount).fill(0);
for (const item of allocated) {
const leg = legOf(item.id);
for (let e = leg.from; e < leg.to; e += 1) {
wagonEdges[e] += item.wagons;
lengthEdges[e] += item.lengthMeters;
}
}
const label = (yardId: string) =>
legCtx.labelByYardId.get(yardId) ?? yardId;
legUsage = edges.map((weight, i) => ({
legUsage = weightEdges.map((weight, i) => ({
from: label(stops[i]),
to: label(stops[i + 1]),
usedWeightTons: round2(weight),
}));
usedWeightTons = round2(Math.max(0, ...edges));
usedWeightTons = round2(Math.max(0, ...weightEdges));
allocatedWagons = Math.max(0, ...wagonEdges);
allocatedLengthMeters = round2(Math.max(0, ...lengthEdges));
}
return {
allocatedWagons: allocated.reduce((sum, i) => sum + i.wagons, 0),
allocatedLengthMeters: round2(
allocated.reduce((sum, i) => sum + i.lengthMeters, 0),
),
allocatedWagons,
allocatedLengthMeters,
maxLengthMeters: caps ? caps.maxLengthMeters : null,
usedWeightTons,
maxWeightTons: caps ? caps.maxWeightTons : null,
@@ -1919,11 +1951,46 @@ export class BookingBatchService implements OnModuleInit {
return new Map(yards.map((y) => [y.id, y.label ?? y.code]));
}
/**
* Corridor stops + labels from the already-loaded route graph (milestones
* with yards) — the list flow must not fire a query per schedule row.
*/
private stopsFromGraph(s: TrainSchedule): {
stops: string[];
labelByYardId: Map<string, string>;
} {
const milestones = [...(s.route?.milestones ?? [])].sort(
(a, b) => a.sequenceNo - b.sequenceNo,
);
const stops: string[] = [];
const labelByYardId = new Map<string, string>();
const push = (yardId?: string | null, label?: string | null) => {
if (!yardId || labelByYardId.has(yardId)) return;
stops.push(yardId);
labelByYardId.set(yardId, label ?? yardId);
};
if (milestones.length >= 2) {
for (const m of milestones) push(m.yardId, m.yard?.label ?? m.yard?.code);
} else {
push(s.originStationId, s.originStation?.label ?? s.originStation?.code);
push(
s.destinationStationId,
s.destinationStation?.label ?? s.destinationStation?.code,
);
}
return { stops, labelByYardId };
}
private buildScheduleSummary(
s: TrainSchedule,
items: BatchBoardBooking[],
yardsByBookingId: Map<
string,
{ originYardId: string | null; destinationYardId: string | null }
>,
): BatchBoardSchedule {
const loco = trainSetLocomotiveLimits(s.trainSet);
const { stops, labelByYardId } = this.stopsFromGraph(s);
return {
scheduleId: s.id,
@@ -1966,9 +2033,9 @@ export class BookingBatchService implements OnModuleInit {
}
: null,
capacity: this.computeBoardCapacity(items, loco, s.maxWagons ?? null, {
stops: [],
labelByYardId: new Map(),
yardsByBookingId: new Map(),
stops,
labelByYardId,
yardsByBookingId,
trainLengthMeters: this.builtTrainLengthOf(s),
}),
counts: {