mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
add service-types and cargo-types CRUD modules with pagination, filtering
This commit is contained in:
@@ -18,6 +18,7 @@
|
|||||||
"@nestjs/common": "^11.0.0",
|
"@nestjs/common": "^11.0.0",
|
||||||
"@nestjs/config": "^4.0.0",
|
"@nestjs/config": "^4.0.0",
|
||||||
"@nestjs/core": "^11.0.0",
|
"@nestjs/core": "^11.0.0",
|
||||||
|
"@nestjs/mapped-types": "^2.1.1",
|
||||||
"@nestjs/microservices": "^11.0.0",
|
"@nestjs/microservices": "^11.0.0",
|
||||||
"@nestjs/platform-express": "^11.0.0",
|
"@nestjs/platform-express": "^11.0.0",
|
||||||
"@nestjs/swagger": "^11.4.2",
|
"@nestjs/swagger": "^11.4.2",
|
||||||
|
|||||||
206
apps/edr-freight-api/pnpm-lock.yaml
generated
Normal file
206
apps/edr-freight-api/pnpm-lock.yaml
generated
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.:
|
||||||
|
dependencies:
|
||||||
|
'@edr/api-common':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/api-common
|
||||||
|
'@edr/types':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/types
|
||||||
|
'@nestjs/common':
|
||||||
|
specifier: ^11.0.0
|
||||||
|
version: 11.1.24(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/mapped-types':
|
||||||
|
specifier: ^2.1.1
|
||||||
|
version: 2.1.1(@nestjs/common@11.1.24(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)
|
||||||
|
reflect-metadata:
|
||||||
|
specifier: ^0.2.2
|
||||||
|
version: 0.2.2
|
||||||
|
rxjs:
|
||||||
|
specifier: ^7.8.1
|
||||||
|
version: 7.8.2
|
||||||
|
devDependencies:
|
||||||
|
'@edr/eslint-config':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/config/eslint-config
|
||||||
|
'@edr/tsconfig':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../../packages/config/tsconfig
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@borewit/text-codec@0.2.2':
|
||||||
|
resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==}
|
||||||
|
|
||||||
|
'@lukeed/csprng@1.1.0':
|
||||||
|
resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
'@nestjs/common@11.1.24':
|
||||||
|
resolution: {integrity: sha512-9zHxaDDM+oXW9As6UsP5yYB+UqczBmpeSCIFWdPEtEukMnZhxODG1BBjaUcdBB8Sc1uzojSJSJlp3yFp853t1g==}
|
||||||
|
peerDependencies:
|
||||||
|
class-transformer: '>=0.4.1'
|
||||||
|
class-validator: '>=0.13.2'
|
||||||
|
reflect-metadata: ^0.1.12 || ^0.2.0
|
||||||
|
rxjs: ^7.1.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
class-transformer:
|
||||||
|
optional: true
|
||||||
|
class-validator:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@nestjs/mapped-types@2.1.1':
|
||||||
|
resolution: {integrity: sha512-SCCoMEJ6jdeI5h/N+KCVF1+pmg/hmEkNA5nHTS8Gvww7T/LCl4o1gFLinw2iQ60w7slFkszHcGLKGdazVI4F8A==}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^10.0.0 || ^11.0.0
|
||||||
|
class-transformer: ^0.4.0 || ^0.5.0
|
||||||
|
class-validator: ^0.13.0 || ^0.14.0 || ^0.15.0
|
||||||
|
reflect-metadata: ^0.1.12 || ^0.2.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
class-transformer:
|
||||||
|
optional: true
|
||||||
|
class-validator:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@tokenizer/inflate@0.4.1':
|
||||||
|
resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@tokenizer/token@0.3.0':
|
||||||
|
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
|
||||||
|
|
||||||
|
debug@4.4.3:
|
||||||
|
resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
|
||||||
|
engines: {node: '>=6.0'}
|
||||||
|
peerDependencies:
|
||||||
|
supports-color: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
supports-color:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
file-type@21.3.4:
|
||||||
|
resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==}
|
||||||
|
engines: {node: '>=20'}
|
||||||
|
|
||||||
|
ieee754@1.2.1:
|
||||||
|
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
|
||||||
|
|
||||||
|
iterare@1.2.1:
|
||||||
|
resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
load-esm@1.0.3:
|
||||||
|
resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==}
|
||||||
|
engines: {node: '>=13.2.0'}
|
||||||
|
|
||||||
|
ms@2.1.3:
|
||||||
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
reflect-metadata@0.2.2:
|
||||||
|
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==}
|
||||||
|
|
||||||
|
strtok3@10.3.5:
|
||||||
|
resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
token-types@6.1.2:
|
||||||
|
resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
|
||||||
|
engines: {node: '>=14.16'}
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
uid@2.0.2:
|
||||||
|
resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
uint8array-extras@1.5.0:
|
||||||
|
resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@borewit/text-codec@0.2.2': {}
|
||||||
|
|
||||||
|
'@lukeed/csprng@1.1.0': {}
|
||||||
|
|
||||||
|
'@nestjs/common@11.1.24(reflect-metadata@0.2.2)(rxjs@7.8.2)':
|
||||||
|
dependencies:
|
||||||
|
file-type: 21.3.4
|
||||||
|
iterare: 1.2.1
|
||||||
|
load-esm: 1.0.3
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
rxjs: 7.8.2
|
||||||
|
tslib: 2.8.1
|
||||||
|
uid: 2.0.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@nestjs/mapped-types@2.1.1(@nestjs/common@11.1.24(reflect-metadata@0.2.2)(rxjs@7.8.2))(reflect-metadata@0.2.2)':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 11.1.24(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
reflect-metadata: 0.2.2
|
||||||
|
|
||||||
|
'@tokenizer/inflate@0.4.1':
|
||||||
|
dependencies:
|
||||||
|
debug: 4.4.3
|
||||||
|
token-types: 6.1.2
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
'@tokenizer/token@0.3.0': {}
|
||||||
|
|
||||||
|
debug@4.4.3:
|
||||||
|
dependencies:
|
||||||
|
ms: 2.1.3
|
||||||
|
|
||||||
|
file-type@21.3.4:
|
||||||
|
dependencies:
|
||||||
|
'@tokenizer/inflate': 0.4.1
|
||||||
|
strtok3: 10.3.5
|
||||||
|
token-types: 6.1.2
|
||||||
|
uint8array-extras: 1.5.0
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- supports-color
|
||||||
|
|
||||||
|
ieee754@1.2.1: {}
|
||||||
|
|
||||||
|
iterare@1.2.1: {}
|
||||||
|
|
||||||
|
load-esm@1.0.3: {}
|
||||||
|
|
||||||
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
reflect-metadata@0.2.2: {}
|
||||||
|
|
||||||
|
rxjs@7.8.2:
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
|
strtok3@10.3.5:
|
||||||
|
dependencies:
|
||||||
|
'@tokenizer/token': 0.3.0
|
||||||
|
|
||||||
|
token-types@6.1.2:
|
||||||
|
dependencies:
|
||||||
|
'@borewit/text-codec': 0.2.2
|
||||||
|
'@tokenizer/token': 0.3.0
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
uid@2.0.2:
|
||||||
|
dependencies:
|
||||||
|
'@lukeed/csprng': 1.1.0
|
||||||
|
|
||||||
|
uint8array-extras@1.5.0: {}
|
||||||
@@ -18,6 +18,8 @@ import { NotificationsModule } from "./modules/notifications/notifications.modul
|
|||||||
import { FileUploadSettingsModule } from "./modules/file-upload-settings/file-upload-settings.module";
|
import { FileUploadSettingsModule } from "./modules/file-upload-settings/file-upload-settings.module";
|
||||||
import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-settings.module";
|
import { DropdownSettingsModule } from "./modules/dropdown-settings/dropdown-settings.module";
|
||||||
import { OtpModule } from './modules/otp/otp.module';
|
import { OtpModule } from './modules/otp/otp.module';
|
||||||
|
import { ServiceTypesModule } from "./modules/service-types/service-types.module";
|
||||||
|
import { CargoTypesModule } from "./modules/cargo-types/cargo-types.module";
|
||||||
import { DropdownSettingsService } from "./modules/dropdown-settings/dropdown-settings.service";
|
import { DropdownSettingsService } from "./modules/dropdown-settings/dropdown-settings.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -44,6 +46,8 @@ import { DropdownSettingsService } from "./modules/dropdown-settings/dropdown-se
|
|||||||
FileUploadSettingsModule,
|
FileUploadSettingsModule,
|
||||||
DropdownSettingsModule,
|
DropdownSettingsModule,
|
||||||
OtpModule,
|
OtpModule,
|
||||||
|
ServiceTypesModule,
|
||||||
|
CargoTypesModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements OnApplicationBootstrap {
|
export class AppModule implements OnApplicationBootstrap {
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
import { MigrationInterface, QueryRunner, Table, TableIndex, TableForeignKey } from "typeorm";
|
||||||
|
|
||||||
|
export class AddServiceTypesAndCargoTypes1748427600000 implements MigrationInterface {
|
||||||
|
name = "AddServiceTypesAndCargoTypes1748427600000";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// Create service_types table
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: "service_types",
|
||||||
|
schema: "freight",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "uuid",
|
||||||
|
isPrimary: true,
|
||||||
|
generationStrategy: "uuid",
|
||||||
|
default: "uuid_generate_v4()",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service_name",
|
||||||
|
type: "varchar",
|
||||||
|
length: "255",
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "description",
|
||||||
|
type: "text",
|
||||||
|
isNullable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "can_be_booked_alone",
|
||||||
|
type: "boolean",
|
||||||
|
default: true,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "includes_first_mile",
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "includes_last_mile",
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "includes_customs",
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "priority_bonus_points",
|
||||||
|
type: "int",
|
||||||
|
default: 0,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is_active",
|
||||||
|
type: "boolean",
|
||||||
|
default: true,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "display_order",
|
||||||
|
type: "int",
|
||||||
|
default: 1,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "created_at",
|
||||||
|
type: "timestamptz",
|
||||||
|
default: "now()",
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "updated_at",
|
||||||
|
type: "timestamptz",
|
||||||
|
default: "now()",
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deleted_at",
|
||||||
|
type: "timestamptz",
|
||||||
|
isNullable: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create indexes for service_types
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
"freight.service_types",
|
||||||
|
new TableIndex({
|
||||||
|
name: "IDX_SERVICE_TYPES_IS_ACTIVE",
|
||||||
|
columnNames: ["is_active"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
"freight.service_types",
|
||||||
|
new TableIndex({
|
||||||
|
name: "IDX_SERVICE_TYPES_DISPLAY_ORDER",
|
||||||
|
columnNames: ["display_order"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create cargo_types table
|
||||||
|
await queryRunner.createTable(
|
||||||
|
new Table({
|
||||||
|
name: "cargo_types",
|
||||||
|
schema: "freight",
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: "id",
|
||||||
|
type: "uuid",
|
||||||
|
isPrimary: true,
|
||||||
|
generationStrategy: "uuid",
|
||||||
|
default: "uuid_generate_v4()",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "cargo_type_name",
|
||||||
|
type: "varchar",
|
||||||
|
length: "255",
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "parent_group_id",
|
||||||
|
type: "uuid",
|
||||||
|
isNullable: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "show_free_text_box",
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "requires_director_approval",
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "is_active",
|
||||||
|
type: "boolean",
|
||||||
|
default: true,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "display_order",
|
||||||
|
type: "int",
|
||||||
|
default: 1,
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "created_at",
|
||||||
|
type: "timestamptz",
|
||||||
|
default: "now()",
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "updated_at",
|
||||||
|
type: "timestamptz",
|
||||||
|
default: "now()",
|
||||||
|
isNullable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deleted_at",
|
||||||
|
type: "timestamptz",
|
||||||
|
isNullable: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create indexes for cargo_types
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
"freight.cargo_types",
|
||||||
|
new TableIndex({
|
||||||
|
name: "IDX_CARGO_TYPES_IS_ACTIVE",
|
||||||
|
columnNames: ["is_active"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
"freight.cargo_types",
|
||||||
|
new TableIndex({
|
||||||
|
name: "IDX_CARGO_TYPES_DISPLAY_ORDER",
|
||||||
|
columnNames: ["display_order"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await queryRunner.createIndex(
|
||||||
|
"freight.cargo_types",
|
||||||
|
new TableIndex({
|
||||||
|
name: "IDX_CARGO_TYPES_PARENT_GROUP_ID",
|
||||||
|
columnNames: ["parent_group_id"],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create self-referencing foreign key for cargo_types
|
||||||
|
await queryRunner.createForeignKey(
|
||||||
|
"freight.cargo_types",
|
||||||
|
new TableForeignKey({
|
||||||
|
name: "FK_CARGO_TYPES_PARENT_GROUP",
|
||||||
|
columnNames: ["parent_group_id"],
|
||||||
|
referencedSchema: "freight",
|
||||||
|
referencedTableName: "cargo_types",
|
||||||
|
referencedColumnNames: ["id"],
|
||||||
|
onDelete: "SET NULL",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
// Drop foreign key first
|
||||||
|
await queryRunner.dropForeignKey("freight.cargo_types", "FK_CARGO_TYPES_PARENT_GROUP");
|
||||||
|
|
||||||
|
// Drop cargo_types table
|
||||||
|
await queryRunner.dropTable("freight.cargo_types", true);
|
||||||
|
|
||||||
|
// Drop service_types table
|
||||||
|
await queryRunner.dropTable("freight.service_types", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { CargoTypesService } from "./cargo-types.service";
|
||||||
|
import { CreateCargoTypeDto } from "./dto/create-cargo-type.dto";
|
||||||
|
import { FilterCargoTypeDto } from "./dto/filter-cargo-type.dto";
|
||||||
|
import { UpdateCargoTypeDto } from "./dto/update-cargo-type.dto";
|
||||||
|
|
||||||
|
@ApiTags("cargo-types")
|
||||||
|
@Controller("cargo-types")
|
||||||
|
// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class CargoTypesController {
|
||||||
|
constructor(private readonly service: CargoTypesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "List cargo types",
|
||||||
|
description: "Paginated list with optional filtering by isActive, requiresDirectorApproval, parentGroupId, and name search.",
|
||||||
|
})
|
||||||
|
findAll(@Query() filter: FilterCargoTypeDto) {
|
||||||
|
return this.service.findAll(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id")
|
||||||
|
@ApiOperation({ summary: "Get a cargo type by ID", description: "Returns the cargo type with parent and children relations." })
|
||||||
|
findOne(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: "Create a new cargo type" })
|
||||||
|
create(@Body() dto: CreateCargoTypeDto) {
|
||||||
|
return this.service.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":id")
|
||||||
|
@ApiOperation({ summary: "Update a cargo type" })
|
||||||
|
update(
|
||||||
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
|
@Body() dto: UpdateCargoTypeDto,
|
||||||
|
) {
|
||||||
|
return this.service.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(":id")
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: "Soft-delete a cargo type" })
|
||||||
|
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
|
||||||
|
import { CargoType } from "./entities/cargo-type.entity";
|
||||||
|
import { CARGO_TYPES_REPOSITORY } from "./interfaces/cargo-types.repository.interface";
|
||||||
|
import { CargoTypesRepository } from "./cargo-types.repository";
|
||||||
|
import { CargoTypesController } from "./cargo-types.controller";
|
||||||
|
import { CargoTypesService } from "./cargo-types.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([CargoType])],
|
||||||
|
controllers: [CargoTypesController],
|
||||||
|
providers: [
|
||||||
|
CargoTypesRepository,
|
||||||
|
{
|
||||||
|
provide: CARGO_TYPES_REPOSITORY,
|
||||||
|
useExisting: CargoTypesRepository,
|
||||||
|
},
|
||||||
|
CargoTypesService,
|
||||||
|
],
|
||||||
|
exports: [CargoTypesService],
|
||||||
|
})
|
||||||
|
export class CargoTypesModule {}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { BaseRepository } from "@edr/api-common";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
|
import { FindManyOptions, Repository } from "typeorm";
|
||||||
|
|
||||||
|
import { CargoType } from "./entities/cargo-type.entity";
|
||||||
|
import { ICargoTypesRepository } from "./interfaces/cargo-types.repository.interface";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CargoTypesRepository
|
||||||
|
extends BaseRepository<CargoType>
|
||||||
|
implements ICargoTypesRepository
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(CargoType)
|
||||||
|
repository: Repository<CargoType>,
|
||||||
|
) {
|
||||||
|
super(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
override findById(id: string): Promise<CargoType | null> {
|
||||||
|
return this.repository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: { parent: true, children: true },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override findAndCount(options?: FindManyOptions<CargoType>): Promise<[CargoType[], number]> {
|
||||||
|
return this.repository.findAndCount(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { ConflictException, Inject, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { ILike } from "typeorm";
|
||||||
|
|
||||||
|
import { CreateCargoTypeDto } from "./dto/create-cargo-type.dto";
|
||||||
|
import { FilterCargoTypeDto } from "./dto/filter-cargo-type.dto";
|
||||||
|
import { UpdateCargoTypeDto } from "./dto/update-cargo-type.dto";
|
||||||
|
import { CargoType } from "./entities/cargo-type.entity";
|
||||||
|
import {
|
||||||
|
CARGO_TYPES_REPOSITORY,
|
||||||
|
ICargoTypesRepository,
|
||||||
|
} from "./interfaces/cargo-types.repository.interface";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CargoTypesService {
|
||||||
|
constructor(
|
||||||
|
@Inject(CARGO_TYPES_REPOSITORY)
|
||||||
|
private readonly repository: ICargoTypesRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all cargo types with pagination and optional filtering.
|
||||||
|
*/
|
||||||
|
async findAll(filter: FilterCargoTypeDto): Promise<{
|
||||||
|
data: CargoType[];
|
||||||
|
meta: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (filter.isActive !== undefined) {
|
||||||
|
where.isActive = filter.isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.requiresDirectorApproval !== undefined) {
|
||||||
|
where.requiresDirectorApproval = filter.requiresDirectorApproval;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.parentGroupId !== undefined) {
|
||||||
|
where.parentGroupId = filter.parentGroupId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.search) {
|
||||||
|
where.cargoTypeName = ILike(`%${filter.search}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await this.repository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { [filter.sortBy!]: filter.sortOrder },
|
||||||
|
skip: (filter.page! - 1) * filter.pageSize!,
|
||||||
|
take: filter.pageSize,
|
||||||
|
relations: { parent: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page: filter.page!,
|
||||||
|
pageSize: filter.pageSize!,
|
||||||
|
totalPages: Math.ceil(total / filter.pageSize!),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single cargo type by ID.
|
||||||
|
*/
|
||||||
|
async findById(id: string): Promise<CargoType> {
|
||||||
|
const entity = await this.repository.findById(id);
|
||||||
|
if (!entity) {
|
||||||
|
throw new NotFoundException(`Cargo type ${id} not found`);
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new cargo type.
|
||||||
|
*/
|
||||||
|
async create(dto: CreateCargoTypeDto): Promise<CargoType> {
|
||||||
|
// Validate parent exists if provided
|
||||||
|
if (dto.parentGroupId) {
|
||||||
|
const parent = await this.repository.findById(dto.parentGroupId);
|
||||||
|
if (!parent) {
|
||||||
|
throw new NotFoundException(`Parent cargo type ${dto.parentGroupId} not found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.repository.create({
|
||||||
|
cargoTypeName: dto.cargoTypeName,
|
||||||
|
parentGroupId: dto.parentGroupId ?? null,
|
||||||
|
showFreeTextBox: dto.showFreeTextBox ?? false,
|
||||||
|
requiresDirectorApproval: dto.requiresDirectorApproval ?? false,
|
||||||
|
isActive: dto.isActive ?? true,
|
||||||
|
displayOrder: dto.displayOrder ?? 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing cargo type.
|
||||||
|
*/
|
||||||
|
async update(id: string, dto: UpdateCargoTypeDto): Promise<CargoType> {
|
||||||
|
await this.findById(id);
|
||||||
|
|
||||||
|
// Validate parent exists if provided
|
||||||
|
const parentGroupId = dto.parentGroupId;
|
||||||
|
if (parentGroupId) {
|
||||||
|
const parent = await this.repository.findById(parentGroupId);
|
||||||
|
if (!parent) {
|
||||||
|
throw new NotFoundException(`Parent cargo type ${parentGroupId} not found`);
|
||||||
|
}
|
||||||
|
// Prevent circular reference
|
||||||
|
if (parentGroupId === id) {
|
||||||
|
throw new ConflictException("A cargo type cannot be its own parent");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await this.repository.update(id, dto);
|
||||||
|
if (!updated) {
|
||||||
|
throw new NotFoundException(`Cargo type ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft-delete a cargo type.
|
||||||
|
*/
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.findById(id);
|
||||||
|
await this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsBoolean, IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateCargoTypeDto {
|
||||||
|
@ApiProperty({ description: "Cargo type name", maxLength: 255 })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
cargoTypeName!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Parent cargo type group ID (UUID) for hierarchical structure" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
parentGroupId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Show free text box for this cargo type", default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
showFreeTextBox?: boolean = false;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Requires director approval for this cargo type", default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
requiresDirectorApproval?: boolean = false;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Is the cargo type active", default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean = true;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Display order for UI", default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
displayOrder?: number = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { Transform, Type } from "class-transformer";
|
||||||
|
import { IsBoolean, IsIn, IsInt, IsOptional, IsString, IsUUID, Min } from "class-validator";
|
||||||
|
|
||||||
|
export class FilterCargoTypeDto {
|
||||||
|
@ApiPropertyOptional({ description: "Filter by active status" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
@Transform(({ value }) => value === "true" || value === true)
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Filter by requires director approval" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
@Transform(({ value }) => value === "true" || value === true)
|
||||||
|
requiresDirectorApproval?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Filter by parent group ID (or 'root' for top-level only)" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
parentGroupId?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Search by cargo type name (case-insensitive)" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: ["cargoTypeName", "displayOrder", "createdAt"], default: "displayOrder" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(["cargoTypeName", "displayOrder", "createdAt"])
|
||||||
|
sortBy?: string = "displayOrder";
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: ["ASC", "DESC"], default: "ASC" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(["ASC", "DESC"])
|
||||||
|
sortOrder?: "ASC" | "DESC" = "ASC";
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Page number", default: 1, minimum: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Items per page", default: 20, minimum: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
pageSize?: number = 20;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { PartialType } from "@nestjs/mapped-types";
|
||||||
|
|
||||||
|
import { CreateCargoTypeDto } from "./create-cargo-type.dto";
|
||||||
|
|
||||||
|
export class UpdateCargoTypeDto extends PartialType(CreateCargoTypeDto) {}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { BaseEntity } from "@edr/api-common";
|
||||||
|
import { Column, Entity, Index, ManyToOne, OneToMany, JoinColumn } from "typeorm";
|
||||||
|
|
||||||
|
@Entity({ schema: "freight", name: "cargo_types" })
|
||||||
|
@Index(["is_active"])
|
||||||
|
@Index(["display_order"])
|
||||||
|
@Index(["parent_group_id"])
|
||||||
|
export class CargoType extends BaseEntity {
|
||||||
|
@Column({ name: "cargo_type_name", type: "varchar", length: 255, nullable: false })
|
||||||
|
cargoTypeName!: string;
|
||||||
|
|
||||||
|
@Column({ name: "parent_group_id", type: "uuid", nullable: true })
|
||||||
|
parentGroupId?: string | null;
|
||||||
|
|
||||||
|
@Column({ name: "show_free_text_box", type: "boolean", default: false })
|
||||||
|
showFreeTextBox!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: "requires_director_approval", type: "boolean", default: false })
|
||||||
|
requiresDirectorApproval!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: "is_active", type: "boolean", default: true })
|
||||||
|
isActive!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: "display_order", type: "int", default: 1 })
|
||||||
|
displayOrder!: number;
|
||||||
|
|
||||||
|
@ManyToOne(() => CargoType, (cargoType) => cargoType.children, {
|
||||||
|
nullable: true,
|
||||||
|
onDelete: "SET NULL",
|
||||||
|
})
|
||||||
|
@JoinColumn({ name: "parent_group_id" })
|
||||||
|
parent?: CargoType | null;
|
||||||
|
|
||||||
|
@OneToMany(() => CargoType, (cargoType) => cargoType.parent)
|
||||||
|
children?: CargoType[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FindManyOptions } from "typeorm";
|
||||||
|
|
||||||
|
import { CargoType } from "../entities/cargo-type.entity";
|
||||||
|
|
||||||
|
export interface ICargoTypesRepository {
|
||||||
|
findById(id: string): Promise<CargoType | null>;
|
||||||
|
findAll(options?: FindManyOptions<CargoType>): Promise<CargoType[]>;
|
||||||
|
findAndCount(options?: FindManyOptions<CargoType>): Promise<[CargoType[], number]>;
|
||||||
|
create(data: Partial<CargoType>): Promise<CargoType>;
|
||||||
|
update(id: string, data: Partial<CargoType>): Promise<CargoType | null>;
|
||||||
|
softDelete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CARGO_TYPES_REPOSITORY = Symbol("CARGO_TYPES_REPOSITORY");
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from "class-validator";
|
||||||
|
|
||||||
|
export class CreateServiceTypeDto {
|
||||||
|
@ApiProperty({ description: "Service type name", maxLength: 255 })
|
||||||
|
@IsString()
|
||||||
|
@MaxLength(255)
|
||||||
|
serviceName!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Detailed description of the service" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Can be booked alone", default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
canBeBookedAlone?: boolean = true;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Includes first mile service", default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
includesFirstMile?: boolean = false;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Includes last mile service", default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
includesLastMile?: boolean = false;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Includes customs clearance", default: false })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
includesCustoms?: boolean = false;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Priority bonus points for booking priority", default: 0 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
priorityBonusPoints?: number = 0;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Is the service type active", default: true })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isActive?: boolean = true;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Display order for UI", default: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
displayOrder?: number = 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||||
|
import { Transform, Type } from "class-transformer";
|
||||||
|
import { IsBoolean, IsIn, IsInt, IsOptional, IsString, Min } from "class-validator";
|
||||||
|
|
||||||
|
export class FilterServiceTypeDto {
|
||||||
|
@ApiPropertyOptional({ description: "Filter by active status" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
@Transform(({ value }) => value === "true" || value === true)
|
||||||
|
isActive?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Filter by can be booked alone" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
@Transform(({ value }) => value === "true" || value === true)
|
||||||
|
canBeBookedAlone?: boolean;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Search by service name (case-insensitive)" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: ["serviceName", "displayOrder", "createdAt"], default: "displayOrder" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(["serviceName", "displayOrder", "createdAt"])
|
||||||
|
sortBy?: string = "displayOrder";
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ enum: ["ASC", "DESC"], default: "ASC" })
|
||||||
|
@IsOptional()
|
||||||
|
@IsIn(["ASC", "DESC"])
|
||||||
|
sortOrder?: "ASC" | "DESC" = "ASC";
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Page number", default: 1, minimum: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({ description: "Items per page", default: 20, minimum: 1 })
|
||||||
|
@IsOptional()
|
||||||
|
@Type(() => Number)
|
||||||
|
@IsInt()
|
||||||
|
@Min(1)
|
||||||
|
pageSize?: number = 20;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { PartialType } from "@nestjs/mapped-types";
|
||||||
|
|
||||||
|
import { CreateServiceTypeDto } from "./create-service-type.dto";
|
||||||
|
|
||||||
|
export class UpdateServiceTypeDto extends PartialType(CreateServiceTypeDto) {}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { BaseEntity } from "@edr/api-common";
|
||||||
|
import { Column, Entity, Index } from "typeorm";
|
||||||
|
|
||||||
|
@Entity({ schema: "freight", name: "service_types" })
|
||||||
|
@Index(["is_active"])
|
||||||
|
@Index(["display_order"])
|
||||||
|
export class ServiceType extends BaseEntity {
|
||||||
|
@Column({ name: "service_name", type: "varchar", length: 255, nullable: false })
|
||||||
|
serviceName!: string;
|
||||||
|
|
||||||
|
@Column({ name: "description", type: "text", nullable: true })
|
||||||
|
description?: string | null;
|
||||||
|
|
||||||
|
@Column({ name: "can_be_booked_alone", type: "boolean", default: true })
|
||||||
|
canBeBookedAlone!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: "includes_first_mile", type: "boolean", default: false })
|
||||||
|
includesFirstMile!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: "includes_last_mile", type: "boolean", default: false })
|
||||||
|
includesLastMile!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: "includes_customs", type: "boolean", default: false })
|
||||||
|
includesCustoms!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: "priority_bonus_points", type: "int", default: 0 })
|
||||||
|
priorityBonusPoints!: number;
|
||||||
|
|
||||||
|
@Column({ name: "is_active", type: "boolean", default: true })
|
||||||
|
isActive!: boolean;
|
||||||
|
|
||||||
|
@Column({ name: "display_order", type: "int", default: 1 })
|
||||||
|
displayOrder!: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { FindManyOptions } from "typeorm";
|
||||||
|
|
||||||
|
import { ServiceType } from "../entities/service-type.entity";
|
||||||
|
|
||||||
|
export interface IServiceTypesRepository {
|
||||||
|
findById(id: string): Promise<ServiceType | null>;
|
||||||
|
findAll(options?: FindManyOptions<ServiceType>): Promise<ServiceType[]>;
|
||||||
|
findAndCount(options?: FindManyOptions<ServiceType>): Promise<[ServiceType[], number]>;
|
||||||
|
create(data: Partial<ServiceType>): Promise<ServiceType>;
|
||||||
|
update(id: string, data: Partial<ServiceType>): Promise<ServiceType | null>;
|
||||||
|
softDelete(id: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SERVICE_TYPES_REPOSITORY = Symbol("SERVICE_TYPES_REPOSITORY");
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
Param,
|
||||||
|
ParseUUIDPipe,
|
||||||
|
Patch,
|
||||||
|
Post,
|
||||||
|
Query,
|
||||||
|
} from "@nestjs/common";
|
||||||
|
import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { CreateServiceTypeDto } from "./dto/create-service-type.dto";
|
||||||
|
import { FilterServiceTypeDto } from "./dto/filter-service-type.dto";
|
||||||
|
import { UpdateServiceTypeDto } from "./dto/update-service-type.dto";
|
||||||
|
import { ServiceTypesService } from "./service-types.service";
|
||||||
|
|
||||||
|
@ApiTags("service-types")
|
||||||
|
@Controller("service-types")
|
||||||
|
// @UseGuards(JwtAuthGuard) — TODO: add when auth package integrated
|
||||||
|
@ApiBearerAuth()
|
||||||
|
export class ServiceTypesController {
|
||||||
|
constructor(private readonly service: ServiceTypesService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
@ApiOperation({
|
||||||
|
summary: "List service types",
|
||||||
|
description: "Paginated list with optional filtering by isActive, canBeBookedAlone, and name search.",
|
||||||
|
})
|
||||||
|
findAll(@Query() filter: FilterServiceTypeDto) {
|
||||||
|
return this.service.findAll(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id")
|
||||||
|
@ApiOperation({ summary: "Get a service type by ID" })
|
||||||
|
findOne(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@ApiOperation({ summary: "Create a new service type" })
|
||||||
|
create(@Body() dto: CreateServiceTypeDto) {
|
||||||
|
return this.service.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":id")
|
||||||
|
@ApiOperation({ summary: "Update a service type" })
|
||||||
|
update(
|
||||||
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
|
@Body() dto: UpdateServiceTypeDto,
|
||||||
|
) {
|
||||||
|
return this.service.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(":id")
|
||||||
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
|
@ApiOperation({ summary: "Soft-delete a service type" })
|
||||||
|
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
|
return this.service.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
|
||||||
|
import { ServiceType } from "./entities/service-type.entity";
|
||||||
|
import { SERVICE_TYPES_REPOSITORY } from "./interfaces/service-types.repository.interface";
|
||||||
|
import { ServiceTypesRepository } from "./service-types.repository";
|
||||||
|
import { ServiceTypesController } from "./service-types.controller";
|
||||||
|
import { ServiceTypesService } from "./service-types.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([ServiceType])],
|
||||||
|
controllers: [ServiceTypesController],
|
||||||
|
providers: [
|
||||||
|
ServiceTypesRepository,
|
||||||
|
{
|
||||||
|
provide: SERVICE_TYPES_REPOSITORY,
|
||||||
|
useExisting: ServiceTypesRepository,
|
||||||
|
},
|
||||||
|
ServiceTypesService,
|
||||||
|
],
|
||||||
|
exports: [ServiceTypesService],
|
||||||
|
})
|
||||||
|
export class ServiceTypesModule {}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { BaseRepository } from "@edr/api-common";
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
|
import { FindManyOptions, Repository } from "typeorm";
|
||||||
|
|
||||||
|
import { ServiceType } from "./entities/service-type.entity";
|
||||||
|
import { IServiceTypesRepository } from "./interfaces/service-types.repository.interface";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServiceTypesRepository
|
||||||
|
extends BaseRepository<ServiceType>
|
||||||
|
implements IServiceTypesRepository
|
||||||
|
{
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(ServiceType)
|
||||||
|
repository: Repository<ServiceType>,
|
||||||
|
) {
|
||||||
|
super(repository);
|
||||||
|
}
|
||||||
|
|
||||||
|
override findById(id: string): Promise<ServiceType | null> {
|
||||||
|
return this.repository.findOne({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override findAndCount(options?: FindManyOptions<ServiceType>): Promise<[ServiceType[], number]> {
|
||||||
|
return this.repository.findAndCount(options);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { Inject, Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import { ILike } from "typeorm";
|
||||||
|
|
||||||
|
import { CreateServiceTypeDto } from "./dto/create-service-type.dto";
|
||||||
|
import { FilterServiceTypeDto } from "./dto/filter-service-type.dto";
|
||||||
|
import { UpdateServiceTypeDto } from "./dto/update-service-type.dto";
|
||||||
|
import { ServiceType } from "./entities/service-type.entity";
|
||||||
|
import {
|
||||||
|
IServiceTypesRepository,
|
||||||
|
SERVICE_TYPES_REPOSITORY,
|
||||||
|
} from "./interfaces/service-types.repository.interface";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServiceTypesService {
|
||||||
|
constructor(
|
||||||
|
@Inject(SERVICE_TYPES_REPOSITORY)
|
||||||
|
private readonly repository: IServiceTypesRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all service types with pagination and optional filtering.
|
||||||
|
*/
|
||||||
|
async findAll(filter: FilterServiceTypeDto): Promise<{
|
||||||
|
data: ServiceType[];
|
||||||
|
meta: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
|
}> {
|
||||||
|
const where: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
if (filter.isActive !== undefined) {
|
||||||
|
where.isActive = filter.isActive;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.canBeBookedAlone !== undefined) {
|
||||||
|
where.canBeBookedAlone = filter.canBeBookedAlone;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.search) {
|
||||||
|
where.serviceName = ILike(`%${filter.search}%`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await this.repository.findAndCount({
|
||||||
|
where,
|
||||||
|
order: { [filter.sortBy!]: filter.sortOrder },
|
||||||
|
skip: (filter.page! - 1) * filter.pageSize!,
|
||||||
|
take: filter.pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
meta: {
|
||||||
|
total,
|
||||||
|
page: filter.page!,
|
||||||
|
pageSize: filter.pageSize!,
|
||||||
|
totalPages: Math.ceil(total / filter.pageSize!),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a single service type by ID.
|
||||||
|
*/
|
||||||
|
async findById(id: string): Promise<ServiceType> {
|
||||||
|
const entity = await this.repository.findById(id);
|
||||||
|
if (!entity) {
|
||||||
|
throw new NotFoundException(`Service type ${id} not found`);
|
||||||
|
}
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new service type.
|
||||||
|
*/
|
||||||
|
async create(dto: CreateServiceTypeDto): Promise<ServiceType> {
|
||||||
|
return this.repository.create({
|
||||||
|
serviceName: dto.serviceName,
|
||||||
|
description: dto.description ?? null,
|
||||||
|
canBeBookedAlone: dto.canBeBookedAlone ?? true,
|
||||||
|
includesFirstMile: dto.includesFirstMile ?? false,
|
||||||
|
includesLastMile: dto.includesLastMile ?? false,
|
||||||
|
includesCustoms: dto.includesCustoms ?? false,
|
||||||
|
priorityBonusPoints: dto.priorityBonusPoints ?? 0,
|
||||||
|
isActive: dto.isActive ?? true,
|
||||||
|
displayOrder: dto.displayOrder ?? 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing service type.
|
||||||
|
*/
|
||||||
|
async update(id: string, dto: UpdateServiceTypeDto): Promise<ServiceType> {
|
||||||
|
await this.findById(id);
|
||||||
|
const updated = await this.repository.update(id, dto);
|
||||||
|
if (!updated) {
|
||||||
|
throw new NotFoundException(`Service type ${id} not found`);
|
||||||
|
}
|
||||||
|
return this.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Soft-delete a service type.
|
||||||
|
*/
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
await this.findById(id);
|
||||||
|
await this.repository.softDelete(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user