feat(train-scheduling): log leg-slot board/alight events at checkpoints

Leg slots (stampSlotLegs — bookings boarding/alighting mid-corridor)
reaching their board/alight yard produced no ScheduleWagonAdjustmentLog
row, unlike planned couples/cuts. Without it the marshalling document
has no way to show a wagon coupling in already loaded, or uncoupling
with cargo, at a mid-corridor stop.

recordCheckpoint now logs ADD at the board yard and REMOVE at the
alight yard for these slots, deduped against existing rows since
passedYardIds re-includes earlier stops on every checkpoint call.
Purely observational — the physical wagon was already pinned to the
slot at schedule-build time, so no wagon/consist state changes.

Stage 3 of the multi-stop marshalling plan (per-event numbered docs);
stages 4-6 (generalized doc builder, endpoints, UI) still open.
This commit is contained in:
Hagernesh
2026-08-28 11:15:18 +00:00
parent cfd182cfea
commit 9cd29a21ed

View File

@@ -5007,6 +5007,76 @@ export class TrainSchedulingService {
await manager.getRepository(ScheduleWagonAdjustmentLog).save(coupleLogRows);
}
}
// Leg slots (booking legs boarding/alighting mid-corridor — see
// stampSlotLegs) reaching their board/alight yard here: logged same as
// planned couples/cuts above, so the marshalling document can show
// what coupled ALREADY LOADED / uncoupled WITH cargo at this stop.
// Purely observational — their physical wagon was already pinned to
// the slot at schedule-build time (assignPhysicalWagonsToSlots), so
// nothing here changes wagon state, only the log. Dedupe against
// existing rows (not a wagon-state flag, unlike the couple/cut blocks
// above) since passedYardIds re-includes earlier stops on every call.
const legSlotsHere = (schedule.trainSet?.wagons ?? []).filter(
(slot) =>
slot.physicalWagonId &&
((slot.boardYardId && passedYardIds.includes(slot.boardYardId)) ||
(slot.alightYardId && passedYardIds.includes(slot.alightYardId))),
);
if (legSlotsHere.length && builtTrainId) {
const legWagonIds = [
...new Set(legSlotsHere.map((slot) => slot.physicalWagonId!)),
];
const legWagonById = new Map(
(
await manager.getRepository(Wagon).find({ where: { id: In(legWagonIds) } })
).map((w) => [w.id, w]),
);
const alreadyLogged = new Set(
(
await manager.getRepository(ScheduleWagonAdjustmentLog).find({
where: {
trainScheduleId: scheduleId,
wagonId: In(legWagonIds),
action: In(['ADD', 'REMOVE']),
},
})
).map((row) => `${row.wagonId}:${row.action}:${row.yardId}`),
);
const legLogRows: ScheduleWagonAdjustmentLog[] = [];
for (const slot of legSlotsHere) {
const wagon = legWagonById.get(slot.physicalWagonId!);
if (!wagon) continue;
const events: Array<{ action: 'ADD' | 'REMOVE'; yardId: string }> = [];
if (slot.boardYardId && passedYardIds.includes(slot.boardYardId)) {
events.push({ action: 'ADD', yardId: slot.boardYardId });
}
if (slot.alightYardId && passedYardIds.includes(slot.alightYardId)) {
events.push({ action: 'REMOVE', yardId: slot.alightYardId });
}
for (const { action, yardId } of events) {
const key = `${wagon.id}:${action}:${yardId}`;
if (alreadyLogged.has(key)) continue;
alreadyLogged.add(key);
legLogRows.push(
manager.getRepository(ScheduleWagonAdjustmentLog).create({
trainScheduleId: scheduleId,
trainId: builtTrainId,
action,
wagonId: wagon.id,
wagonNumber: wagon.wagonNumber,
adjustedByUserId: null,
yardId,
occurredAt,
}),
);
}
}
if (legLogRows.length) {
await manager.getRepository(ScheduleWagonAdjustmentLog).save(legLogRows);
}
}
await manager
.getRepository(Wagon)
.createQueryBuilder()