mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 18:48:11 +00:00
[200~feat: add CustomsPaymentsCard and PaymentsTab components for handling customs payments and payment summaries
This commit is contained in:
@@ -138,6 +138,12 @@ import {
|
||||
import { CorridorBudget } from '../corridor-capacity.util';
|
||||
import { deriveScheduleDirection } from '../utils/derive-schedule-direction.util';
|
||||
import { computeScheduleWagonUsage } from '../utils/schedule-wagon-usage.util';
|
||||
import {
|
||||
defaultPlannedWagonYards,
|
||||
misalignedWagons,
|
||||
type PlannedWagonYards,
|
||||
scheduleYardOf,
|
||||
} from '../utils/planned-wagon-yards.util';
|
||||
import { pickLowestFreeNumber, pickTrainNumberPool } from '../train-number.util';
|
||||
import {
|
||||
bookingCargoTons,
|
||||
@@ -1557,6 +1563,7 @@ export class TrainSchedulingService {
|
||||
// and yard follow the schedule.
|
||||
let builtTrain: Train | null = null;
|
||||
let locomotiveIds: string[];
|
||||
let plannedWagonYards: PlannedWagonYards | null = null;
|
||||
if (dto.trainId) {
|
||||
builtTrain = await this.dataSource.getRepository(Train).findOne({
|
||||
where: { id: dto.trainId },
|
||||
@@ -1589,7 +1596,7 @@ export class TrainSchedulingService {
|
||||
`Train ${builtTrain.code} is not at the origin yard yet; it must arrive before this departure dispatches`,
|
||||
);
|
||||
}
|
||||
await this.assertRouteCoversWagonYards(builtTrain, route);
|
||||
plannedWagonYards = await this.defaultPlannedWagonYardsFor(builtTrain, route, scheduleWarnings);
|
||||
const conflict = await this.findTrainRouteDayConflict(
|
||||
builtTrain.id,
|
||||
route.id,
|
||||
@@ -1844,6 +1851,7 @@ export class TrainSchedulingService {
|
||||
direction,
|
||||
trainNumber: pairTrainNumber ?? undefined,
|
||||
maxWagons,
|
||||
plannedWagonYards,
|
||||
reverseWagonOrder: dto.reverseWagonOrder ?? false,
|
||||
shippingLineCompanyId: dto.shippingLineCompanyId ?? null,
|
||||
...windowFields,
|
||||
@@ -2742,6 +2750,9 @@ export class TrainSchedulingService {
|
||||
);
|
||||
}
|
||||
}
|
||||
// The yard plan this departure was SOLD against must match where the steel
|
||||
// actually stands: a wagon sold from Dire but still in Mojo cannot board.
|
||||
await this.assertPlannedYardsAligned(schedule);
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const trainNumber = await this.assignTrainNumber(manager, schedule);
|
||||
@@ -5288,15 +5299,30 @@ export class TrainSchedulingService {
|
||||
return rows[0]?.train_id ?? null;
|
||||
}
|
||||
|
||||
/** `{ wagonId: yardId }` this schedule boards each wagon from; `{}` when unset. */
|
||||
private async plannedWagonYardsOf(
|
||||
scheduleId: string | undefined,
|
||||
manager?: EntityManager,
|
||||
): Promise<PlannedWagonYards> {
|
||||
if (!scheduleId) return {};
|
||||
const runner = manager ?? this.dataSource;
|
||||
const rows: { planned_wagon_yards: PlannedWagonYards | null }[] = await runner.query(
|
||||
`SELECT planned_wagon_yards FROM freight.train_schedules WHERE id = $1`,
|
||||
[scheduleId],
|
||||
);
|
||||
return rows[0]?.planned_wagon_yards ?? {};
|
||||
}
|
||||
|
||||
private async countFleetAvailability(
|
||||
originYardId: string,
|
||||
targetScheduleId?: string,
|
||||
): Promise<Array<{ wagonTypeId: string; wagonTypeCode: string; available: number }>> {
|
||||
const [wagons, wagonTypes, builtTrainId, pinnedToTargetIds] = await Promise.all([
|
||||
const [wagons, wagonTypes, builtTrainId, pinnedToTargetIds, plan] = await Promise.all([
|
||||
this.dataSource.getRepository(Wagon).find(),
|
||||
this.dataSource.getRepository(WagonType).find(),
|
||||
this.builtTrainIdOfSchedule(targetScheduleId),
|
||||
this.pinnedPhysicalWagonIdsForSchedule(targetScheduleId),
|
||||
this.plannedWagonYardsOf(targetScheduleId),
|
||||
]);
|
||||
const typeCodeById = new Map(wagonTypes.map((type) => [type.id, type.code]));
|
||||
const counts = new Map<string, { code: string; available: number }>();
|
||||
@@ -5304,11 +5330,14 @@ export class TrainSchedulingService {
|
||||
// A built consist spread across several yards can only offer, at each yard,
|
||||
// the wagons standing there. A single-yard consist keeps the original
|
||||
// behaviour: the whole train counts wherever it currently sits.
|
||||
// Yards are the SCHEDULE's plan (falling back to the physical yard), so a
|
||||
// departure sold from Dire counts its Dire wagons even while they still
|
||||
// stand in Mojo — dispatch is what demands the two agree.
|
||||
const consistYards = builtTrainId
|
||||
? new Set(
|
||||
wagons
|
||||
.filter((w) => w.trainId === builtTrainId && w.currentYardId)
|
||||
.map((w) => w.currentYardId as string),
|
||||
.filter((w) => w.trainId === builtTrainId && scheduleYardOf(plan, w))
|
||||
.map((w) => scheduleYardOf(plan, w) as string),
|
||||
)
|
||||
: new Set<string>();
|
||||
const consistIsSplit = consistYards.size > 1;
|
||||
@@ -5320,7 +5349,7 @@ export class TrainSchedulingService {
|
||||
// counted at the yard each wagon actually stands in.
|
||||
if (builtTrainId) {
|
||||
if (wagon.trainId !== builtTrainId) continue;
|
||||
if (consistIsSplit && wagon.currentYardId !== originYardId) continue;
|
||||
if (consistIsSplit && scheduleYardOf(plan, wagon) !== originYardId) continue;
|
||||
} else {
|
||||
// Schedule-scoped availability: pins held by OTHER schedules never
|
||||
// consume a wagon here — the same physical wagon may serve the July 17
|
||||
@@ -5491,6 +5520,7 @@ export class TrainSchedulingService {
|
||||
|
||||
const pinSchedule = await this.trainSchedulesRepository.findById(scheduleId);
|
||||
const stops = pinSchedule ? await this.stopYardsForSchedule(pinSchedule) : [];
|
||||
const plannedYards = pinSchedule?.plannedWagonYards ?? {};
|
||||
|
||||
const unpinnable = this.findUnpinnableWagonSlots(
|
||||
planSlots,
|
||||
@@ -5500,6 +5530,7 @@ export class TrainSchedulingService {
|
||||
builtTrainId,
|
||||
pinnedToScheduleIds,
|
||||
stops,
|
||||
plannedYards,
|
||||
);
|
||||
if (unpinnable.length) {
|
||||
throw new BadRequestException({
|
||||
@@ -5521,6 +5552,7 @@ export class TrainSchedulingService {
|
||||
builtTrainId,
|
||||
pinnedToScheduleIds,
|
||||
reverseWagonOrder,
|
||||
plannedYards,
|
||||
);
|
||||
if (!physical) continue;
|
||||
|
||||
@@ -5570,6 +5602,7 @@ export class TrainSchedulingService {
|
||||
builtTrainId,
|
||||
pinnedToScheduleIds,
|
||||
stops,
|
||||
targetSchedule?.plannedWagonYards ?? {},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5602,6 +5635,7 @@ export class TrainSchedulingService {
|
||||
builtTrainId: string | null = null,
|
||||
pinnedToScheduleIds: Set<string> = new Set(),
|
||||
stops: string[] = [],
|
||||
plannedYards: PlannedWagonYards = {},
|
||||
): string[] {
|
||||
const violations: string[] = [];
|
||||
// One physical wagon may serve several slots whose leg spans don't overlap
|
||||
@@ -5620,6 +5654,8 @@ export class TrainSchedulingService {
|
||||
span,
|
||||
builtTrainId,
|
||||
pinnedToScheduleIds,
|
||||
false,
|
||||
plannedYards,
|
||||
);
|
||||
if (!physical) {
|
||||
violations.push(
|
||||
@@ -5650,6 +5686,7 @@ export class TrainSchedulingService {
|
||||
builtTrainId: string | null = null,
|
||||
pinnedToScheduleIds: Set<string> = new Set(),
|
||||
reverseWagonOrder = false,
|
||||
plannedYards: PlannedWagonYards = {},
|
||||
): Wagon | undefined {
|
||||
// Free for this slot = no already-assigned span on this wagon overlaps the
|
||||
// slot's own leg. Disjoint legs (alight before board) share the wagon.
|
||||
@@ -5682,8 +5719,8 @@ export class TrainSchedulingService {
|
||||
// takes slot #1). Unsequenced wagons sort after every sequenced one.
|
||||
const consistYards = new Set(
|
||||
wagons
|
||||
.filter((w) => w.trainId === builtTrainId && w.currentYardId)
|
||||
.map((w) => w.currentYardId as string),
|
||||
.filter((w) => w.trainId === builtTrainId && scheduleYardOf(plannedYards, w))
|
||||
.map((w) => scheduleYardOf(plannedYards, w) as string),
|
||||
);
|
||||
// Split consist: a slot boarding at a given yard must take a wagon that
|
||||
// physically stands there — the train cannot load a Mojo wagon at Dire.
|
||||
@@ -5696,7 +5733,7 @@ export class TrainSchedulingService {
|
||||
w.trainId === builtTrainId &&
|
||||
w.wagonTypeId === slot.wagonTypeId &&
|
||||
spanFree(w.id) &&
|
||||
(!requiredYardId || w.currentYardId === requiredYardId),
|
||||
(!requiredYardId || scheduleYardOf(plannedYards, w) === requiredYardId),
|
||||
)
|
||||
.sort((a, b) => {
|
||||
if (a.sequenceNumber == null || b.sequenceNumber == null) {
|
||||
@@ -5836,7 +5873,7 @@ export class TrainSchedulingService {
|
||||
preloadedBuiltTrainId !== undefined
|
||||
? preloadedBuiltTrainId
|
||||
: await this.builtTrainIdOfSchedule(scheduleId);
|
||||
if (builtTrainId) return this.builtTrainStock(builtTrainId);
|
||||
if (builtTrainId) return this.builtTrainStock(builtTrainId, scheduleId);
|
||||
|
||||
const boardYardIds = [
|
||||
...new Set([originYardId, ...boardingYardIds].filter((id): id is string => Boolean(id))),
|
||||
@@ -5858,11 +5895,17 @@ export class TrainSchedulingService {
|
||||
return { mode: 'YARD', remainingByTypeId, codesByTypeId };
|
||||
}
|
||||
|
||||
private async builtTrainStock(builtTrainId: string): Promise<WagonStock> {
|
||||
const wagons = await this.dataSource.getRepository(Wagon).find({
|
||||
where: { trainId: builtTrainId },
|
||||
relations: { wagonType: true },
|
||||
});
|
||||
private async builtTrainStock(
|
||||
builtTrainId: string,
|
||||
scheduleId?: string,
|
||||
): Promise<WagonStock> {
|
||||
const [wagons, plan] = await Promise.all([
|
||||
this.dataSource.getRepository(Wagon).find({
|
||||
where: { trainId: builtTrainId },
|
||||
relations: { wagonType: true },
|
||||
}),
|
||||
this.plannedWagonYardsOf(scheduleId),
|
||||
]);
|
||||
const remainingByTypeId = new Map<string, number>();
|
||||
const codesByTypeId = new Map<string, string>();
|
||||
const byYardId = new Map<string, Map<string, number>>();
|
||||
@@ -5872,10 +5915,12 @@ export class TrainSchedulingService {
|
||||
(remainingByTypeId.get(wagon.wagonTypeId) ?? 0) + 1,
|
||||
);
|
||||
if (wagon.wagonType) codesByTypeId.set(wagon.wagonTypeId, wagon.wagonType.code);
|
||||
if (wagon.currentYardId) {
|
||||
const perType = byYardId.get(wagon.currentYardId) ?? new Map<string, number>();
|
||||
// The schedule's own yard plan, not the physical yard — see plannedWagonYards.
|
||||
const yardId = scheduleYardOf(plan, wagon);
|
||||
if (yardId) {
|
||||
const perType = byYardId.get(yardId) ?? new Map<string, number>();
|
||||
perType.set(wagon.wagonTypeId, (perType.get(wagon.wagonTypeId) ?? 0) + 1);
|
||||
byYardId.set(wagon.currentYardId, perType);
|
||||
byYardId.set(yardId, perType);
|
||||
}
|
||||
}
|
||||
// Single-yard consist (the overwhelming majority): the whole train is
|
||||
@@ -6215,17 +6260,22 @@ export class TrainSchedulingService {
|
||||
}
|
||||
|
||||
/**
|
||||
* A built train's wagons may stand in several yards. The route must pass
|
||||
* through every one of them as origin or an intermediate stop — never only
|
||||
* as the final destination (the train has to pick the wagons up en route).
|
||||
* Default yard plan for a schedule created from a built train: every wagon
|
||||
* keeps the yard it physically stands in when that yard is a pickup stop of
|
||||
* the route (origin or intermediate — never only the destination, the train
|
||||
* has to collect it en route); the rest are planned at the origin and
|
||||
* reported as a warning so staff can redistribute in the schedule-yards tab.
|
||||
*/
|
||||
private async assertRouteCoversWagonYards(train: Train, route: Route) {
|
||||
private async defaultPlannedWagonYardsFor(
|
||||
train: Train,
|
||||
route: Route,
|
||||
warnings: string[],
|
||||
): Promise<PlannedWagonYards | null> {
|
||||
const wagons = await this.dataSource.getRepository(Wagon).find({
|
||||
where: { trainId: train.id },
|
||||
select: { id: true, currentYardId: true },
|
||||
select: { id: true, currentYardId: true, wagonNumber: true },
|
||||
});
|
||||
const wagonYards = [...new Set(wagons.map((w) => w.currentYardId).filter((y): y is string => !!y))];
|
||||
if (!wagonYards.length) return;
|
||||
if (!wagons.length) return null;
|
||||
|
||||
const milestones = await this.dataSource
|
||||
.getRepository(RouteMilestone)
|
||||
@@ -6236,23 +6286,203 @@ export class TrainSchedulingService {
|
||||
// Every stop except the last one is a pickup point.
|
||||
const pickupYards = new Set(stops.slice(0, -1));
|
||||
|
||||
const uncovered = wagonYards.filter((y) => !pickupYards.has(y));
|
||||
if (!uncovered.length) return;
|
||||
const { plan, rehomed } = defaultPlannedWagonYards(wagons, pickupYards, route.originYardId);
|
||||
if (rehomed.length) {
|
||||
const labels = await this.yardLabelMap([
|
||||
...new Set(rehomed.map((w) => w.currentYardId).filter((y): y is string => !!y)),
|
||||
]);
|
||||
const origin = labels.get(route.originYardId) ?? route.originYard?.label ?? 'the origin';
|
||||
const where = [...new Set(rehomed.map((w) => (w.currentYardId ? labels.get(w.currentYardId) ?? w.currentYardId : 'no yard')))].join(', ');
|
||||
warnings.push(
|
||||
`${rehomed.length} wagon(s) of train ${train.code} stand off this route (${where}) and were planned at ${origin}; ` +
|
||||
'they must be moved there before dispatch — adjust in the schedule yards tab if they should board elsewhere',
|
||||
);
|
||||
}
|
||||
return plan;
|
||||
}
|
||||
|
||||
const labels = await this.yardLabelMap(uncovered);
|
||||
const destination = stops[stops.length - 1];
|
||||
const detail = uncovered
|
||||
.map((y) =>
|
||||
y === destination
|
||||
? `${labels.get(y) ?? y} (only as the destination)`
|
||||
: `${labels.get(y) ?? y} (not on route)`,
|
||||
)
|
||||
/** Dispatch gate: every planned wagon must physically stand at its planned yard. */
|
||||
private async assertPlannedYardsAligned(schedule: TrainSchedule) {
|
||||
const builtTrainId = schedule.trainSet?.trainId;
|
||||
if (!builtTrainId) return;
|
||||
const wagons = await this.dataSource.getRepository(Wagon).find({
|
||||
where: { trainId: builtTrainId },
|
||||
select: { id: true, currentYardId: true, wagonNumber: true },
|
||||
});
|
||||
const off = misalignedWagons(schedule.plannedWagonYards, wagons);
|
||||
if (!off.length) return;
|
||||
const plan = schedule.plannedWagonYards ?? {};
|
||||
const labels = await this.yardLabelMap([
|
||||
...new Set(off.flatMap((w) => [plan[w.id], w.currentYardId]).filter((y): y is string => !!y)),
|
||||
]);
|
||||
const detail = off
|
||||
.slice(0, 5)
|
||||
.map((w) => `${w.wagonNumber} (planned ${labels.get(plan[w.id]) ?? plan[w.id]}, at ${w.currentYardId ? labels.get(w.currentYardId) ?? w.currentYardId : 'no yard'})`)
|
||||
.join(', ');
|
||||
throw new BadRequestException(
|
||||
`Route ${formatRouteLabel(route)} does not pass through every yard where train ${train.code}'s wagons stand: ${detail}`,
|
||||
throw new ConflictException(
|
||||
`Cannot dispatch: ${off.length} wagon(s) are not at the yard this schedule planned them for — ${detail}` +
|
||||
(off.length > 5 ? ', …' : '') +
|
||||
'. Move them in the train builder or re-plan them in the schedule yards tab.',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule-yards tab: where THIS departure boards each consist wagon vs
|
||||
* where it physically stands, per stop totals, and which wagons are locked
|
||||
* (already carrying this schedule's cargo).
|
||||
*/
|
||||
async getScheduleWagonYards(scheduleId: string) {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
const builtTrain = schedule.trainSet?.train;
|
||||
if (!builtTrain) {
|
||||
throw new BadRequestException(
|
||||
'This schedule was not created from a built train — it has no wagon yard plan',
|
||||
);
|
||||
}
|
||||
const stops = this.mapScheduleStops(schedule);
|
||||
const pickupYardIds = new Set(stops.slice(0, -1).map((s) => s.yardId));
|
||||
const plan = schedule.plannedWagonYards ?? {};
|
||||
const wagons = await this.dataSource.getRepository(Wagon).find({
|
||||
where: { trainId: builtTrain.id },
|
||||
relations: { wagonType: true, currentYard: true },
|
||||
order: { sequenceNumber: 'ASC' },
|
||||
});
|
||||
const lockedIds = new Set(
|
||||
(schedule.trainSet?.wagons ?? [])
|
||||
.filter((slot) => slot.physicalWagonId && (slot.allocations?.length ?? 0) > 0)
|
||||
.map((slot) => slot.physicalWagonId as string),
|
||||
);
|
||||
const offRouteYardIds = [
|
||||
...new Set(
|
||||
wagons
|
||||
.flatMap((w) => [scheduleYardOf(plan, w), w.currentYardId])
|
||||
.filter((y): y is string => !!y && !stops.some((s) => s.yardId === y)),
|
||||
),
|
||||
];
|
||||
const labels = new Map([
|
||||
...stops.map((s) => [s.yardId, s.label] as const),
|
||||
...(await this.yardLabelMap(offRouteYardIds)),
|
||||
]);
|
||||
const editable =
|
||||
schedule.status === TrainScheduleStatusEnum.Draft ||
|
||||
schedule.status === TrainScheduleStatusEnum.Scheduled;
|
||||
|
||||
const rows = wagons.map((w) => {
|
||||
const plannedYardId = scheduleYardOf(plan, w);
|
||||
const locked = lockedIds.has(w.id);
|
||||
return {
|
||||
id: w.id,
|
||||
wagonNumber: w.wagonNumber,
|
||||
sequenceNumber: w.sequenceNumber,
|
||||
wagonType: w.wagonType
|
||||
? { id: w.wagonType.id, code: w.wagonType.code, name: w.wagonType.name }
|
||||
: { id: w.wagonTypeId, code: w.wagonTypeId, name: w.wagonTypeId },
|
||||
physicalYardId: w.currentYardId,
|
||||
physicalYardLabel: w.currentYardId ? labels.get(w.currentYardId) ?? w.currentYardId : null,
|
||||
plannedYardId,
|
||||
plannedYardLabel: plannedYardId ? labels.get(plannedYardId) ?? plannedYardId : null,
|
||||
aligned: plannedYardId === w.currentYardId,
|
||||
locked,
|
||||
lockReason: locked ? 'Carries cargo booked on this schedule' : null,
|
||||
};
|
||||
});
|
||||
const perStop = stops.map((s) => ({
|
||||
yardId: s.yardId,
|
||||
label: s.label,
|
||||
pickup: pickupYardIds.has(s.yardId),
|
||||
planned: rows.filter((r) => r.plannedYardId === s.yardId).length,
|
||||
physical: rows.filter((r) => r.physicalYardId === s.yardId).length,
|
||||
}));
|
||||
return {
|
||||
scheduleId,
|
||||
train: { id: builtTrain.id, code: builtTrain.code },
|
||||
editable,
|
||||
stops: perStop,
|
||||
wagons: rows,
|
||||
misaligned: rows.filter((r) => !r.aligned).length,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-plan which yard this departure boards wagons from. Only DRAFT/SCHEDULED
|
||||
* schedules, only the train's own wagons, only pickup stops of the route,
|
||||
* never a wagon already carrying this schedule's cargo. Physical yards are
|
||||
* untouched — the train builder owns those.
|
||||
*/
|
||||
async updateScheduleWagonYards(
|
||||
scheduleId: string,
|
||||
moves: Array<{ wagonId: string; yardId: string }>,
|
||||
) {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
const builtTrain = schedule.trainSet?.train;
|
||||
if (!builtTrain) {
|
||||
throw new BadRequestException(
|
||||
'This schedule was not created from a built train — it has no wagon yard plan',
|
||||
);
|
||||
}
|
||||
if (
|
||||
schedule.status !== TrainScheduleStatusEnum.Draft &&
|
||||
schedule.status !== TrainScheduleStatusEnum.Scheduled
|
||||
) {
|
||||
throw new ConflictException(
|
||||
`Wagon yards can only be re-planned before departure (schedule is ${schedule.status})`,
|
||||
);
|
||||
}
|
||||
const stops = this.mapScheduleStops(schedule);
|
||||
const pickupYardIds = new Set(stops.slice(0, -1).map((s) => s.yardId));
|
||||
const wagons = await this.dataSource.getRepository(Wagon).find({
|
||||
where: { trainId: builtTrain.id },
|
||||
select: { id: true, currentYardId: true, wagonNumber: true },
|
||||
});
|
||||
const wagonById = new Map(wagons.map((w) => [w.id, w]));
|
||||
const lockedIds = new Set(
|
||||
(schedule.trainSet?.wagons ?? [])
|
||||
.filter((slot) => slot.physicalWagonId && (slot.allocations?.length ?? 0) > 0)
|
||||
.map((slot) => slot.physicalWagonId as string),
|
||||
);
|
||||
|
||||
const plan: PlannedWagonYards = { ...(schedule.plannedWagonYards ?? {}) };
|
||||
for (const move of moves) {
|
||||
const wagon = wagonById.get(move.wagonId);
|
||||
if (!wagon) {
|
||||
throw new BadRequestException(`Wagon ${move.wagonId} is not coupled to train ${builtTrain.code}`);
|
||||
}
|
||||
if (!pickupYardIds.has(move.yardId)) {
|
||||
throw new BadRequestException(
|
||||
`Yard ${move.yardId} is not a pickup stop of this schedule's route`,
|
||||
);
|
||||
}
|
||||
if (lockedIds.has(wagon.id) && scheduleYardOf(plan, wagon) !== move.yardId) {
|
||||
throw new ConflictException(
|
||||
`Wagon ${wagon.wagonNumber} already carries cargo booked on this schedule and cannot change yard`,
|
||||
);
|
||||
}
|
||||
plan[wagon.id] = move.yardId;
|
||||
}
|
||||
await this.dataSource
|
||||
.getRepository(TrainSchedule)
|
||||
.update(scheduleId, { plannedWagonYards: plan });
|
||||
|
||||
// ponytail: per-stop over-booking check counts bookings boarding at the
|
||||
// stop against wagons planned there, ignoring leg sharing — a warning, not
|
||||
// a gate; switch to CorridorBudget per stop if staff need exact numbers.
|
||||
const warnings: string[] = [];
|
||||
for (const stop of stops.slice(0, -1)) {
|
||||
const planned = wagons.filter((w) => scheduleYardOf(plan, w) === stop.yardId).length;
|
||||
const booked = (schedule.scheduleBookings ?? [])
|
||||
.filter((sb) => sb.booking?.originYardId === stop.yardId)
|
||||
.reduce((sum, sb) => sum + (sb.booking ? this.effectiveWagonsRequired(sb.booking) : 0), 0);
|
||||
if (booked > planned) {
|
||||
warnings.push(
|
||||
`${stop.label}: bookings boarding here need ${booked} wagon(s) but only ${planned} are planned at this yard`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return { ...(await this.getScheduleWagonYards(scheduleId)), warnings };
|
||||
}
|
||||
|
||||
private async getSchedulableRoute(routeId: string) {
|
||||
const route = await this.dataSource.getRepository(Route).findOne({
|
||||
where: { id: routeId },
|
||||
|
||||
Reference in New Issue
Block a user