This commit is contained in:
hagiye
2026-06-24 09:56:12 +03:00
255 changed files with 21010 additions and 7887 deletions

View File

@@ -87,6 +87,7 @@ import {
DEFAULT_BULK_WAGON_LENGTH_METERS,
DEFAULT_CONTAINER_WAGON_LENGTH_METERS,
} from './booking-batch.constants';
import { eatDay } from './batch-window.util';
import { TrainCheckpointEvent } from './entities/train-checkpoint-event.entity';
import { TrainCheckpointEventsRepository } from './train-checkpoint-events.repository';
import { RecordCheckpointDto } from './dto/record-checkpoint.dto';
@@ -167,12 +168,28 @@ export class TrainSchedulingService {
) {}
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
// resolving the schedule's route + day and filtering on the day instead.
let day: string | undefined;
let originStationId = query.originStationId;
let destinationStationId = query.destinationStationId;
if (query.trainScheduleId) {
const schedule = await this.trainSchedulesRepository.findById(query.trainScheduleId);
if (schedule?.scheduledDepartureDate) {
day = eatDay(schedule.scheduledDepartureDate);
originStationId = originStationId ?? schedule.originStationId;
destinationStationId = destinationStationId ?? schedule.destinationStationId;
}
}
const bookings = await this.bookingsRepository.findEligibleForScheduling({
freightType: query.freightType,
originStationId: query.originStationId,
destinationStationId: query.destinationStationId,
originStationId,
destinationStationId,
schedulingStatus: query.schedulingStatus,
trainScheduleId: query.trainScheduleId,
day,
});
return { count: bookings.length, items: bookings.map((b) => this.mapEligibleBooking(b)) };
}
@@ -721,27 +738,37 @@ export class TrainSchedulingService {
: null;
if (route) {
const origin = route.originYard;
const destination = route.destinationYard;
// `route.milestones` is the complete ordered corridor and already includes
// the origin (first) and destination (last) yards — `route.originYardId`
// and `route.destinationYardId` are derived from them. Use the milestones
// directly so the endpoints aren't double-counted (Addis…Addis, Dire…Dire).
const milestones = [...(route.milestones ?? [])].sort(
(a: RouteMilestone, b: RouteMilestone) => a.sequenceNo - b.sequenceNo,
);
if (milestones.length > 0) {
milestones.forEach((m, i) =>
stations.push({
sequenceNo: i,
yardId: m.yardId,
label: m.yard?.label ?? m.yard?.code ?? `Stop ${i + 1}`,
code: m.yard?.code ?? '',
}),
);
return stations;
}
// Route with no milestones recorded — fall back to its origin/destination.
const origin = route.originYard;
const destination = route.destinationYard;
stations.push({
sequenceNo: 0,
yardId: route.originYardId,
label: origin?.label ?? origin?.code ?? 'Origin',
code: origin?.code ?? '',
});
milestones.forEach((m, i) =>
stations.push({
sequenceNo: i + 1,
yardId: m.yardId,
label: m.yard?.label ?? m.yard?.code ?? `Stop ${i + 1}`,
code: m.yard?.code ?? '',
}),
);
stations.push({
sequenceNo: milestones.length + 1,
sequenceNo: 1,
yardId: route.destinationYardId,
label: destination?.label ?? destination?.code ?? 'Destination',
code: destination?.code ?? '',
@@ -775,8 +802,16 @@ export class TrainSchedulingService {
const stations = await this.buildScheduleStations(schedule);
const events = await this.trainCheckpointEventsRepository.findBySchedule(scheduleId);
// Resolve each checkpoint's position by its yard against the canonical
// corridor rather than the stored sequenceNo, so legacy checkpoints logged
// under an older station numbering still line up with the current stations.
const seqByYard = new Map(stations.map((s) => [s.yardId, s.sequenceNo]));
const resolvedSeq = (e: TrainCheckpointEvent) =>
seqByYard.get(e.yardId) ?? e.sequenceNo;
const currentSequenceNo = events.length
? Math.max(...events.map((e) => e.sequenceNo))
? Math.max(...events.map(resolvedSeq))
: -1;
return {
@@ -790,13 +825,19 @@ export class TrainSchedulingService {
actualArrivalAt: schedule.actualArrivalAt
? schedule.actualArrivalAt.toISOString()
: null,
scheduledDepartureAt: schedule.scheduledDepartureDate
? schedule.scheduledDepartureDate.toISOString()
: null,
scheduledArrivalAt: schedule.scheduledArrivalDate
? schedule.scheduledArrivalDate.toISOString()
: null,
origin: stations[0]?.label ?? null,
destination: stations[stations.length - 1]?.label ?? null,
stations,
currentSequenceNo,
checkpoints: events.map((e) => ({
id: e.id,
sequenceNo: e.sequenceNo,
sequenceNo: resolvedSeq(e),
yardId: e.yardId,
label: e.yard?.label ?? e.yard?.code ?? null,
kind: e.kind,
@@ -890,6 +931,19 @@ export class TrainSchedulingService {
});
}
await manager.query(
`UPDATE freight.bookings b
SET status = $2,
scheduling_status = $3
FROM freight.train_schedule_bookings tsb
WHERE tsb.booking_id = b.id
AND tsb.train_schedule_id = $1
AND tsb.deleted_at IS NULL
AND b.deleted_at IS NULL
AND b.status NOT IN ('DRAFT', 'REJECTED', 'CANCELLED', 'COMPLETED')`,
[scheduleId, 'IN_TRANSIT', SchedulingStatus.Dispatched],
);
if (schedule.trainSet?.locomotiveId) {
const loco = await manager
.getRepository(Locomotive)
@@ -1965,6 +2019,33 @@ export class TrainSchedulingService {
return filteredSchedules;
}
/**
* Day-level pool: the distinct EAT calendar days that have ≥1 OPEN bookable
* departure on the route. Customers pick a DAY (not a train) — so this returns
* only the day strings, no capacity, counts or train info.
*/
async getAvailableDays(
originYardId?: string,
destinationYardId?: string,
): Promise<{ days: string[] }> {
const schedules = await this.getBookableSchedules(originYardId, destinationYardId);
const days = new Set<string>();
for (const s of schedules) {
if (s.scheduleDate) days.add(eatDay(new Date(s.scheduleDate)));
}
return { days: [...days].sort() };
}
/** Whether a route has ≥1 OPEN bookable departure on a given EAT day. */
async existsOpenScheduleOnRouteDay(
originYardId: string,
destinationYardId: string,
day: string,
): Promise<boolean> {
const { days } = await this.getAvailableDays(originYardId, destinationYardId);
return days.includes(day);
}
private async mapScheduleDetail(
schedule: import('../train-schedules/entities/train-schedule.entity').TrainSchedule,
) {