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

@@ -1,5 +1,8 @@
import { PaginatedResponse } from '@edr/types';
import { Injectable } from '@nestjs/common';
import { DataSource, FindManyOptions, Repository } from 'typeorm';
import { paginateQuery } from '../../../common/utils/pagination.util';
import { ListApprovalRulesQueryDto } from '../dto/list-rule-engine-query.dto';
import { ApprovalRule } from '../entities/approval-rule.entity';
import { IApprovalRulesRepository } from '../interfaces/approval-rules.repository.interface';
@@ -30,6 +33,31 @@ export class ApprovalRulesRepository implements IApprovalRulesRepository {
return this.repo.findAndCount(options);
}
/**
* Paged list in the standard envelope. Chain grouping is preserved: rows are
* grouped by chain (requiresDirectorApproval) first, then step order.
*/
findPaged(query: ListApprovalRulesQueryDto): Promise<PaginatedResponse<ApprovalRule>> {
const qb = this.repo
.createQueryBuilder('rule')
.orderBy('rule.requiresDirectorApproval', 'ASC')
.addOrderBy('rule.stepOrder', query.sortOrder ?? 'ASC');
if (query.requiresDirectorApproval !== undefined) {
qb.andWhere('rule.requiresDirectorApproval = :requiresDirectorApproval', {
requiresDirectorApproval: query.requiresDirectorApproval,
});
}
if (query.search) {
qb.andWhere(
'(rule.actionLabel ILIKE :search OR rule.requiredRole ILIKE :search OR rule.blocksRole ILIKE :search)',
{ search: `%${query.search}%` },
);
}
return paginateQuery(qb, query);
}
async create(data: Partial<ApprovalRule>): Promise<ApprovalRule> {
const entity = this.repo.create(data);
return this.repo.save(entity);

View File

@@ -1,5 +1,8 @@
import { PaginatedResponse } from '@edr/types';
import { Injectable } from '@nestjs/common';
import { DataSource, FindManyOptions, Repository } from 'typeorm';
import { paginateQuery } from '../../../common/utils/pagination.util';
import { ListCargoTypesQueryDto } from '../dto/list-rule-engine-query.dto';
import { CargoType } from '../entities/cargo-type.entity';
import { ICargoTypesRepository } from '../interfaces/cargo-types.repository.interface';
@@ -27,6 +30,33 @@ export class CargoTypesRepository implements ICargoTypesRepository {
return this.repo.findAndCount(options);
}
/** Paged list with server-side search (name/code) in the standard envelope. */
findPaged(query: ListCargoTypesQueryDto): Promise<PaginatedResponse<CargoType>> {
const qb = this.repo
.createQueryBuilder('cargoType')
.leftJoinAndSelect('cargoType.parent', 'parent')
.orderBy(`cargoType.${query.sortBy ?? 'displayOrder'}`, query.sortOrder ?? 'ASC');
if (query.isActive !== undefined) {
qb.andWhere('cargoType.isActive = :isActive', { isActive: query.isActive });
}
if (query.requiresDirectorApproval !== undefined) {
qb.andWhere('cargoType.requiresDirectorApproval = :requiresDirectorApproval', {
requiresDirectorApproval: query.requiresDirectorApproval,
});
}
if (query.parentGroupId !== undefined) {
qb.andWhere('cargoType.parentGroupId = :parentGroupId', { parentGroupId: query.parentGroupId });
}
if (query.search) {
qb.andWhere('(cargoType.cargoTypeName ILIKE :search OR cargoType.code ILIKE :search)', {
search: `%${query.search}%`,
});
}
return paginateQuery(qb, query);
}
async create(data: Partial<CargoType>): Promise<CargoType> {
const entity = this.repo.create(data);
return this.repo.save(entity);

View File

@@ -1,5 +1,8 @@
import { PaginatedResponse } from '@edr/types';
import { Injectable } from '@nestjs/common';
import { DataSource, FindManyOptions, Repository } from 'typeorm';
import { paginateQuery } from '../../../common/utils/pagination.util';
import { ListContainerTypesQueryDto } from '../dto/list-rule-engine-query.dto';
import { ContainerType } from '../entities/container-type.entity';
import { IContainerTypesRepository } from '../interfaces/container-types.repository.interface';
@@ -27,6 +30,24 @@ export class ContainerTypesRepository implements IContainerTypesRepository {
return this.repo.findAndCount(options);
}
/** Paged list with server-side search (label/code) in the standard envelope. */
findPaged(query: ListContainerTypesQueryDto): Promise<PaginatedResponse<ContainerType>> {
const qb = this.repo
.createQueryBuilder('containerType')
.orderBy(`containerType.${query.sortBy ?? 'displayOrder'}`, query.sortOrder ?? 'ASC');
if (query.isActive !== undefined) {
qb.andWhere('containerType.isActive = :isActive', { isActive: query.isActive });
}
if (query.search) {
qb.andWhere('(containerType.label ILIKE :search OR containerType.code ILIKE :search)', {
search: `%${query.search}%`,
});
}
return paginateQuery(qb, query);
}
async create(data: Partial<ContainerType>): Promise<ContainerType> {
const entity = this.repo.create(data);
return this.repo.save(entity);

View File

@@ -1,5 +1,8 @@
import { PaginatedResponse } from '@edr/types';
import { Injectable } from '@nestjs/common';
import { DataSource, FindManyOptions, Repository } from 'typeorm';
import { paginateQuery } from '../../../common/utils/pagination.util';
import { ListPriorityConfigsQueryDto } from '../dto/list-rule-engine-query.dto';
import { PriorityConfig } from '../entities/priority-config.entity';
import { IPriorityConfigsRepository } from '../interfaces/priority-configs.repository.interface';
@@ -23,6 +26,28 @@ export class PriorityConfigsRepository implements IPriorityConfigsRepository {
return this.repo.findAndCount(options);
}
/** Paged list with server-side search (label/type/currency) in the standard envelope. */
findPaged(query: ListPriorityConfigsQueryDto): Promise<PaginatedResponse<PriorityConfig>> {
const qb = this.repo
.createQueryBuilder('config')
.orderBy(`config.${query.sortBy ?? 'displayOrder'}`, query.sortOrder ?? 'ASC');
if (query.type !== undefined) {
qb.andWhere('config.type = :type', { type: query.type });
}
if (query.isActive !== undefined) {
qb.andWhere('config.isActive = :isActive', { isActive: query.isActive });
}
if (query.search) {
qb.andWhere(
'(config.label ILIKE :search OR config.type ILIKE :search OR config.currency ILIKE :search)',
{ search: `%${query.search}%` },
);
}
return paginateQuery(qb, query);
}
async findAllActive(): Promise<PriorityConfig[]> {
return this.repo.find({
where: { isActive: true },

View File

@@ -1,5 +1,8 @@
import { PaginatedResponse } from '@edr/types';
import { Injectable } from '@nestjs/common';
import { DataSource, FindManyOptions, Repository } from 'typeorm';
import { paginateQuery } from '../../../common/utils/pagination.util';
import { ListRatesQueryDto } from '../dto/list-rule-engine-query.dto';
import { Rate } from '../entities/rate.entity';
import { IRatesRepository } from '../interfaces/rates.repository.interface';
@@ -68,6 +71,28 @@ export class RatesRepository implements IRatesRepository {
return this.repo.findAndCount(options);
}
/** Paged list with server-side search (type/status/unit/currency), newest first. */
findPaged(query: ListRatesQueryDto): Promise<PaginatedResponse<Rate>> {
const qb = this.repo
.createQueryBuilder('rate')
.orderBy('rate.createdAt', query.sortOrder ?? 'DESC');
if (query.status) {
qb.andWhere('rate.status = :status', { status: query.status });
}
if (query.rateType) {
qb.andWhere('rate.rateType = :rateType', { rateType: query.rateType });
}
if (query.search) {
qb.andWhere(
'(rate.rateType ILIKE :search OR rate.status ILIKE :search OR rate.rateUnit ILIKE :search OR rate.currency ILIKE :search)',
{ search: `%${query.search}%` },
);
}
return paginateQuery(qb, query);
}
async create(data: Partial<Rate>): Promise<Rate> {
const entity = this.repo.create(data);
return this.repo.save(entity);

View File

@@ -1,5 +1,8 @@
import { PaginatedResponse } from '@edr/types';
import { Injectable } from '@nestjs/common';
import { DataSource, FindManyOptions, Repository } from 'typeorm';
import { paginateQuery } from '../../../common/utils/pagination.util';
import { ListServiceTypesQueryDto } from '../dto/list-rule-engine-query.dto';
import { ServiceType } from '../entities/service-type.entity';
import { IServiceTypesRepository } from '../interfaces/service-types.repository.interface';
@@ -27,6 +30,30 @@ export class ServiceTypesRepository implements IServiceTypesRepository {
return this.repo.findAndCount(options);
}
/** Paged list with server-side search (name/code/description) in the standard envelope. */
findPaged(query: ListServiceTypesQueryDto): Promise<PaginatedResponse<ServiceType>> {
const qb = this.repo
.createQueryBuilder('serviceType')
.orderBy(`serviceType.${query.sortBy ?? 'displayOrder'}`, query.sortOrder ?? 'ASC');
if (query.isActive !== undefined) {
qb.andWhere('serviceType.isActive = :isActive', { isActive: query.isActive });
}
if (query.canBeBookedAlone !== undefined) {
qb.andWhere('serviceType.canBeBookedAlone = :canBeBookedAlone', {
canBeBookedAlone: query.canBeBookedAlone,
});
}
if (query.search) {
qb.andWhere(
'(serviceType.serviceName ILIKE :search OR serviceType.code ILIKE :search OR serviceType.description ILIKE :search)',
{ search: `%${query.search}%` },
);
}
return paginateQuery(qb, query);
}
async create(data: Partial<ServiceType>): Promise<ServiceType> {
const entity = this.repo.create(data);
return this.repo.save(entity);

View File

@@ -1,5 +1,8 @@
import { PaginatedResponse } from '@edr/types';
import { Injectable } from '@nestjs/common';
import { DataSource, FindManyOptions, Repository } from 'typeorm';
import { paginateQuery } from '../../../common/utils/pagination.util';
import { ListRuleEngineQueryDto } from '../dto/list-rule-engine-query.dto';
import { ShippingLine } from '../entities/shipping-line.entity';
import { IShippingLinesRepository } from '../interfaces/shipping-lines.repository.interface';
@@ -27,6 +30,25 @@ export class ShippingLinesRepository implements IShippingLinesRepository {
return this.repo.findAndCount(options);
}
/** Paged list with server-side search (label/code/mappedToCode), ordered by code. */
findPaged(query: ListRuleEngineQueryDto): Promise<PaginatedResponse<ShippingLine>> {
const qb = this.repo
.createQueryBuilder('line')
.orderBy('line.code', query.sortOrder ?? 'ASC');
if (query.isActive !== undefined) {
qb.andWhere('line.isActive = :isActive', { isActive: query.isActive });
}
if (query.search) {
qb.andWhere(
'(line.label ILIKE :search OR line.code ILIKE :search OR line.mappedToCode ILIKE :search)',
{ search: `%${query.search}%` },
);
}
return paginateQuery(qb, query);
}
async create(data: Partial<ShippingLine>): Promise<ShippingLine> {
const entity = this.repo.create(data);
return this.repo.save(entity);

View File

@@ -1,5 +1,8 @@
import { PaginatedResponse } from '@edr/types';
import { Injectable } from '@nestjs/common';
import { DataSource, FindManyOptions, Repository } from 'typeorm';
import { paginateQuery } from '../../../common/utils/pagination.util';
import { ListWeightLimitRulesQueryDto } from '../dto/list-rule-engine-query.dto';
import { WeightLimitRule } from '../entities/weight-limit-rule.entity';
import { IWeightLimitRulesRepository } from '../interfaces/weight-limit-rules.repository.interface';
@@ -59,6 +62,34 @@ export class WeightLimitRulesRepository implements IWeightLimitRulesRepository {
return this.repo.findAndCount(options);
}
/**
* Paged list with the container relation loaded, newest first. `search`
* matches the trade direction and the joined container type's label/code.
*/
findPaged(query: ListWeightLimitRulesQueryDto): Promise<PaginatedResponse<WeightLimitRule>> {
const qb = this.repo
.createQueryBuilder('rule')
.leftJoinAndSelect('rule.containerType', 'containerType')
.orderBy('rule.createdAt', query.sortOrder ?? 'DESC');
if (query.containerTypeId) {
qb.andWhere('rule.containerTypeId = :containerTypeId', {
containerTypeId: query.containerTypeId,
});
}
if (query.tradeDirection) {
qb.andWhere('rule.tradeDirection = :tradeDirection', { tradeDirection: query.tradeDirection });
}
if (query.search) {
qb.andWhere(
'(rule.tradeDirection ILIKE :search OR containerType.label ILIKE :search OR containerType.code ILIKE :search)',
{ search: `%${query.search}%` },
);
}
return paginateQuery(qb, query);
}
async create(data: Partial<WeightLimitRule>): Promise<WeightLimitRule> {
const entity = this.repo.create(data);
return this.repo.save(entity);

View File

@@ -1,5 +1,8 @@
import { PaginatedResponse } from '@edr/types';
import { Injectable } from '@nestjs/common';
import { DataSource, FindManyOptions, Repository } from 'typeorm';
import { paginateQuery } from '../../../common/utils/pagination.util';
import { ListYardsQueryDto } from '../dto/list-rule-engine-query.dto';
import { Yard } from '../entities/yard.entity';
import { IYardsRepository } from '../interfaces/yards.repository.interface';
@@ -27,6 +30,29 @@ export class YardsRepository implements IYardsRepository {
return this.repo.findAndCount(options);
}
/** Paged list with server-side search (label/code/country) in the standard envelope. */
findPaged(query: ListYardsQueryDto): Promise<PaginatedResponse<Yard>> {
const qb = this.repo
.createQueryBuilder('yard')
.orderBy(`yard.${query.sortBy ?? 'displayOrder'}`, query.sortOrder ?? 'ASC')
.addOrderBy('yard.label', 'ASC');
if (query.isActive !== undefined) {
qb.andWhere('yard.isActive = :isActive', { isActive: query.isActive });
}
if (query.country) {
qb.andWhere('yard.country = :country', { country: query.country });
}
if (query.search) {
qb.andWhere(
'(yard.label ILIKE :search OR yard.code ILIKE :search OR yard.country ILIKE :search)',
{ search: `%${query.search}%` },
);
}
return paginateQuery(qb, query);
}
async create(data: Partial<Yard>): Promise<Yard> {
const entity = this.repo.create(data);
return this.repo.save(entity);