enhance user management and booking features with permissions and real-time updates

This commit is contained in:
Marshal
2026-07-06 17:15:22 +00:00
parent d3de4a6abd
commit 8bd00ea78a
11 changed files with 214 additions and 254 deletions

View File

@@ -67,6 +67,7 @@ import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduli
import { UpdateScheduleWindowRuleDto } from './dto/update-schedule-window-rule.dto';
import { UpdateScheduleDateDto } from './dto/update-schedule-date.dto';
import { type BookingWindowConfig } from './booking-window.config';
import { BookingWindowGateway } from './booking-window.gateway';
import {
buildCappedWagonPlan,
computeFleetAvailability,
@@ -272,9 +273,27 @@ export class TrainSchedulingService {
private readonly trainCompositionRemovalLogRepository: TrainCompositionRemovalLogRepository,
private readonly warehouseInventoryService: WarehouseInventoryService,
private readonly pdfDocuments: WarehouseReleaseDocumentService,
private readonly bookingWindowGateway: BookingWindowGateway,
private readonly configService?: ConfigService,
) {}
/**
* Push a schedule's current booking-window state over the socket so the
* portal home card and backoffice GL/batch views update in real time —
* used for lifecycle changes outside the window tick (create, cancel,
* finalize, restamp). A push failure must never break the mutation.
*/
private async emitWindowState(scheduleId: string): Promise<void> {
try {
const fresh = await this.trainSchedulesRepository.findById(scheduleId);
if (fresh) this.bookingWindowGateway.emitPhase(fresh);
} catch (err) {
this.logger.warn(
`Booking-window push failed for ${scheduleId}: ${(err as Error).message}`,
);
}
}
async getEligibleBookings(query: GetEligibleBookingsDto) {
// Day-level pooling: when the wizard targets a schedule, surface the whole
// (route, EAT day) pool — not just bookings pre-pinned to that train — by
@@ -449,6 +468,7 @@ export class TrainSchedulingService {
this.logger.log(
`Booking-window rule overridden for schedule ${id} — reopens ${times.windowOpensAt.toISOString()}`,
);
void this.emitWindowState(id);
const fresh = await this.trainSchedulesRepository.findById(id);
return fresh ?? schedule;
@@ -522,6 +542,7 @@ export class TrainSchedulingService {
`Departure date changed for schedule ${id}${departure.toISOString()} ` +
`(window reopens ${times.windowOpensAt.toISOString()})`,
);
void this.emitWindowState(id);
const fresh = await this.trainSchedulesRepository.findById(id);
return fresh ?? schedule;
@@ -561,6 +582,9 @@ export class TrainSchedulingService {
...windowRuleSnapshot(cfg),
});
restamped += 1;
// New times take effect immediately on every card (the tick then opens
// the window within seconds if the re-derived open is already due).
void this.emitWindowState(s.id);
}
if (restamped > 0) {
this.logger.log(
@@ -764,6 +788,8 @@ export class TrainSchedulingService {
});
const created = await this.getTrainScheduleById(createdScheduleId);
// New window announced — portal home / GL cards pick it up immediately.
void this.emitWindowState(createdScheduleId);
return { ...created, warnings: scheduleWarnings };
}
@@ -1343,6 +1369,8 @@ export class TrainSchedulingService {
}
});
// Finalized — push so portal/GL cards reflect the new state instantly.
void this.emitWindowState(scheduleId);
return this.getTrainScheduleById(scheduleId);
}
@@ -1413,6 +1441,8 @@ export class TrainSchedulingService {
);
}
// Dispatch closed the window — drop it from portal/GL cards right away.
void this.emitWindowState(scheduleId);
return this.getTrainScheduleById(scheduleId);
}
@@ -2122,6 +2152,7 @@ export class TrainSchedulingService {
await this.dataSource
.getRepository(TrainSchedule)
.update(scheduleId, { bookingWindowStatus: status });
void this.emitWindowState(scheduleId);
}
/** Build the ordered station list for a schedule's corridor (origin → milestones → destination). */
@@ -2462,6 +2493,8 @@ export class TrainSchedulingService {
}
});
// Window retired (DONE) — remove the card from portal/GL lists right away.
void this.emitWindowState(id);
return this.getTrainScheduleById(id);
}
@@ -2758,7 +2791,16 @@ export class TrainSchedulingService {
take: 1,
});
return rows[0] ?? null;
} catch {
} catch (err) {
// A read failure here silently downgrades every booking window to the
// hardcoded defaults (desk 817, duration 3h, lead 3) while the settings
// UI keeps showing the saved row — a maddening mismatch. The usual cause
// is a missing column (migrations not run on this database). Scream.
this.logger.error(
`Failed to read train-scheduling global rules — booking windows are ` +
`running on HARDCODED DEFAULTS (817). Run pending migrations. ` +
`Cause: ${(err as Error).message}`,
);
return null;
}
}
@@ -3787,7 +3829,18 @@ export class TrainSchedulingService {
order: { scheduledDepartureDate: 'ASC' },
});
// A train that has already departed can never be booked, even if the window
// engine hasn't yet flipped its bookingWindowStatus off OPEN. Mirror the
// `scheduled_departure_date >= now()` guard the booking-window SQL uses so a
// past-departure schedule never leaks into the portal day pool, the schedule
// calendar, or the ET GL create-booking gate.
const now = new Date();
return schedules
.filter(
(s) =>
s.scheduledDepartureDate != null &&
s.scheduledDepartureDate > now,
)
.filter((s) => ['DRAFT', 'SCHEDULED'].includes(s.status))
.filter((s) => {
// Build the full stop list: origin -> milestones (ordered) -> destination