mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
- Implemented pagination in ScheduleHistoryPanel to manage large history entries. - Updated API to support pagination parameters for schedule history. - Enhanced ConsolidationApprovalsPage with tabbed navigation and pagination for approval rows. - Introduced new types for paginated responses in bookings and train scheduling services. - Added a database migration to create an index on wagon_booking_allocations for performance improvements.
131 lines
4.7 KiB
TypeScript
131 lines
4.7 KiB
TypeScript
import { BaseRepository } from '@edr/api-common';
|
||
import { Injectable } from '@nestjs/common';
|
||
import { InjectRepository } from '@nestjs/typeorm';
|
||
import { EntityManager, Repository } from 'typeorm';
|
||
|
||
import { TrainSchedule, TrainScheduleStatus } from './entities/train-schedule.entity';
|
||
|
||
@Injectable()
|
||
export class TrainSchedulesRepository extends BaseRepository<TrainSchedule> {
|
||
constructor(
|
||
@InjectRepository(TrainSchedule)
|
||
repository: Repository<TrainSchedule>,
|
||
) {
|
||
super(repository);
|
||
}
|
||
|
||
private repo(manager?: EntityManager) {
|
||
return manager ? manager.getRepository(TrainSchedule) : this.repository;
|
||
}
|
||
|
||
/**
|
||
* Slim consist view for read paths that only need the route stops, the
|
||
* built train, and slot→allocation existence (e.g. the schedule-yards tab):
|
||
* skips the booking/company/container branches of the full graph, which
|
||
* dominate its cost and go unused there.
|
||
*/
|
||
findByIdWithConsistLite(id: string): Promise<TrainSchedule | null> {
|
||
return this.repository.findOne({
|
||
where: { id },
|
||
relationLoadStrategy: 'query',
|
||
relations: {
|
||
route: { milestones: { yard: true } },
|
||
trainSet: {
|
||
train: true,
|
||
locomotive: true,
|
||
locomotives: { locomotive: true },
|
||
wagons: { allocations: true },
|
||
},
|
||
originStation: true,
|
||
destinationStation: true,
|
||
},
|
||
});
|
||
}
|
||
|
||
findByIdWithFullGraph(id: string, manager?: EntityManager): Promise<TrainSchedule | null> {
|
||
return this.repo(manager).findOne({
|
||
where: { id },
|
||
// One SELECT per relation instead of a single monster join — the nested
|
||
// wagon×allocation×booking×container branches multiply rows catastrophically
|
||
// when joined (measured ~925ms vs ~84ms on a 21-wagon schedule).
|
||
relationLoadStrategy: 'query',
|
||
relations: {
|
||
// Yards carry the route's display name; without them formatRouteLabel
|
||
// degrades to the literal "Origin → Destination". Milestones (with
|
||
// their yards) give it the full corridor path.
|
||
route: { originYard: true, destinationYard: true, milestones: { yard: true } },
|
||
trainSet: {
|
||
locomotive: true,
|
||
locomotives: { locomotive: true },
|
||
train: true,
|
||
wagons: {
|
||
wagonType: true,
|
||
physicalWagon: true,
|
||
allocations: {
|
||
booking: { company: true, bookingContainers: { containerType: true } },
|
||
// Both size sources loaded: the item's own container_type_id FK
|
||
// (always set for a manually-entered item) and the booking-line
|
||
// fallback via bookingContainer.containerType — the marshalling
|
||
// document's 40ft/20ft tally reads whichever is present.
|
||
containerItems: { containerType: true, bookingContainer: { containerType: true } },
|
||
},
|
||
},
|
||
},
|
||
originStation: true,
|
||
destinationStation: true,
|
||
scheduleBookings: {
|
||
booking: {
|
||
company: true,
|
||
originYard: true,
|
||
destinationYard: true,
|
||
// wagonTypes feed grossBookingWeightTons the REAL tare of the
|
||
// wagon type the booking rides — without them it falls back to
|
||
// default tares and the workspace gross drifts from the validator.
|
||
bookingContainers: { containerType: { wagonTypes: true } },
|
||
cargoType: { wagonTypes: true },
|
||
},
|
||
},
|
||
},
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Light fetch for human-facing labels (notifications): reference, train
|
||
* number, departure and the two station names — none of the composition
|
||
* graph {@link findByIdWithFullGraph} drags in.
|
||
*/
|
||
findByIdWithStations(id: string): Promise<TrainSchedule | null> {
|
||
return this.repository.findOne({
|
||
where: { id },
|
||
relations: { originStation: true, destinationStation: true },
|
||
});
|
||
}
|
||
|
||
async updateStatus(
|
||
id: string,
|
||
status: TrainScheduleStatus,
|
||
extra?: Partial<TrainSchedule>,
|
||
manager?: EntityManager,
|
||
): Promise<void> {
|
||
await this.repo(manager).update(id, { status, ...extra } as never);
|
||
}
|
||
|
||
/**
|
||
* Highest NNNNN sequence already issued for `S-<year>-…` references. Includes
|
||
* soft-deleted rows so the next number never reuses one still occupying the
|
||
* unique index (see the same pattern on BookingsRepository).
|
||
*/
|
||
async maxReferenceSequence(year: number): Promise<number> {
|
||
const row = await this.repository
|
||
.createQueryBuilder('schedule')
|
||
.withDeleted()
|
||
.select(
|
||
"COALESCE(MAX(CAST(SUBSTRING(schedule.reference FROM '[0-9]+$') AS int)), 0)",
|
||
'max',
|
||
)
|
||
.where('schedule.reference LIKE :prefix', { prefix: `S-${year}-%` })
|
||
.getRawOne<{ max: string | number | null }>();
|
||
return Number(row?.max ?? 0);
|
||
}
|
||
}
|