Files
edr-platform/apps/edr-freight-api/src/modules/train-schedules/train-schedules.repository.ts
Marshal e2189040fa feat: add pagination to schedule history and consolidation approvals
- 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.
2026-08-23 04:49:58 +00:00

131 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}
}