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

@@ -10,12 +10,14 @@ import {
Patch,
Post,
Put,
Query,
} from "@nestjs/common";
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { FreightAdmin } from "../../common/booking-guards";
import { CreateDropdownOptionDto } from "./dto/create-dropdown-option.dto";
import { CreateDropdownSettingDto } from "./dto/create-dropdown-setting.dto";
import { ListDropdownSettingsQueryDto } from "./dto/list-dropdown-settings-query.dto";
import { UpdateDropdownOptionDto } from "./dto/update-dropdown-option.dto";
import { UpdateDropdownSettingDto } from "./dto/update-dropdown-setting.dto";
import { DropdownSettingsService } from "./dropdown-settings.service";
@@ -34,6 +36,15 @@ export class DropdownSettingsController {
return this.service.list();
}
// Must be declared before @Get(":id") so "paged" isn't captured as an id.
@Get("paged")
@ApiOperation({
summary: "Paged admin listing of dropdown settings (server-side search)",
})
listPaged(@Query() query: ListDropdownSettingsQueryDto) {
return this.service.listPaged(query);
}
@Get(":id")
@ApiOperation({ summary: "Get a dropdown setting by ID" })
getById(@Param("id", ParseUUIDPipe) id: string) {

View File

@@ -1,8 +1,11 @@
import { BaseRepository } from "@edr/api-common";
import { PaginatedResponse } from "@edr/types";
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { paginateQuery } from "../../common/utils/pagination.util";
import { ListDropdownSettingsQueryDto } from "./dto/list-dropdown-settings-query.dto";
import { DropdownOption } from "./entities/dropdown-option.entity";
import { DropdownSetting } from "./entities/dropdown-setting.entity";
import type { IDropdownSettingsRepository } from "./interfaces/dropdown-settings.repository.interface";
@@ -44,6 +47,27 @@ export class DropdownSettingsRepository
});
}
findPaged(
query: ListDropdownSettingsQueryDto,
): Promise<PaginatedResponse<DropdownSetting>> {
// Soft-deleted rows are excluded automatically by the query builder
// (BaseEntity's deletedAt column). Ordering mirrors findAll (label ASC).
const qb = this.repository
.createQueryBuilder("setting")
.leftJoinAndSelect("setting.children", "option")
.orderBy("setting.label", query.sortOrder ?? "ASC")
.addOrderBy("option.order", "ASC");
if (query.search) {
qb.andWhere(
"(setting.code ILIKE :search OR setting.label ILIKE :search OR setting.description ILIKE :search)",
{ search: `%${query.search}%` },
);
}
return paginateQuery(qb, query);
}
async replaceOptions(
settingId: string,
options: Array<Partial<DropdownOption>>,

View File

@@ -5,8 +5,11 @@ import {
NotFoundException,
} from "@nestjs/common";
import { PaginatedResponse } from "@edr/types";
import { CreateDropdownOptionDto } from "./dto/create-dropdown-option.dto";
import { CreateDropdownSettingDto } from "./dto/create-dropdown-setting.dto";
import { ListDropdownSettingsQueryDto } from "./dto/list-dropdown-settings-query.dto";
import { UpdateDropdownOptionDto } from "./dto/update-dropdown-option.dto";
import { UpdateDropdownSettingDto } from "./dto/update-dropdown-setting.dto";
import { DropdownOption } from "./entities/dropdown-option.entity";
@@ -16,71 +19,6 @@ import {
IDropdownSettingsRepository,
} from "./interfaces/dropdown-settings.repository.interface";
const STATIONS_TER_CODE = "stations_ter";
const DEFAULT_STATION_OPTIONS: CreateDropdownOptionDto[] = [
{
value: "inside_addis_ababa",
label: "Addis Ababa",
note: "Inside country",
order: 1,
},
{
value: "inside_adama",
label: "Adama",
note: "Inside country",
order: 2,
},
{
value: "inside_mojo",
label: "Mojo",
note: "Inside country",
order: 3,
},
{
value: "inside_awash",
label: "Awash",
note: "Inside country",
order: 4,
},
{
value: "inside_mieso",
label: "Mieso",
note: "Inside country",
order: 5,
},
{
value: "inside_dire_dawa",
label: "Dire Dawa",
note: "Inside country",
order: 6,
},
{
value: "outside_ali_sabieh",
label: "Ali Sabieh",
note: "Outside country",
order: 7,
},
{
value: "outside_holhol",
label: "Holhol",
note: "Outside country",
order: 8,
},
{
value: "outside_djibouti_city",
label: "Djibouti City",
note: "Outside country",
order: 9,
},
{
value: "outside_doraleh_terminal",
label: "Doraleh Terminal",
note: "Outside country",
order: 10,
},
];
@Injectable()
export class DropdownSettingsService {
constructor(
@@ -92,6 +30,12 @@ export class DropdownSettingsService {
return this.repository.findAll();
}
listPaged(
query: ListDropdownSettingsQueryDto,
): Promise<PaginatedResponse<DropdownSetting>> {
return this.repository.findPaged(query);
}
async getById(id: string): Promise<DropdownSetting> {
const setting = await this.repository.findById(id);
if (!setting) throw new NotFoundException(`Setting ${id} not found`);
@@ -127,34 +71,6 @@ export class DropdownSettingsService {
return this.getById(setting.id);
}
async seedDefaultStations(): Promise<void> {
const existing = await this.repository.findByCode(STATIONS_TER_CODE);
if (!existing) {
await this.create({
code: STATIONS_TER_CODE,
label: "Stations TER",
description:
"Temporary freight station list used by booking origin and destination yards.",
multiple: false,
meta: {
searchable: true,
clearable: true,
version: "temporary",
},
children: DEFAULT_STATION_OPTIONS,
});
return;
}
if ((existing.children?.length ?? 0) === 0) {
await this.repository.replaceOptions(
existing.id,
DEFAULT_STATION_OPTIONS,
);
}
}
async update(
id: string,
dto: UpdateDropdownSettingDto,

View File

@@ -0,0 +1,8 @@
import { PaginationQueryDto } from "../../../common/dto/pagination-query.dto";
/**
* Query params for the paged admin listing (`GET /dropdown-settings/paged`).
* `search` matches code, label and description server-side. The entity has no
* status/isActive flag, so the base pagination fields are all that's needed.
*/
export class ListDropdownSettingsQueryDto extends PaginationQueryDto {}

View File

@@ -1,3 +1,6 @@
import { PaginatedResponse } from "@edr/types";
import { ListDropdownSettingsQueryDto } from "../dto/list-dropdown-settings-query.dto";
import { DropdownOption } from "../entities/dropdown-option.entity";
import { DropdownSetting } from "../entities/dropdown-setting.entity";
@@ -11,6 +14,9 @@ export const DROPDOWN_SETTINGS_REPOSITORY = Symbol(
export interface IDropdownSettingsRepository {
findAll(): Promise<DropdownSetting[]>;
findPaged(
query: ListDropdownSettingsQueryDto,
): Promise<PaginatedResponse<DropdownSetting>>;
findById(id: string): Promise<DropdownSetting | null>;
findByCode(code: string): Promise<DropdownSetting | null>;