mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
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:
44
apps/edr-freight-api/src/common/dto/pagination-query.dto.ts
Normal file
44
apps/edr-freight-api/src/common/dto/pagination-query.dto.ts
Normal 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';
|
||||
}
|
||||
85
apps/edr-freight-api/src/common/utils/pagination.util.ts
Normal file
85
apps/edr-freight-api/src/common/utils/pagination.util.ts
Normal 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),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user