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

@@ -0,0 +1,44 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsIn, IsInt, IsOptional, Max, Min } from 'class-validator';
/**
* Base query DTO for every paginated list endpoint. Extend it and add the
* module's own filter fields; sort-field whitelists stay in the subclass
* because the allowed columns differ per resource.
*
* All list endpoints built on this return the shared `PaginatedResponse<T>`
* envelope from `@edr/types` (`items` + `meta`), produced by
* `common/utils/pagination.util.ts`.
*/
export class PaginationQueryDto {
@ApiPropertyOptional({ default: 1, minimum: 1 })
@IsOptional()
@Transform(({ value }) => parseInt(String(value), 10) || 1)
@IsInt()
@Min(1)
page?: number;
@ApiPropertyOptional({ default: 20, minimum: 1, maximum: 100 })
@IsOptional()
@Transform(({ value }) => parseInt(String(value), 10) || 20)
@IsInt()
@Min(1)
@Max(100)
pageSize?: number;
@ApiPropertyOptional({
description: 'Free-text search, applied server-side (resource-specific columns).',
})
@IsOptional()
@Transform(({ value }) =>
typeof value === 'string' && value.trim() ? value.trim() : undefined,
)
search?: string;
@ApiPropertyOptional({ enum: ['ASC', 'DESC'], default: 'DESC' })
@IsOptional()
@Transform(({ value }) => String(value).toUpperCase())
@IsIn(['ASC', 'DESC'])
sortOrder?: 'ASC' | 'DESC';
}

View File

@@ -0,0 +1,85 @@
import { PaginatedResponse, PaginationMeta } from '@edr/types';
import { ObjectLiteral, SelectQueryBuilder } from 'typeorm';
/** Raw page/pageSize as they arrive from a query DTO (both optional). */
export interface PageRequest {
page?: number;
pageSize?: number;
}
export interface PaginationOptions {
defaultPageSize?: number;
maxPageSize?: number;
}
export interface NormalizedPage {
page: number;
pageSize: number;
skip: number;
take: number;
}
const DEFAULT_PAGE_SIZE = 20;
const MAX_PAGE_SIZE = 100;
/** Clamp raw query values into a safe page window (page ≥ 1, pageSize capped). */
export function normalizePagination(
request: PageRequest,
options: PaginationOptions = {},
): NormalizedPage {
const defaultPageSize = options.defaultPageSize ?? DEFAULT_PAGE_SIZE;
const maxPageSize = options.maxPageSize ?? MAX_PAGE_SIZE;
const page = Math.max(1, Math.floor(request.page ?? 1) || 1);
const requested = Math.floor(request.pageSize ?? defaultPageSize) || defaultPageSize;
const pageSize = Math.min(Math.max(1, requested), maxPageSize);
return { page, pageSize, skip: (page - 1) * pageSize, take: pageSize };
}
export function buildPaginationMeta(
total: number,
page: number,
pageSize: number,
): PaginationMeta {
const totalPages = pageSize > 0 ? Math.ceil(total / pageSize) : 0;
return {
page,
pageSize,
total,
totalPages,
hasNextPage: page < totalPages,
hasPreviousPage: page > 1,
};
}
/**
* Apply skip/take to a query builder, run it, and wrap the result in the
* shared `PaginatedResponse` envelope. Ordering and filtering must already be
* applied by the caller.
*/
export async function paginateQuery<T extends ObjectLiteral>(
qb: SelectQueryBuilder<T>,
request: PageRequest,
options?: PaginationOptions,
): Promise<PaginatedResponse<T>> {
const { page, pageSize, skip, take } = normalizePagination(request, options);
const [items, total] = await qb.skip(skip).take(take).getManyAndCount();
return { items, meta: buildPaginationMeta(total, page, pageSize) };
}
/**
* Paginate an already-materialized array. Prefer `paginateQuery` (DB-level
* LIMIT/OFFSET); use this only for lists that are inherently in-memory.
*/
export function paginateArray<T>(
rows: readonly T[],
request: PageRequest,
options?: PaginationOptions,
): PaginatedResponse<T> {
const { page, pageSize, skip } = normalizePagination(request, options);
return {
items: rows.slice(skip, skip + pageSize),
meta: buildPaginationMeta(rows.length, page, pageSize),
};
}