enhance contract and booking services with server-side search and validation improvements

- Added  parameter to  and  for server-side free-text search on contract reference, company name, and booking details.
- Introduced new validation errors in  for container clashes and space issues when creating bookings.
- Implemented paginated dropdown settings retrieval in .
- Updated  to fetch active yards using a new method that handles pagination.
- Enhanced  with a  method to fetch all records by walking through pages.
- Refactored  to support filtering and pagination in schedule listings.
- Improved  to return a paginated list of facilities.
- Updated UI components in  and  to utilize debounced search inputs for better performance.
- Added alerts in  to inform users about booking constraints related to splits and capacity.
- Enhanced  to display notifications for split bookings and capacity usage.
This commit is contained in:
Marshal
2026-07-12 10:51:31 +00:00
parent 6695c5448e
commit 4b7f6d2548
108 changed files with 3187 additions and 1368 deletions

View File

@@ -17,8 +17,21 @@ import {
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource, EntityManager, In, Not, QueryFailedError } from 'typeorm';
import {
DataSource,
EntityManager,
FindOptionsWhere,
ILike,
In,
Not,
QueryFailedError,
Raw,
} from 'typeorm';
import {
buildPaginationMeta,
normalizePagination,
} from '../../common/utils/pagination.util';
import { BookingsRepository } from '../bookings/bookings.repository';
import { Booking } from '../bookings/entities/booking.entity';
import { BookingContainer } from '../bookings/entities/booking-container.entity';
@@ -50,6 +63,10 @@ import { CreateContainerTrainScheduleDto } from './dto/create-container-train-sc
import { GetEligibleBookingsDto } from './dto/get-eligible-bookings.dto';
import { GetEligibleBulkBookingsDto } from './dto/get-eligible-bulk-bookings.dto';
import { GetEligibleContainerBookingsDto } from './dto/get-eligible-container-bookings.dto';
import {
ListTrainSchedulesQueryDto,
TrainScheduleFreightType,
} from './dto/list-train-schedules-query.dto';
import { PinWagonsDto } from './dto/pin-wagons.dto';
import { UpdateContainerItemDto } from './dto/update-container-item.dto';
import { UpdateImportLoadingStatusDto } from './dto/update-import-loading-status.dto';
@@ -2641,8 +2658,42 @@ export class TrainSchedulingService {
return Object.assign(detail, { warehouseAutomation });
}
async getContainerTrainSchedules() {
const schedules = await this.trainSchedulesRepository.findAll({
async getContainerTrainSchedules(query: ListTrainSchedulesQueryDto = {}) {
const { page, pageSize, skip, take } = normalizePagination(query);
// Exact-match filters (enum/id semantics). Freight type is derived from
// the bookings aboard — no column to match — so it rides on `id` as an
// EXISTS fragment instead.
const base: FindOptionsWhere<TrainSchedule> = {};
if (query.status) base.status = query.status;
if (query.originStationId) base.originStationId = query.originStationId;
if (query.destinationStationId) base.destinationStationId = query.destinationStationId;
if (query.freightType) base.id = this.scheduleFreightTypeFilter(query.freightType) as never;
// Search fans out across every human-recognizable label; each OR variant
// repeats the base filters so the search never widens them.
const term = query.search?.trim();
let where: FindOptionsWhere<TrainSchedule> | FindOptionsWhere<TrainSchedule>[] =
base;
if (term) {
const like = ILike(`%${term}%`);
where = [
{ ...base, reference: like as never },
{ ...base, trainNumber: like as never },
{ ...base, originStation: { label: like } },
{ ...base, destinationStation: { label: like } },
{ ...base, route: { originYard: { label: like } } },
{ ...base, route: { destinationYard: { label: like } } },
{ ...base, trainSet: { locomotive: { code: like } } },
] as FindOptionsWhere<TrainSchedule>[];
}
// Newest-created first (the client can re-sort; this is the default order).
const sortBy = query.sortBy ?? 'createdAt';
const sortOrder = query.sortOrder ?? 'DESC';
const [schedules, total] = await this.trainSchedulesRepository.findAndCount({
where,
relations: {
trainSet: { locomotive: true, locomotives: { locomotive: true } },
// Yards carry the route's display name used by mapScheduleListItem;
@@ -2652,10 +2703,14 @@ export class TrainSchedulingService {
destinationStation: true,
scheduleBookings: { booking: true },
},
// Newest-created first (the client can re-sort; this is the default order).
order: { createdAt: 'DESC', scheduledDepartureDate: 'DESC' },
order: { [sortBy]: sortOrder } as never,
skip,
take,
});
return schedules.map((s) => this.mapScheduleListItem(s));
return {
items: schedules.map((s) => this.mapScheduleListItem(s)),
meta: buildPaginationMeta(total, page, pageSize),
};
}
async getContainerTrainScheduleById(id: string) {
@@ -3869,6 +3924,33 @@ export class TrainSchedulingService {
};
}
/**
* WHERE fragment matching the DERIVED schedule freight type — the SQL mirror
* of {@link resolveScheduleFreightType} (keep the two in sync). CONTAINER /
* BULK = has bookings and every one is that kind; MIXED = both kinds aboard.
* Schedules with no bookings (type null) match nothing. Applied to `id` so
* the list query stays on findAndCount instead of a query-builder rewrite.
*/
private scheduleFreightTypeFilter(freightType: TrainScheduleFreightType) {
const hasBookingOfType = (alias: string, cmp: '=' | '<>', param: string) =>
'EXISTS (SELECT 1 FROM freight.train_schedule_bookings tsb ' +
'JOIN freight.bookings b ON b.id = tsb.booking_id AND b.deleted_at IS NULL ' +
`WHERE tsb.train_schedule_id = ${alias} AND tsb.deleted_at IS NULL ` +
`AND b.freight_type ${cmp} :${param})`;
if (freightType === 'MIXED') {
return Raw(
(alias) =>
`${hasBookingOfType(alias, '=', 'ftContainer')} AND ${hasBookingOfType(alias, '=', 'ftBulk')}`,
{ ftContainer: 'CONTAINER', ftBulk: 'BULK' },
);
}
return Raw(
(alias) =>
`${hasBookingOfType(alias, '=', 'ftIs')} AND NOT ${hasBookingOfType(alias, '<>', 'ftNot')}`,
{ ftIs: freightType, ftNot: freightType },
);
}
private resolveScheduleFreightType(
schedule: import('../train-schedules/entities/train-schedule.entity').TrainSchedule,
): 'CONTAINER' | 'BULK' | 'MIXED' | null {