diff --git a/apps/edr-freight-api/src/common/utils/generate-code.util.ts b/apps/edr-freight-api/src/common/utils/generate-code.util.ts new file mode 100644 index 000000000..26f978f02 --- /dev/null +++ b/apps/edr-freight-api/src/common/utils/generate-code.util.ts @@ -0,0 +1,15 @@ +/** + * Derives a stable, uppercase, underscore-separated code from a human-readable name. + * + * Examples: + * "Hazard Surcharge" → "HAZARD_SURCHARGE" + * "20ft Dry Container" → "20FT_DRY_CONTAINER" + * "Kality Yard (ET)" → "KALITY_YARD_ET" + */ +export function generateCode(name: string): string { + return name + .trim() + .toUpperCase() + .replace(/[^A-Z0-9]+/g, '_') + .replace(/^_+|_+$/g, ''); +} diff --git a/apps/edr-freight-api/src/migrations/1749000000000-NormalizeWeightLimitTradeDirectionBoth.ts b/apps/edr-freight-api/src/migrations/1749000000000-NormalizeWeightLimitTradeDirectionBoth.ts new file mode 100644 index 000000000..4bded0fab --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1749000000000-NormalizeWeightLimitTradeDirectionBoth.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Align weight_limit_rules.trade_direction with app code: IMPORT, EXPORT, BOTH (not ANY). + */ +export class NormalizeWeightLimitTradeDirectionBoth1749000000000 + implements MigrationInterface +{ + name = 'NormalizeWeightLimitTradeDirectionBoth1749000000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DO $$ BEGIN + UPDATE freight.weight_limit_rules + SET trade_direction = 'BOTH' + WHERE trade_direction::text = 'ANY'; + EXCEPTION WHEN undefined_table OR undefined_column THEN NULL; + END $$; + `); + } + + public async down(_queryRunner: QueryRunner): Promise { + // No-op: ANY is not a valid enum value in PostgreSQL. + } +} diff --git a/apps/edr-freight-api/src/modules/rule-engine/controllers/cargo-types.controller.ts b/apps/edr-freight-api/src/modules/rule-engine/controllers/cargo-types.controller.ts index ed1f661c2..de855c7ef 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/controllers/cargo-types.controller.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/controllers/cargo-types.controller.ts @@ -3,7 +3,7 @@ import { Param, ParseUUIDPipe, Patch, Post, Query, } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; -// import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto'; +import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto'; import { UpdateCargoTypeDto } from '../dto/update-cargo-type.dto'; import { CargoTypesService } from '../services/cargo-types.service'; @@ -39,8 +39,7 @@ export class CargoTypesController { @Post() @ApiOperation({ summary: 'Create a cargo type' }) - create(@Body() dto: any) { - return dto; + create(@Body() dto: CreateCargoTypeDto) { return this.service.create(dto); } diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-cargo-type.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-cargo-type.dto.ts index 53a007b80..ae2e23c33 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-cargo-type.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-cargo-type.dto.ts @@ -2,11 +2,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator'; export class CreateCargoTypeDto { - @ApiProperty({ description: 'Machine-readable code, e.g. BULK, BREAK_BULK', maxLength: 50 }) - @IsString() - @MaxLength(50) - code!: string; - @ApiProperty({ description: 'Cargo type display name', maxLength: 255 }) @IsString() @MaxLength(255) diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-container-type.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-container-type.dto.ts index 08f90c607..dbfb5ca2b 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-container-type.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-container-type.dto.ts @@ -3,11 +3,6 @@ import { Transform } from 'class-transformer'; import { IsBoolean, IsInt, IsNumber, IsOptional, IsString, Max, MaxLength, Min } from 'class-validator'; export class CreateContainerTypeDto { - @ApiProperty({ description: 'Unique container code, e.g. 20DV, 40HC', maxLength: 20 }) - @IsString() - @MaxLength(20) - code!: string; - @ApiProperty({ description: 'Customer-facing label, e.g. "20ft Dry Container"', maxLength: 100 }) @IsString() @MaxLength(100) diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-rule.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-rule.dto.ts index f56cf3e47..16b01fc81 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-rule.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-priority-rule.dto.ts @@ -2,11 +2,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator'; export class CreatePriorityRuleDto { - @ApiProperty({ description: 'Unique rule code, e.g. USD_PAYER, GOV_REQUEST', maxLength: 40 }) - @IsString() - @MaxLength(40) - code!: string; - @ApiProperty({ description: 'Human-readable label', maxLength: 100 }) @IsString() @MaxLength(100) diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-rate.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-rate.dto.ts index a6ded833e..fe9084395 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-rate.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-rate.dto.ts @@ -3,7 +3,7 @@ import { Transform } from 'class-transformer'; import { IsDateString, IsIn, IsNumber, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator'; import { RATE_TYPES, RATE_UNITS } from '../entities/rate.entity'; -const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'ANY'] as const; +const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'BOTH'] as const; const CURRENCIES = ['ETB', 'USD'] as const; export class CreateRateDto { diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-service-type.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-service-type.dto.ts index 6fe8a3227..b20203e13 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-service-type.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-service-type.dto.ts @@ -2,11 +2,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator'; export class CreateServiceTypeDto { - @ApiProperty({ description: 'Machine-readable code, e.g. RAIL_ONLY', maxLength: 50 }) - @IsString() - @MaxLength(50) - code!: string; - @ApiProperty({ description: 'Service type display name', maxLength: 255 }) @IsString() @MaxLength(255) diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-surcharge-type.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-surcharge-type.dto.ts index 39ceb51c4..7aef9bbde 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-surcharge-type.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-surcharge-type.dto.ts @@ -10,11 +10,6 @@ const TRIGGER_CONDITIONS = [ ] as const; export class CreateSurchargeTypeDto { - @ApiProperty({ description: 'Unique code, e.g. HAZARD, REEFER, OVERWEIGHT', maxLength: 40 }) - @IsString() - @MaxLength(40) - code!: string; - @ApiProperty({ description: 'Human-readable label', maxLength: 100 }) @IsString() @MaxLength(100) diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-weight-limit-rule.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-weight-limit-rule.dto.ts index f7e395f64..87cd8ccdf 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-weight-limit-rule.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-weight-limit-rule.dto.ts @@ -2,14 +2,14 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsDateString, IsIn, IsNumber, IsOptional, IsUUID, Min } from 'class-validator'; -const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'ANY'] as const; +const TRADE_DIRECTIONS = ['IMPORT', 'EXPORT', 'BOTH'] as const; export class CreateWeightLimitRuleDto { @ApiProperty({ description: 'FK to container_types.id' }) @IsUUID() containerTypeId!: string; - @ApiProperty({ enum: TRADE_DIRECTIONS, description: 'Trade direction: IMPORT, EXPORT, or ANY' }) + @ApiProperty({ enum: TRADE_DIRECTIONS, description: 'Trade direction: IMPORT, EXPORT, or BOTH' }) @IsIn([...TRADE_DIRECTIONS]) tradeDirection!: string; diff --git a/apps/edr-freight-api/src/modules/rule-engine/dto/create-yard.dto.ts b/apps/edr-freight-api/src/modules/rule-engine/dto/create-yard.dto.ts index 1bb39f12a..f0d9ff012 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/dto/create-yard.dto.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/dto/create-yard.dto.ts @@ -2,11 +2,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { IsBoolean, IsInt, IsOptional, IsString, MaxLength, Min } from 'class-validator'; export class CreateYardDto { - @ApiProperty({ description: 'Unique yard code, e.g. KALITY, DJIB_PORT', maxLength: 20 }) - @IsString() - @MaxLength(20) - code!: string; - @ApiProperty({ description: 'Customer-facing yard label', maxLength: 100 }) @IsString() @MaxLength(100) diff --git a/apps/edr-freight-api/src/modules/rule-engine/repositories/weight-limit-rules.repository.ts b/apps/edr-freight-api/src/modules/rule-engine/repositories/weight-limit-rules.repository.ts index e236d5918..0d151c561 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/repositories/weight-limit-rules.repository.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/repositories/weight-limit-rules.repository.ts @@ -27,9 +27,9 @@ export class WeightLimitRulesRepository implements IWeightLimitRulesRepository { .createQueryBuilder('rule') .innerJoinAndSelect('rule.containerType', 'ct') .where('rule.container_type_id = :containerTypeId', { containerTypeId }) - .andWhere('(rule.trade_direction = :dir OR rule.trade_direction = :any)', { + .andWhere('(rule.trade_direction = :dir OR rule.trade_direction = :both)', { dir: tradeDirection, - any: 'ANY', + both: 'BOTH', }) .andWhere('rule.effective_from <= :now', { now }) .andWhere('(rule.effective_to IS NULL OR rule.effective_to > :now)', { now }) diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/cargo-types.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/cargo-types.service.ts index 3ac0f3e67..130b1d605 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/cargo-types.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/cargo-types.service.ts @@ -1,5 +1,6 @@ import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { ILike } from 'typeorm'; +import { generateCode } from '../../../common/utils/generate-code.util'; import { CreateCargoTypeDto } from '../dto/create-cargo-type.dto'; import { UpdateCargoTypeDto } from '../dto/update-cargo-type.dto'; import { CargoType } from '../entities/cargo-type.entity'; @@ -58,14 +59,15 @@ export class CargoTypesService { /** Create a new cargo type. */ async create(dto: CreateCargoTypeDto): Promise { - const existing = await this.repository.findByCode(dto.code); - if (existing) throw new ConflictException(`Cargo type with code "${dto.code}" already exists`); + const code = generateCode(dto.cargoTypeName); + const existing = await this.repository.findByCode(code); + if (existing) throw new ConflictException(`Cargo type with name "${dto.cargoTypeName}" conflicts with existing code "${code}"`); 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({ - code: dto.code, + code, cargoTypeName: dto.cargoTypeName, parentGroupId: dto.parentGroupId ?? null, showFreeTextBox: dto.showFreeTextBox ?? false, @@ -78,12 +80,6 @@ export class CargoTypesService { /** Update an existing cargo type. */ async update(id: string, dto: UpdateCargoTypeDto): Promise { await this.findById(id); - if (dto.code) { - const conflict = await this.repository.findByCode(dto.code); - if (conflict && conflict.id !== id) { - throw new ConflictException(`Cargo type with code "${dto.code}" already exists`); - } - } if (dto.parentGroupId) { if (dto.parentGroupId === id) throw new ConflictException('A cargo type cannot be its own parent'); const parent = await this.repository.findById(dto.parentGroupId); diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/container-types.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/container-types.service.ts index cbc4fbe86..9b0209311 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/container-types.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/container-types.service.ts @@ -1,4 +1,5 @@ import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { generateCode } from '../../../common/utils/generate-code.util'; import { CreateContainerTypeDto } from '../dto/create-container-type.dto'; import { UpdateContainerTypeDto } from '../dto/update-container-type.dto'; import { ContainerType } from '../entities/container-type.entity'; @@ -27,7 +28,7 @@ export class ContainerTypesService { const [data, total] = await this.repository.findAndCount({ where, - order: { code: 'ASC' }, + order: { displayOrder: 'ASC' }, skip: (page - 1) * pageSize, take: pageSize, }); @@ -43,10 +44,11 @@ export class ContainerTypesService { /** Create a new container type. */ async create(dto: CreateContainerTypeDto): Promise { - const existing = await this.repository.findByCode(dto.code); - if (existing) throw new ConflictException(`Container type with code "${dto.code}" already exists`); + const code = generateCode(dto.label); + const existing = await this.repository.findByCode(code); + if (existing) throw new ConflictException(`Container type with label "${dto.label}" conflicts with existing code "${code}"`); return this.repository.create({ - code: dto.code, + code, label: dto.label, sizeFt: dto.sizeFt, wagonsPerUnit: dto.wagonsPerUnit, @@ -60,12 +62,6 @@ export class ContainerTypesService { /** Update an existing container type. */ async update(id: string, dto: UpdateContainerTypeDto): Promise { await this.findById(id); - if (dto.code) { - const conflict = await this.repository.findByCode(dto.code); - if (conflict && conflict.id !== id) { - throw new ConflictException(`Container type with code "${dto.code}" already exists`); - } - } const updated = await this.repository.update(id, dto); if (!updated) throw new NotFoundException(`Container type ${id} not found`); return updated; diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/priority-rules.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/priority-rules.service.ts index 63ff03a8c..07e282aba 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/priority-rules.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/priority-rules.service.ts @@ -1,4 +1,5 @@ import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { generateCode } from '../../../common/utils/generate-code.util'; import { CreatePriorityRuleDto } from '../dto/create-priority-rule.dto'; import { UpdatePriorityRuleDto } from '../dto/update-priority-rule.dto'; import { PriorityRule } from '../entities/priority-rule.entity'; @@ -27,7 +28,7 @@ export class PriorityRulesService { const [data, total] = await this.repository.findAndCount({ where, - order: { code: 'ASC' }, + order: { label: 'ASC' }, skip: (page - 1) * pageSize, take: pageSize, }); @@ -43,12 +44,13 @@ export class PriorityRulesService { /** Create a new priority rule. */ async create(dto: CreatePriorityRuleDto): Promise { - const existing = await this.repository.findAll({ where: { code: dto.code } }); + const code = generateCode(dto.label); + const existing = await this.repository.findAll({ where: { code } }); if (existing.length > 0) { - throw new ConflictException(`Priority rule with code "${dto.code}" already exists`); + throw new ConflictException(`Priority rule with label "${dto.label}" conflicts with existing code "${code}"`); } return this.repository.create({ - code: dto.code, + code, label: dto.label, score: dto.score, conditionCurrency: dto.conditionCurrency ?? null, @@ -59,7 +61,8 @@ export class PriorityRulesService { /** Update an existing priority rule. */ async update(id: string, dto: UpdatePriorityRuleDto): Promise { await this.findById(id); - const updated = await this.repository.update(id, dto); + const { ...patch } = dto; + const updated = await this.repository.update(id, patch); if (!updated) throw new NotFoundException(`Priority rule ${id} not found`); return updated; } diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/service-types.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/service-types.service.ts index 011cf0e95..1d54582a1 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/service-types.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/service-types.service.ts @@ -1,5 +1,6 @@ import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { ILike } from 'typeorm'; +import { generateCode } from '../../../common/utils/generate-code.util'; import { CreateServiceTypeDto } from '../dto/create-service-type.dto'; import { UpdateServiceTypeDto } from '../dto/update-service-type.dto'; import { ServiceType } from '../entities/service-type.entity'; @@ -55,10 +56,11 @@ export class ServiceTypesService { /** Create a new service type. */ async create(dto: CreateServiceTypeDto): Promise { - const existing = await this.repository.findByCode(dto.code); - if (existing) throw new ConflictException(`Service type with code "${dto.code}" already exists`); + const code = generateCode(dto.serviceName); + const existing = await this.repository.findByCode(code); + if (existing) throw new ConflictException(`Service type with name "${dto.serviceName}" conflicts with existing code "${code}"`); return this.repository.create({ - code: dto.code, + code, serviceName: dto.serviceName, description: dto.description ?? null, canBeBookedAlone: dto.canBeBookedAlone ?? true, @@ -74,13 +76,8 @@ export class ServiceTypesService { /** Update an existing service type. */ async update(id: string, dto: UpdateServiceTypeDto): Promise { await this.findById(id); - if (dto.code) { - const conflict = await this.repository.findByCode(dto.code); - if (conflict && conflict.id !== id) { - throw new ConflictException(`Service type with code "${dto.code}" already exists`); - } - } - const updated = await this.repository.update(id, dto); + const { ...patch } = dto; + const updated = await this.repository.update(id, patch); if (!updated) throw new NotFoundException(`Service type ${id} not found`); return updated; } diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/surcharge-types.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/surcharge-types.service.ts index 0160f3651..98e5b1642 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/surcharge-types.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/surcharge-types.service.ts @@ -1,4 +1,5 @@ import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { generateCode } from '../../../common/utils/generate-code.util'; import { CreateSurchargeTypeDto } from '../dto/create-surcharge-type.dto'; import { UpdateSurchargeTypeDto } from '../dto/update-surcharge-type.dto'; import { SurchargeType } from '../entities/surcharge-type.entity'; @@ -43,10 +44,11 @@ export class SurchargeTypesService { /** Create a new surcharge type. */ async create(dto: CreateSurchargeTypeDto): Promise { - const existing = await this.repository.findByCode(dto.code); - if (existing) throw new ConflictException(`Surcharge type with code "${dto.code}" already exists`); + const code = generateCode(dto.label); + const existing = await this.repository.findByCode(code); + if (existing) throw new ConflictException(`Surcharge type with label "${dto.label}" conflicts with existing code "${code}"`); return this.repository.create({ - code: dto.code, + code, label: dto.label, triggerCondition: dto.triggerCondition as SurchargeType['triggerCondition'], rateId: dto.rateId, @@ -57,14 +59,7 @@ export class SurchargeTypesService { /** Update an existing surcharge type. */ async update(id: string, dto: UpdateSurchargeTypeDto): Promise { await this.findById(id); - if (dto.code) { - const conflict = await this.repository.findByCode(dto.code); - if (conflict && conflict.id !== id) { - throw new ConflictException(`Surcharge type with code "${dto.code}" already exists`); - } - } const patch: Partial = {}; - if (dto.code !== undefined) patch.code = dto.code; if (dto.label !== undefined) patch.label = dto.label; if (dto.triggerCondition !== undefined) patch.triggerCondition = dto.triggerCondition as SurchargeType['triggerCondition']; if (dto.rateId !== undefined) patch.rateId = dto.rateId; diff --git a/apps/edr-freight-api/src/modules/rule-engine/services/yards.service.ts b/apps/edr-freight-api/src/modules/rule-engine/services/yards.service.ts index d2977df2a..5e53cb1fd 100644 --- a/apps/edr-freight-api/src/modules/rule-engine/services/yards.service.ts +++ b/apps/edr-freight-api/src/modules/rule-engine/services/yards.service.ts @@ -1,4 +1,5 @@ import { ConflictException, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { generateCode } from '../../../common/utils/generate-code.util'; import { CreateYardDto } from '../dto/create-yard.dto'; import { UpdateYardDto } from '../dto/update-yard.dto'; import { Yard } from '../entities/yard.entity'; @@ -26,7 +27,7 @@ export class YardsService { const [data, total] = await this.repository.findAndCount({ where, - order: { displayOrder: 'ASC', code: 'ASC' }, + order: { displayOrder: 'ASC', label: 'ASC' }, skip: (page - 1) * pageSize, take: pageSize, }); @@ -42,10 +43,11 @@ export class YardsService { /** Create a yard. */ async create(dto: CreateYardDto): Promise { - const existing = await this.repository.findByCode(dto.code); - if (existing) throw new ConflictException(`Yard with code "${dto.code}" already exists`); + const code = generateCode(dto.label); + const existing = await this.repository.findByCode(code); + if (existing) throw new ConflictException(`Yard with label "${dto.label}" conflicts with existing code "${code}"`); return this.repository.create({ - code: dto.code, + code, label: dto.label, country: dto.country, isActive: dto.isActive ?? true, @@ -56,12 +58,6 @@ export class YardsService { /** Update a yard. */ async update(id: string, dto: UpdateYardDto): Promise { await this.findById(id); - if (dto.code) { - const conflict = await this.repository.findByCode(dto.code); - if (conflict && conflict.id !== id) { - throw new ConflictException(`Yard with code "${dto.code}" already exists`); - } - } const updated = await this.repository.update(id, dto); if (!updated) throw new NotFoundException(`Yard ${id} not found`); return updated; diff --git a/apps/edr-freight-web/backoffice/index.css b/apps/edr-freight-web/backoffice/index.css index 4909e33bd..d232351ed 100644 --- a/apps/edr-freight-web/backoffice/index.css +++ b/apps/edr-freight-web/backoffice/index.css @@ -1,2 +1,9 @@ @import "tailwindcss"; @import "@edr/ui-common/theme.css" layer(theme); + +html, +body, +#root { + height: 100%; + overflow: hidden; +} diff --git a/apps/edr-freight-web/backoffice/public/assets/login.png b/apps/edr-freight-web/backoffice/public/assets/login.png index b5c1ff21b..f4854a45c 100644 Binary files a/apps/edr-freight-web/backoffice/public/assets/login.png and b/apps/edr-freight-web/backoffice/public/assets/login.png differ diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 75a233955..99e51457c 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -1,7 +1,7 @@ import { Navigate, Outlet, Route, Routes, useLocation, useNavigate } from "react-router-dom"; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { DashboardLayout, type SidebarItem } from "@edr/ui-common"; -import { LayoutDashboard, Network, Paperclip, Settings } from "lucide-react"; +import { FreightDashboardLayout, type SidebarSection } from "@/components/layout"; +import { Boxes, LayoutDashboard, Network, Paperclip, Settings, SlidersHorizontal } from "lucide-react"; import { useAuth } from "./auth/useAuth"; import LoginPage from "./pages/auth/LoginPage"; import OverviewPage from "./pages/dashboard/OverviewPage"; @@ -14,7 +14,9 @@ import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage"; import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage"; import DemoUser1Page from "./pages/dashboard/demo/DemoUser1Page"; import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page"; -import { RuleEnginePage } from "./pages/ruleEngine/RuleEngine"; +import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirect"; +import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage"; +import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources"; // Create a QueryClient instance const queryClient = new QueryClient({ @@ -59,46 +61,75 @@ const queryClient = new QueryClient({ // }, // ]; -const sidebarItems: SidebarItem[] = [ +const sidebarSections: SidebarSection[] = [ { - label: "Overview", - href: "/dashboard/overview", - icon: , - }, - { - label: "User management", - href: "/dashboard/user-management", - icon: , - children: [ + title: "Main menu", + mutedTitle: true, + items: [ { - label: "Employees", - href: "/dashboard/user-management/employees", - }, - { - label: "Permissions", - href: "/dashboard/user-management/permissions", - }, - { - label: "Roles", - href: "/dashboard/user-management/roles", + label: "Overview", + href: "/dashboard/overview", + icon: , }, ], }, { - label: "File Settings", - href: "/dashboard/file-settings", - icon: , + title: "Administration", + items: [ + { + label: "User management", + href: "/dashboard/user-management", + icon: , + children: [ + { + label: "Employees", + href: "/dashboard/user-management/employees", + }, + { + label: "Permissions", + href: "/dashboard/user-management/permissions", + }, + { + label: "Roles", + href: "/dashboard/user-management/roles", + }, + ], + }, + { + label: "File settings", + href: "/dashboard/file-settings", + icon: , + }, + { + label: "Dropdown settings", + href: "/dashboard/dropdown-settings", + icon: , + }, + ], }, { - label: "Dropdown Settings", - href: "/dashboard/dropdown-settings", - icon: , + title: "Freight configuration", + mutedTitle: true, + items: [ + { + label: "Configuration", + href: "/dashboard/configuration", + icon: , + children: getCategorySidebarChildren("configuration"), + }, + ], }, { - label: "Rule Engine", - href: "/dashboard/rule-engine", - icon: , - } + title: "Rules & pricing", + items: [ + { + label: "Rules", + href: "/dashboard/rules", + icon: , + children: getCategorySidebarChildren("rules"), + }, + ], + }, ]; const hasPermission = ( @@ -120,9 +151,8 @@ const DashboardShell = () => { const displayName = user?.name?.en || user?.username || user?.email || "User"; return ( - { onLogout={logout} > - + ); }; @@ -161,7 +191,21 @@ const App = () => { }> } /> } /> - } /> + } + /> + } /> + } + /> + } /> + } + /> + } /> } /> } /> } /> diff --git a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx new file mode 100644 index 000000000..48f13f4ff --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx @@ -0,0 +1,173 @@ +import { type ReactNode, useEffect, useRef, useState } from "react"; +import { + Bell, + ChevronDown, + Languages, + LogOut, + MessageSquare, + Moon, + Sun, + User, +} from "lucide-react"; + +import { cn } from "@/lib/utils"; + +import type { PageMeta } from "./types"; + +const iconButtonClass = + "relative inline-flex h-10 w-10 items-center justify-center rounded-xl border border-gray-200 bg-white text-gray-600 shadow-sm transition hover:border-primary/30 hover:bg-gray-50 hover:text-gray-900"; + +export interface FreightDashboardHeaderProps { + pageMeta: PageMeta; + headerRight?: ReactNode; + enableThemeToggle?: boolean; + userName?: string; + userEmail?: string; + userInitials?: string; + onLogout?: () => void; + theme: "light" | "dark"; + onToggleTheme: () => void; +} + +const FreightDashboardHeader = ({ + pageMeta, + headerRight, + enableThemeToggle = false, + userName = "User", + userEmail, + userInitials, + onLogout, + theme, + onToggleTheme, +}: FreightDashboardHeaderProps) => { + const initials = + userInitials ?? + userName + .split(" ") + .filter(Boolean) + .slice(0, 2) + .map((n) => n[0].toUpperCase()) + .join(""); + + const [isUserMenuOpen, setIsUserMenuOpen] = useState(false); + const userMenuRef = useRef(null); + + useEffect(() => { + if (!isUserMenuOpen) return; + + const handlePointerDown = (event: MouseEvent) => { + if (userMenuRef.current && !userMenuRef.current.contains(event.target as Node)) { + setIsUserMenuOpen(false); + } + }; + + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === "Escape") setIsUserMenuOpen(false); + }; + + document.addEventListener("mousedown", handlePointerDown); + document.addEventListener("keydown", handleKeyDown); + return () => { + document.removeEventListener("mousedown", handlePointerDown); + document.removeEventListener("keydown", handleKeyDown); + }; + }, [isUserMenuOpen]); + + return ( +
+
+

{pageMeta.title}

+

{pageMeta.subtitle}

+
+ +
+ {enableThemeToggle ? ( + + ) : null} + + + + + + + +
+ + + {isUserMenuOpen ? ( +
+
+

{userName}

+ {userEmail ?

{userEmail}

: null} +
+ setIsUserMenuOpen(false)} + className="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 transition hover:bg-gray-50" + > + + Profile + + +
+ ) : null} +
+ + {headerRight} +
+
+ ); +}; + +export default FreightDashboardHeader; diff --git a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardLayout.tsx b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardLayout.tsx new file mode 100644 index 000000000..657b56ea7 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardLayout.tsx @@ -0,0 +1,111 @@ +import { type ReactNode, useEffect, useState } from "react"; + +import FreightDashboardHeader from "./FreightDashboardHeader"; +import FreightSidebar from "./FreightSidebar"; +import { getPageMeta } from "./route-meta"; +import type { SidebarSection } from "./types"; + +type Theme = "light" | "dark"; +const THEME_STORAGE_KEY = "edr-theme"; + +function getInitialTheme(): Theme { + if (typeof window === "undefined") return "light"; + const stored = window.localStorage.getItem(THEME_STORAGE_KEY); + if (stored === "dark" || stored === "light") return stored; + return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light"; +} + +export interface FreightDashboardLayoutProps { + sidebarSections: SidebarSection[]; + activeHref?: string; + onNavigate?: (href: string) => void; + headerRight?: ReactNode; + enableThemeToggle?: boolean; + userName?: string; + userEmail?: string; + userInitials?: string; + onLogout?: () => void; + children: ReactNode; +} + +const panelClass = + "rounded-2xl border border-gray-200/80 bg-white shadow-[0_1px_3px_rgba(15,23,42,0.06)]"; + +const FreightDashboardLayout = ({ + sidebarSections, + activeHref = "", + onNavigate, + headerRight, + enableThemeToggle = false, + userName, + userEmail, + userInitials, + onLogout, + children, +}: FreightDashboardLayoutProps) => { + const pageMeta = getPageMeta(activeHref); + const [theme, setTheme] = useState(() => + enableThemeToggle ? getInitialTheme() : "light", + ); + + useEffect(() => { + if (!enableThemeToggle) return; + const root = document.documentElement; + if (theme === "dark") { + root.classList.add("dark"); + } else { + root.classList.remove("dark"); + } + window.localStorage.setItem(THEME_STORAGE_KEY, theme); + }, [theme, enableThemeToggle]); + + const toggleTheme = () => setTheme((current) => (current === "dark" ? "light" : "dark")); + + return ( + <> + + + + +
+
+ + +
+
+ +
+ +
+ {children} +
+
+
+
+ + ); +}; + +export default FreightDashboardLayout; diff --git a/apps/edr-freight-web/backoffice/src/components/layout/FreightSidebar.tsx b/apps/edr-freight-web/backoffice/src/components/layout/FreightSidebar.tsx new file mode 100644 index 000000000..9439983f2 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/layout/FreightSidebar.tsx @@ -0,0 +1,285 @@ +import { type MouseEvent, useCallback, useEffect, useMemo, useState } from "react"; +import { ChevronDown, ChevronRight } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +import type { SidebarItem, SidebarSection } from "./types"; + +const EDR_LOGO = "/assets/logo.svg"; + +export interface FreightSidebarProps { + sections: SidebarSection[]; + activeHref?: string; + onNavigate?: (href: string) => void; +} + +const sidebarItemKey = (item: SidebarItem, parentKey: string) => + item.href ?? `${parentKey}::${item.label}`; + +const collectSidebarHrefs = (items: SidebarItem[]): string[] => + items.flatMap((item) => { + const hrefs: string[] = []; + if (item.href) hrefs.push(item.href.toLowerCase()); + if (item.children?.length) hrefs.push(...collectSidebarHrefs(item.children)); + return hrefs; + }); + +const flattenSectionItems = (sections: SidebarSection[]) => + sections.flatMap((section) => section.items); + +const FreightSidebar = ({ sections, activeHref, onNavigate }: FreightSidebarProps) => { + const items = useMemo(() => flattenSectionItems(sections), [sections]); + const activePath = activeHref?.toLowerCase() ?? ""; + + const isHrefActive = useCallback( + (href: string) => { + const normalized = href.toLowerCase(); + return activePath === normalized || activePath.startsWith(`${normalized}/`); + }, + [activePath], + ); + + const branchContainsActive = useCallback( + (branch: SidebarItem[]) => + collectSidebarHrefs(branch).some((href) => isHrefActive(href)), + [isHrefActive], + ); + + const defaultExpanded = useMemo(() => { + const acc: Record = {}; + + const walk = (entries: SidebarItem[], parentKey: string) => { + for (const entry of entries) { + if (!entry.children?.length) continue; + const key = sidebarItemKey(entry, parentKey); + acc[key] = + branchContainsActive(entry.children) || + (entry.href ? isHrefActive(entry.href) : false); + walk(entry.children, key); + } + }; + + for (const item of items) { + if (!item.children?.length) continue; + const key = item.href ?? item.label; + acc[key] = + activePath === key.toLowerCase() || + activePath.startsWith(`${key.toLowerCase()}/`) || + branchContainsActive(item.children); + walk(item.children, key); + } + + return acc; + }, [activePath, branchContainsActive, isHrefActive, items]); + + const [expanded, setExpanded] = useState>(defaultExpanded); + + useEffect(() => { + setExpanded((current) => ({ ...defaultExpanded, ...current })); + }, [defaultExpanded]); + + const navigateTo = (event: MouseEvent, href: string) => { + if (onNavigate) { + event.preventDefault(); + onNavigate(href); + } + }; + + const toggleExpanded = (key: string) => { + setExpanded((current) => ({ ...current, [key]: !current[key] })); + }; + + const navLinkClass = (active: boolean, depth: number) => + cn( + "flex items-center justify-between rounded-md px-3 py-2.5 text-base font-medium leading-snug transition-colors", + active + ? "bg-primary text-primary-foreground shadow-sm" + : "text-gray-900 hover:bg-gray-100", + depth > 0 && "text-[15px]", + ); + + const iconClass = (active: boolean, sectionActive: boolean) => + cn( + "flex h-5 w-5 shrink-0 items-center justify-center [&_svg]:h-5 [&_svg]:w-5", + active + ? "text-primary-foreground" + : sectionActive + ? "text-gray-900" + : "text-gray-900", + ); + + const renderNavBranch = (children: SidebarItem[], depth: number, parentKey: string) => + children.map((child) => { + const key = sidebarItemKey(child, parentKey); + const isGroup = Boolean(child.children?.length) && !child.href; + + if (isGroup) { + const isOpen = expanded[key] ?? false; + const groupActive = branchContainsActive(child.children!); + + return ( +
+ + {isOpen ? ( +
+ {renderNavBranch(child.children!, depth + 1, key)} +
+ ) : null} +
+ ); + } + + if (!child.href) return null; + + const childHref = child.href.toLowerCase(); + const childActiveHref = isHrefActive(childHref); + + return ( + navigateTo(event, child.href!)} + aria-current={childActiveHref ? "page" : undefined} + className={navLinkClass(childActiveHref, depth)} + > + {child.label} + + + ); + }); + + const renderTopLevelItem = (item: SidebarItem) => { + if (!item.href) return null; + + const hasChildren = Boolean(item.children?.length); + const itemHref = item.href.toLowerCase(); + const childActive = hasChildren ? branchContainsActive(item.children!) : false; + const isCurrentItem = hasChildren + ? activePath === itemHref + : isHrefActive(itemHref); + const isSectionActive = childActive && !isCurrentItem; + const isActive = isCurrentItem || isSectionActive; + const isOpen = expanded[item.href] ?? false; + const leafActive = isCurrentItem && !hasChildren; + + return ( +
+
+ navigateTo(event, item.href!)} + aria-current={isCurrentItem ? "page" : undefined} + className="flex min-w-0 flex-1 items-center gap-3 px-3 py-2.5 text-base font-medium leading-snug" + > + {item.icon ? ( + {item.icon} + ) : null} + {item.label} + + + {hasChildren ? ( + + ) : ( + + + + )} +
+ + {hasChildren && isOpen ? ( +
+ {renderNavBranch(item.children!, 0, item.href)} +
+ ) : null} +
+ ); + }; + + return ( + + ); +}; + +export default FreightSidebar; diff --git a/apps/edr-freight-web/backoffice/src/components/layout/index.ts b/apps/edr-freight-web/backoffice/src/components/layout/index.ts new file mode 100644 index 000000000..7a02cfbaf --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/layout/index.ts @@ -0,0 +1,6 @@ +export { default as FreightDashboardLayout } from "./FreightDashboardLayout"; +export type { FreightDashboardLayoutProps } from "./FreightDashboardLayout"; +export { default as FreightSidebar } from "./FreightSidebar"; +export { default as FreightDashboardHeader } from "./FreightDashboardHeader"; +export { getPageMeta } from "./route-meta"; +export type { SidebarItem, SidebarSection, PageMeta } from "./types"; diff --git a/apps/edr-freight-web/backoffice/src/components/layout/route-meta.ts b/apps/edr-freight-web/backoffice/src/components/layout/route-meta.ts new file mode 100644 index 000000000..517ca6468 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/layout/route-meta.ts @@ -0,0 +1,125 @@ +import type { PageMeta } from "./types"; +import { + RULE_ENGINE_CATEGORY_BASE_PATH, + RULE_ENGINE_RESOURCES, +} from "@/pages/ruleEngine/config/resources"; + +const APP_TITLE = "EDR Freight Backoffice"; +const APP_SUBTITLE = "Manage freight operations and platform settings"; + +const configurationRouteMeta = RULE_ENGINE_RESOURCES.filter( + (r) => r.category === "configuration", +).map((resource) => ({ + prefix: `${RULE_ENGINE_CATEGORY_BASE_PATH.configuration}/${resource.slug}`, + meta: { + title: resource.label, + subtitle: resource.subtitle, + }, +})); + +const rulesRouteMeta = RULE_ENGINE_RESOURCES.filter((r) => r.category === "rules").map( + (resource) => ({ + prefix: `${RULE_ENGINE_CATEGORY_BASE_PATH.rules}/${resource.slug}`, + meta: { + title: resource.label, + subtitle: resource.subtitle, + }, + }), +); + +const ROUTE_META: Array<{ prefix: string; meta: PageMeta }> = [ + { + prefix: "/dashboard/overview", + meta: { + title: "Overview", + subtitle: "Dashboard summary and key metrics", + }, + }, + { + prefix: "/dashboard/user-management/employees", + meta: { + title: "Employees", + subtitle: "Manage employee accounts and assignments", + }, + }, + { + prefix: "/dashboard/user-management/permissions", + meta: { + title: "Permissions", + subtitle: "Configure access permissions for roles and users", + }, + }, + { + prefix: "/dashboard/user-management/roles", + meta: { + title: "Roles", + subtitle: "Manage roles and their permission sets", + }, + }, + { + prefix: "/dashboard/user-management", + meta: { + title: "User management", + subtitle: "Organization structure, employees, roles, and permissions", + }, + }, + { + prefix: "/dashboard/file-settings", + meta: { + title: "File Settings", + subtitle: "Configure file upload rules and document fields", + }, + }, + { + prefix: "/dashboard/dropdown-settings", + meta: { + title: "Dropdown Settings", + subtitle: "Manage dropdown options used across the platform", + }, + }, + { + prefix: RULE_ENGINE_CATEGORY_BASE_PATH.configuration, + meta: { + title: "Configuration", + subtitle: "Master data: cargo, containers, services, surcharges, yards, and shipping lines", + }, + }, + ...configurationRouteMeta, + { + prefix: RULE_ENGINE_CATEGORY_BASE_PATH.rules, + meta: { + title: "Rules", + subtitle: "Priority, weight limits, rates, and approval workflows", + }, + }, + ...rulesRouteMeta, + { + prefix: "/dashboard/user1", + meta: { + title: "Demo User 1", + subtitle: "Demo workspace", + }, + }, + { + prefix: "/dashboard/user2", + meta: { + title: "Demo User 2", + subtitle: "Demo workspace", + }, + }, +]; + +export const getPageMeta = (pathname: string): PageMeta => { + const normalized = pathname.toLowerCase(); + const sorted = [...ROUTE_META].sort((a, b) => b.prefix.length - a.prefix.length); + const match = sorted.find(({ prefix }) => normalized.startsWith(prefix.toLowerCase())); + + if (match) { + return match.meta; + } + + return { + title: APP_TITLE, + subtitle: APP_SUBTITLE, + }; +}; diff --git a/apps/edr-freight-web/backoffice/src/components/layout/types.ts b/apps/edr-freight-web/backoffice/src/components/layout/types.ts new file mode 100644 index 000000000..ba37f33e5 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/layout/types.ts @@ -0,0 +1,22 @@ +import type { ReactNode } from "react"; + +export interface SidebarItem { + label: string; + /** Omit for non-navigable group headers (e.g. Rule Engine categories). */ + href?: string; + icon?: ReactNode; + children?: SidebarItem[]; +} + +export interface SidebarSection { + /** Section label shown above a group of nav items (e.g. "Main menu"). */ + title: string; + items: SidebarItem[]; + /** When true, section title uses muted grey instead of dark text. */ + mutedTitle?: boolean; +} + +export interface PageMeta { + title: string; + subtitle: string; +} diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/ContractType copy 2.tsx b/apps/edr-freight-web/backoffice/src/components/ruleEngine/ContractType copy 2.tsx deleted file mode 100644 index d6275d717..000000000 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/ContractType copy 2.tsx +++ /dev/null @@ -1,718 +0,0 @@ -// src/components/ruleEngine/ContractType.tsx -import { useState, useEffect } from 'react'; -import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; - -// ==================== Toast Notification Component ==================== -const Toast = ({ message, type, onClose }: { message: string; type: string; onClose: () => void }) => { - useEffect(() => { - const timer = setTimeout(onClose, 3000); - return () => clearTimeout(timer); - }, [onClose]); - - const bgColor = type === 'success' ? 'bg-green-500' : type === 'error' ? 'bg-red-500' : 'bg-blue-500'; - - return ( -
- {message} -
- ); -}; - -// ==================== API Service ==================== -const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'; - -const apiService = { - // Cargo Types - getCargoTypes: () => fetch(`${API_BASE_URL}/cargo-types`).then(res => res.json()), - createCargoType: (data: any) => fetch(`${API_BASE_URL}/cargo-types`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - updateCargoType: (id: string, data: any) => fetch(`${API_BASE_URL}/cargo-types/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - deleteCargoType: (id: string) => fetch(`${API_BASE_URL}/cargo-types/${id}`, { - method: 'DELETE' - }).then(res => res.json()), - - // Container Types - getContainerTypes: () => fetch(`${API_BASE_URL}/container-types`).then(res => res.json()), - createContainerType: (data: any) => fetch(`${API_BASE_URL}/container-types`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - updateContainerType: (id: string, data: any) => fetch(`${API_BASE_URL}/container-types/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - deleteContainerType: (id: string) => fetch(`${API_BASE_URL}/container-types/${id}`, { - method: 'DELETE' - }).then(res => res.json()), - - // Priority Rules - getPriorityRules: () => fetch(`${API_BASE_URL}/priority-rules`).then(res => res.json()), - createPriorityRule: (data: any) => fetch(`${API_BASE_URL}/priority-rules`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - updatePriorityRule: (id: string, data: any) => fetch(`${API_BASE_URL}/priority-rules/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - deletePriorityRule: (id: string) => fetch(`${API_BASE_URL}/priority-rules/${id}`, { - method: 'DELETE' - }).then(res => res.json()), - - // Service Types - getServiceTypes: () => fetch(`${API_BASE_URL}/service-types`).then(res => res.json()), - createServiceType: (data: any) => fetch(`${API_BASE_URL}/service-types`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - updateServiceType: (id: string, data: any) => fetch(`${API_BASE_URL}/service-types/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - deleteServiceType: (id: string) => fetch(`${API_BASE_URL}/service-types/${id}`, { - method: 'DELETE' - }).then(res => res.json()), - - // Surcharge Types - getSurchargeTypes: () => fetch(`${API_BASE_URL}/surcharge-types`).then(res => res.json()), - createSurchargeType: (data: any) => fetch(`${API_BASE_URL}/surcharge-types`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - updateSurchargeType: (id: string, data: any) => fetch(`${API_BASE_URL}/surcharge-types/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - deleteSurchargeType: (id: string) => fetch(`${API_BASE_URL}/surcharge-types/${id}`, { - method: 'DELETE' - }).then(res => res.json()), - - // Surcharges - getSurcharges: () => fetch(`${API_BASE_URL}/surcharges`).then(res => res.json()), - createSurcharge: (data: any) => fetch(`${API_BASE_URL}/surcharges`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - updateSurcharge: (id: string, data: any) => fetch(`${API_BASE_URL}/surcharges/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - deleteSurcharge: (id: string) => fetch(`${API_BASE_URL}/surcharges/${id}`, { - method: 'DELETE' - }).then(res => res.json()), - - // Weight Limit Rules - getWeightLimitRules: () => fetch(`${API_BASE_URL}/weight-limit-rules`).then(res => res.json()), - createWeightLimitRule: (data: any) => fetch(`${API_BASE_URL}/weight-limit-rules`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - updateWeightLimitRule: (id: string, data: any) => fetch(`${API_BASE_URL}/weight-limit-rules/${id}`, { - method: 'PATCH', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data) - }).then(res => res.json()), - deleteWeightLimitRule: (id: string) => fetch(`${API_BASE_URL}/weight-limit-rules/${id}`, { - method: 'DELETE' - }).then(res => res.json()), -}; - -// ==================== Entity Table Component ==================== -const EntityTable = ({ - title, - data, - columns, - onAdd, - onEdit, - onDelete, - isLoading -}: any) => { - const [expanded, setExpanded] = useState(true); - const [searchTerm, setSearchTerm] = useState(''); - - const filteredData = data?.filter((item: any) => - Object.values(item).some(value => - String(value).toLowerCase().includes(searchTerm.toLowerCase()) - ) - ) || []; - - if (isLoading) { - return ( -
-
setExpanded(!expanded)} - > -
- {expanded ? '▼' : '▶'} -

{title}

-
-
- {expanded && ( -
-
-

Loading...

-
- )} -
- ); - } - - return ( -
-
setExpanded(!expanded)} - > -
- - {expanded ? '▼' : '▶'} - -

{title}

- - {filteredData.length} items - -
-
- - {expanded && ( -
-
- -
- setSearchTerm(e.target.value)} - /> - - - -
-
- -
- - - - {columns.map((col: any) => ( - - ))} - - - - - {filteredData.map((item: any) => ( - - {columns.map((col: any) => ( - - ))} - - - ))} - -
- {col.label} - - Actions -
- {col.render ? col.render(item[col.key], item) : item[col.key]} - - - -
- {filteredData.length === 0 && ( -
- - - -

No data found

-
- )} -
-
- )} -
- ); -}; - -// ==================== Main Component ==================== -const ContractTypePage = () => { - const [activeTab, setActiveTab] = useState('cargo-types'); - const [modalOpen, setModalOpen] = useState(false); - const [editingItem, setEditingItem] = useState(null); - const [currentEntity, setCurrentEntity] = useState(''); - const [formData, setFormData] = useState({}); - const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); - const queryClient = useQueryClient(); - - const showToast = (message: string, type: 'success' | 'error') => { - setToast({ message, type }); - }; - - // Fetch all data - const { data: cargoTypes = [], isLoading: cargoLoading } = useQuery({ - queryKey: ['cargo-types'], - queryFn: apiService.getCargoTypes, - }); - - const { data: containerTypes = [], isLoading: containerLoading } = useQuery({ - queryKey: ['container-types'], - queryFn: apiService.getContainerTypes, - }); - - const { data: priorityRules = [], isLoading: priorityLoading } = useQuery({ - queryKey: ['priority-rules'], - queryFn: apiService.getPriorityRules, - }); - - const { data: serviceTypes = [], isLoading: serviceLoading } = useQuery({ - queryKey: ['service-types'], - queryFn: apiService.getServiceTypes, - }); - - const { data: surchargeTypes = [], isLoading: surchargeTypeLoading } = useQuery({ - queryKey: ['surcharge-types'], - queryFn: apiService.getSurchargeTypes, - }); - - const { data: surcharges = [], isLoading: surchargeLoading } = useQuery({ - queryKey: ['surcharges'], - queryFn: apiService.getSurcharges, - }); - - const { data: weightLimitRules = [], isLoading: weightLimitLoading } = useQuery({ - queryKey: ['weight-limit-rules'], - queryFn: apiService.getWeightLimitRules, - }); - - // Mutations for Cargo Types - const createCargoType = useMutation({ - mutationFn: apiService.createCargoType, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['cargo-types'] }); - showToast('Cargo type created successfully', 'success'); - setModalOpen(false); - setFormData({}); - }, - onError: () => showToast('Failed to create cargo type', 'error'), - }); - - const updateCargoType = useMutation({ - mutationFn: ({ id, data }: { id: string; data: any }) => apiService.updateCargoType(id, data), - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['cargo-types'] }); - showToast('Cargo type updated successfully', 'success'); - setModalOpen(false); - setFormData({}); - setEditingItem(null); - }, - onError: () => showToast('Failed to update cargo type', 'error'), - }); - - const deleteCargoType = useMutation({ - mutationFn: apiService.deleteCargoType, - onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ['cargo-types'] }); - showToast('Cargo type deleted successfully', 'success'); - }, - onError: () => showToast('Failed to delete cargo type', 'error'), - }); - - const handleAdd = (entity: string) => { - setCurrentEntity(entity); - setEditingItem(null); - setFormData(getDefaultFormData(entity)); - setModalOpen(true); - }; - - const handleEdit = (entity: string, item: any) => { - setCurrentEntity(entity); - setEditingItem(item); - setFormData(item); - setModalOpen(true); - }; - - const handleDelete = (entity: string, item: any) => { - if (window.confirm(`Are you sure you want to delete this ${entity}?`)) { - if (entity === 'cargo-types') deleteCargoType.mutate(item.id); - } - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (currentEntity === 'cargo-types') { - if (editingItem) { - updateCargoType.mutate({ id: editingItem.id, data: formData }); - } else { - createCargoType.mutate(formData); - } - } - }; - - const getDefaultFormData = (entity: string) => { - switch(entity) { - case 'cargo-types': - return { code: '', cargoTypeName: '', showFreeTextBox: false, requiresDirectorApproval: false, isActive: true, displayOrder: 1 }; - default: - return {}; - } - }; - - const getEntityData = (entity: string) => { - switch(entity) { - case 'cargo-types': return cargoTypes; - case 'container-types': return containerTypes; - case 'priority-rules': return priorityRules; - case 'service-types': return serviceTypes; - case 'surcharge-types': return surchargeTypes; - case 'surcharges': return surcharges; - case 'weight-limit-rules': return weightLimitRules; - default: return []; - } - }; - - const getEntityLoading = (entity: string) => { - switch(entity) { - case 'cargo-types': return cargoLoading; - case 'container-types': return containerLoading; - case 'priority-rules': return priorityLoading; - case 'service-types': return serviceLoading; - case 'surcharge-types': return surchargeTypeLoading; - case 'surcharges': return surchargeLoading; - case 'weight-limit-rules': return weightLimitLoading; - default: return false; - } - }; - - const getColumns = (entity: string) => { - switch(entity) { - case 'cargo-types': - return [ - { key: 'code', label: 'Code' }, - { key: 'cargoTypeName', label: 'Name' }, - { key: 'displayOrder', label: 'Order' }, - { - key: 'isActive', - label: 'Status', - render: (val: boolean) => ( - - {val ? 'Active' : 'Inactive'} - - ) - } - ]; - case 'container-types': - return [ - { key: 'sizeCode', label: 'Size Code' }, - { key: 'description', label: 'Description' }, - { key: 'containersPerWagon', label: 'Containers/Wagon' }, - { - key: 'isActive', - label: 'Status', - render: (val: boolean) => ( - - {val ? 'Active' : 'Inactive'} - - ) - } - ]; - case 'priority-rules': - return [ - { key: 'priorityType', label: 'Priority Type' }, - { key: 'ruleName', label: 'Rule Name' }, - { key: 'bonusPoints', label: 'Bonus Points' }, - { - key: 'isActive', - label: 'Status', - render: (val: boolean) => ( - - {val ? 'Active' : 'Inactive'} - - ) - } - ]; - case 'service-types': - return [ - { key: 'code', label: 'Code' }, - { key: 'serviceName', label: 'Service Name' }, - { key: 'displayOrder', label: 'Order' }, - { - key: 'isActive', - label: 'Status', - render: (val: boolean) => ( - - {val ? 'Active' : 'Inactive'} - - ) - } - ]; - case 'surcharge-types': - return [ - { key: 'code', label: 'Code' }, - { key: 'name', label: 'Name' }, - { - key: 'isActive', - label: 'Status', - render: (val: boolean) => ( - - {val ? 'Active' : 'Inactive'} - - ) - } - ]; - case 'surcharges': - return [ - { key: 'feeName', label: 'Fee Name' }, - { key: 'calculationMethod', label: 'Method' }, - { - key: 'rate', - label: 'Rate', - render: (val: number, item: any) => `${val} ${item.currency}` - }, - { - key: 'isActive', - label: 'Status', - render: (val: boolean) => ( - - {val ? 'Active' : 'Inactive'} - - ) - } - ]; - case 'weight-limit-rules': - return [ - { key: 'tradeDirection', label: 'Direction' }, - { - key: 'maxWeightTons', - label: 'Max Weight', - render: (val: number) => `${val} tons` - }, - { key: 'exceededAction', label: 'Action' }, - { - key: 'isActive', - label: 'Status', - render: (val: boolean) => ( - - {val ? 'Active' : 'Inactive'} - - ) - } - ]; - default: - return []; - } - }; - - const tabs = [ - { id: 'cargo-types', label: 'Cargo Types' }, - { id: 'container-types', label: 'Container Types' }, - { id: 'priority-rules', label: 'Priority Rules' }, - { id: 'service-types', label: 'Service Types' }, - { id: 'surcharge-types', label: 'Surcharge Types' }, - { id: 'surcharges', label: 'Surcharges' }, - { id: 'weight-limit-rules', label: 'Weight Limit Rules' }, - ]; - - const isLoading = cargoLoading || containerLoading || priorityLoading || serviceLoading || surchargeTypeLoading || surchargeLoading || weightLimitLoading; - - if (isLoading) { - return ( -
-
-
-

Loading master data...

-
-
- ); - } - - return ( -
- {toast && ( - setToast(null)} - /> - )} - -
-
- {tabs.map((tab) => ( - - ))} -
-
- -
- {tabs.map((tab) => ( -
- handleAdd(tab.id)} - onEdit={(item: any) => handleEdit(tab.id, item)} - onDelete={(item: any) => handleDelete(tab.id, item)} - isLoading={getEntityLoading(tab.id)} - /> -
- ))} -
- - {modalOpen && currentEntity === 'cargo-types' && ( -
-
-
-

- {editingItem ? 'Edit Cargo Type' : 'Add Cargo Type'} -

- -
-
-
-
- - setFormData({...formData, code: e.target.value.toUpperCase()})} - required - /> -
-
- - setFormData({...formData, cargoTypeName: e.target.value})} - required - /> -
-
- - setFormData({...formData, displayOrder: parseInt(e.target.value)})} - /> -
-
- setFormData({...formData, showFreeTextBox: e.target.checked})} - /> - -
-
- setFormData({...formData, requiresDirectorApproval: e.target.checked})} - /> - -
-
- setFormData({...formData, isActive: e.target.checked})} - /> - -
-
-
- - -
-
-
-
- )} -
- ); -}; - -export default ContractTypePage; - diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/ContractType copy 3.tsx b/apps/edr-freight-web/backoffice/src/components/ruleEngine/ContractType copy 3.tsx deleted file mode 100644 index 208423745..000000000 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/ContractType copy 3.tsx +++ /dev/null @@ -1,366 +0,0 @@ -// src/components/ruleEngine/ContractType.tsx -import { useState } from 'react'; - -// ==================== MOCK DATA (Replace with your API calls later) ==================== -const mockCargoTypes = [ - { id: '1', code: 'BULK', cargoTypeName: 'Bulk Cargo', displayOrder: 1, isActive: true }, - { id: '2', code: 'BREAK_BULK', cargoTypeName: 'Break Bulk', displayOrder: 2, isActive: true }, - { id: '3', code: 'CONTAINER', cargoTypeName: 'Containerized', displayOrder: 3, isActive: false }, - { id: '4', code: 'LIQUID', cargoTypeName: 'Liquid Bulk', displayOrder: 4, isActive: true }, -]; - -const mockContainerTypes = [ - { id: '1', sizeCode: '20FT', description: '20 Foot Standard Container', containersPerWagon: 2, isActive: true }, - { id: '2', sizeCode: '40FT', description: '40 Foot Standard Container', containersPerWagon: 1, isActive: true }, - { id: '3', sizeCode: '20RF', description: '20 Foot Refrigerated', containersPerWagon: 2, isActive: true }, -]; - -const mockPriorityRules = [ - { id: '1', priorityType: 'HIGH', ruleName: 'High Priority Booking', bonusPoints: 100, isActive: true }, - { id: '2', priorityType: 'URGENT', ruleName: 'Urgent Delivery', bonusPoints: 200, isActive: true }, - { id: '3', priorityType: 'LOW', ruleName: 'Standard Booking', bonusPoints: 0, isActive: true }, -]; - -const mockServiceTypes = [ - { id: '1', code: 'RAIL', serviceName: 'Rail Only', displayOrder: 1, isActive: true }, - { id: '2', code: 'RAIL_FIRST', serviceName: 'Rail + First Mile', displayOrder: 2, isActive: true }, - { id: '3', code: 'RAIL_LAST', serviceName: 'Rail + Last Mile', displayOrder: 3, isActive: false }, -]; - -const mockSurchargeTypes = [ - { id: '1', code: 'HAZ', name: 'Hazardous Material', isActive: true }, - { id: '2', code: 'REF', name: 'Refrigerated', isActive: true }, - { id: '3', code: 'OVR', name: 'Overweight', isActive: true }, -]; - -const mockSurcharges = [ - { id: '1', feeName: 'Hazardous Fee', calculationMethod: 'FLAT', rate: 150, currency: 'USD', isActive: true }, - { id: '2', feeName: 'Refrigeration Fee', calculationMethod: 'PER_TON', rate: 25, currency: 'USD', isActive: true }, -]; - -const mockWeightLimitRules = [ - { id: '1', tradeDirection: 'IMPORT', maxWeightTons: 20, exceededAction: 'WARNING_ONLY', isActive: true }, - { id: '2', tradeDirection: 'EXPORT', maxWeightTons: 22, exceededAction: 'BLOCK', isActive: true }, -]; - -// ==================== Toast Component ==================== -const Toast = ({ message, type, onClose }: { message: string; type: string; onClose: () => void }) => { - setTimeout(onClose, 3000); - const bgColor = type === 'success' ? 'bg-green-500' : 'bg-red-500'; - return ( -
- {message} -
- ); -}; - -// ==================== Entity Table Component ==================== -const EntityTable = ({ title, data, columns, onAdd, onEdit, onDelete }: any) => { - const [expanded, setExpanded] = useState(true); - const [searchTerm, setSearchTerm] = useState(''); - - const filteredData = Array.isArray(data) ? data.filter((item: any) => - Object.values(item).some(value => - String(value).toLowerCase().includes(searchTerm.toLowerCase()) - ) - ) : []; - - return ( -
-
setExpanded(!expanded)} - > -
- {expanded ? '▼' : '▶'} -

{title}

- - {filteredData.length} items - -
-
- - {expanded && ( -
-
- -
- setSearchTerm(e.target.value)} - /> - - - -
-
- -
- - - - {columns.map((col: any) => ( - - ))} - - - - - {filteredData.map((item: any) => ( - - {columns.map((col: any) => ( - - ))} - - - ))} - -
- {col.label} - Actions
- {col.render ? col.render(item[col.key], item) : item[col.key]} - - - -
- {filteredData.length === 0 && ( -
No data found
- )} -
-
- )} -
- ); -}; - -// ==================== Main Component ==================== -const ContractTypePage = () => { - const [activeTab, setActiveTab] = useState('cargo-types'); - const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' } | null>(null); - - // State for each entity - const [cargoTypes, setCargoTypes] = useState(mockCargoTypes); - const [containerTypes, setContainerTypes] = useState(mockContainerTypes); - const [priorityRules, setPriorityRules] = useState(mockPriorityRules); - const [serviceTypes, setServiceTypes] = useState(mockServiceTypes); - const [surchargeTypes, setSurchargeTypes] = useState(mockSurchargeTypes); - const [surcharges, setSurcharges] = useState(mockSurcharges); - const [weightLimitRules, setWeightLimitRules] = useState(mockWeightLimitRules); - - const showToast = (message: string, type: 'success' | 'error') => { - setToast({ message, type }); - setTimeout(() => setToast(null), 3000); - }; - - const handleAdd = (entity: string) => { - const newId = String(Date.now()); - let newItem; - - switch(entity) { - case 'cargo-types': - newItem = { id: newId, code: 'NEW', cargoTypeName: 'New Type', displayOrder: cargoTypes.length + 1, isActive: true }; - setCargoTypes([...cargoTypes, newItem]); - break; - case 'container-types': - newItem = { id: newId, sizeCode: 'NEW', description: 'New Container', containersPerWagon: 1, isActive: true }; - setContainerTypes([...containerTypes, newItem]); - break; - case 'priority-rules': - newItem = { id: newId, priorityType: 'MEDIUM', ruleName: 'New Rule', bonusPoints: 0, isActive: true }; - setPriorityRules([...priorityRules, newItem]); - break; - case 'service-types': - newItem = { id: newId, code: 'NEW', serviceName: 'New Service', displayOrder: serviceTypes.length + 1, isActive: true }; - setServiceTypes([...serviceTypes, newItem]); - break; - case 'surcharge-types': - newItem = { id: newId, code: 'NEW', name: 'New Surcharge Type', isActive: true }; - setSurchargeTypes([...surchargeTypes, newItem]); - break; - case 'surcharges': - newItem = { id: newId, feeName: 'New Fee', calculationMethod: 'FLAT', rate: 0, currency: 'USD', isActive: true }; - setSurcharges([...surcharges, newItem]); - break; - case 'weight-limit-rules': - newItem = { id: newId, tradeDirection: 'IMPORT', maxWeightTons: 20, exceededAction: 'WARNING_ONLY', isActive: true }; - setWeightLimitRules([...weightLimitRules, newItem]); - break; - } - showToast(`${entity} added successfully`, 'success'); - }; - - const handleEdit = (entity: string, item: any) => { - showToast(`Edit ${item.code || item.sizeCode || item.ruleName || item.serviceName || item.name || item.feeName}`, 'success'); - }; - - const handleDelete = (entity: string, item: any) => { - if (confirm('Are you sure you want to delete this item?')) { - switch(entity) { - case 'cargo-types': - setCargoTypes(cargoTypes.filter(c => c.id !== item.id)); - break; - case 'container-types': - setContainerTypes(containerTypes.filter(c => c.id !== item.id)); - break; - case 'priority-rules': - setPriorityRules(priorityRules.filter(p => p.id !== item.id)); - break; - case 'service-types': - setServiceTypes(serviceTypes.filter(s => s.id !== item.id)); - break; - case 'surcharge-types': - setSurchargeTypes(surchargeTypes.filter(s => s.id !== item.id)); - break; - case 'surcharges': - setSurcharges(surcharges.filter(s => s.id !== item.id)); - break; - case 'weight-limit-rules': - setWeightLimitRules(weightLimitRules.filter(w => w.id !== item.id)); - break; - } - showToast(`${entity} deleted successfully`, 'success'); - } - }; - - const getColumns = (entity: string) => { - switch(entity) { - case 'cargo-types': - return [ - { key: 'code', label: 'Code' }, - { key: 'cargoTypeName', label: 'Name' }, - { key: 'displayOrder', label: 'Order' }, - { key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' } - ]; - case 'container-types': - return [ - { key: 'sizeCode', label: 'Size Code' }, - { key: 'description', label: 'Description' }, - { key: 'containersPerWagon', label: 'Containers/Wagon' }, - { key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' } - ]; - case 'priority-rules': - return [ - { key: 'priorityType', label: 'Priority Type' }, - { key: 'ruleName', label: 'Rule Name' }, - { key: 'bonusPoints', label: 'Bonus Points' }, - { key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' } - ]; - case 'service-types': - return [ - { key: 'code', label: 'Code' }, - { key: 'serviceName', label: 'Service Name' }, - { key: 'displayOrder', label: 'Order' }, - { key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' } - ]; - case 'surcharge-types': - return [ - { key: 'code', label: 'Code' }, - { key: 'name', label: 'Name' }, - { key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' } - ]; - case 'surcharges': - return [ - { key: 'feeName', label: 'Fee Name' }, - { key: 'calculationMethod', label: 'Method' }, - { key: 'rate', label: 'Rate', render: (val: number, item: any) => `${val} ${item.currency}` }, - { key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' } - ]; - case 'weight-limit-rules': - return [ - { key: 'tradeDirection', label: 'Direction' }, - { key: 'maxWeightTons', label: 'Max Weight', render: (val: number) => `${val} tons` }, - { key: 'exceededAction', label: 'Action' }, - { key: 'isActive', label: 'Status', render: (val: boolean) => val ? '✅ Active' : '❌ Inactive' } - ]; - default: - return []; - } - }; - - const getEntityData = (entity: string) => { - switch(entity) { - case 'cargo-types': return cargoTypes; - case 'container-types': return containerTypes; - case 'priority-rules': return priorityRules; - case 'service-types': return serviceTypes; - case 'surcharge-types': return surchargeTypes; - case 'surcharges': return surcharges; - case 'weight-limit-rules': return weightLimitRules; - default: return []; - } - }; - - const tabs = [ - { id: 'cargo-types', label: 'Cargo Types' }, - { id: 'container-types', label: 'Container Types' }, - { id: 'priority-rules', label: 'Priority Rules' }, - { id: 'service-types', label: 'Service Types' }, - { id: 'surcharge-types', label: 'Surcharge Types' }, - { id: 'surcharges', label: 'Surcharges' }, - { id: 'weight-limit-rules', label: 'Weight Limit Rules' }, - ]; - - return ( -
- {toast && setToast(null)} />} - -
-

Rule Engine - Master Data

-

Manage cargo types, container types, priority rules, and more

-
- -
-
- {tabs.map((tab) => ( - - ))} -
-
- -
- {tabs.map((tab) => ( -
- handleAdd(tab.id)} - onEdit={(item: any) => handleEdit(tab.id, item)} - onDelete={(item: any) => handleDelete(tab.id, item)} - /> -
- ))} -
-
- ); -}; - -export default ContractTypePage; \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/src/components/ruleEngine/ContractType copy 4.tsx b/apps/edr-freight-web/backoffice/src/components/ruleEngine/ContractType copy 4.tsx deleted file mode 100644 index e7b815ae9..000000000 --- a/apps/edr-freight-web/backoffice/src/components/ruleEngine/ContractType copy 4.tsx +++ /dev/null @@ -1,874 +0,0 @@ -// src/components/ruleEngine/ContractType.tsx -import { createCargoType } from '@/services/rule.engine/cargoType'; -import { useState, useEffect } from 'react'; - -// ==================== API Service ==================== -const API_BASE_URL = 'http://localhost:3001/api'; - -const apiFetch = async (endpoint: string, options?: RequestInit): Promise => { - try { - const url = `${API_BASE_URL}${endpoint}`; - const response = await fetch(url, { - headers: { - 'Content-Type': 'application/json', - 'Accept': 'application/json', - }, - ...options, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`HTTP ${response.status}: ${errorText || response.statusText}`); - } - - return await response.json(); - } catch (error) { - console.error(`API Error (${endpoint}):`, error); - throw error; - } -}; - -const apiService = { - getCargoTypes: (): Promise => apiFetch('/cargo-types'), - createCargoType: (data: any): Promise => apiFetch('/cargo-types', { method: 'POST', body: JSON.stringify(data) }), - updateCargoType: (id: string, data: any): Promise => apiFetch(`/cargo-types/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), - deleteCargoType: (id: string): Promise => apiFetch(`/cargo-types/${id}`, { method: 'DELETE' }), - - getContainerTypes: (): Promise => apiFetch('/container-types'), - createContainerType: (data: any): Promise => apiFetch('/container-types', { method: 'POST', body: JSON.stringify(data) }), - updateContainerType: (id: string, data: any): Promise => apiFetch(`/container-types/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), - deleteContainerType: (id: string): Promise => apiFetch(`/container-types/${id}`, { method: 'DELETE' }), - - getPriorityRules: (): Promise => apiFetch('/priority-rules'), - createPriorityRule: (data: any): Promise => apiFetch('/priority-rules', { method: 'POST', body: JSON.stringify(data) }), - updatePriorityRule: (id: string, data: any): Promise => apiFetch(`/priority-rules/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), - deletePriorityRule: (id: string): Promise => apiFetch(`/priority-rules/${id}`, { method: 'DELETE' }), - - getServiceTypes: (): Promise => apiFetch('/service-types'), - createServiceType: (data: any): Promise => apiFetch('/service-types', { method: 'POST', body: JSON.stringify(data) }), - updateServiceType: (id: string, data: any): Promise => apiFetch(`/service-types/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), - deleteServiceType: (id: string): Promise => apiFetch(`/service-types/${id}`, { method: 'DELETE' }), - - getSurchargeTypes: (): Promise => apiFetch('/surcharge-types'), - createSurchargeType: (data: any): Promise => apiFetch('/surcharge-types', { method: 'POST', body: JSON.stringify(data) }), - updateSurchargeType: (id: string, data: any): Promise => apiFetch(`/surcharge-types/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), - deleteSurchargeType: (id: string): Promise => apiFetch(`/surcharge-types/${id}`, { method: 'DELETE' }), - - getSurcharges: (): Promise => apiFetch('/surcharges'), - createSurcharge: (data: any): Promise => apiFetch('/surcharges', { method: 'POST', body: JSON.stringify(data) }), - updateSurcharge: (id: string, data: any): Promise => apiFetch(`/surcharges/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), - deleteSurcharge: (id: string): Promise => apiFetch(`/surcharges/${id}`, { method: 'DELETE' }), - - getWeightLimitRules: (): Promise => apiFetch('/weight-limit-rules'), - createWeightLimitRule: (data: any): Promise => apiFetch('/weight-limit-rules', { method: 'POST', body: JSON.stringify(data) }), - updateWeightLimitRule: (id: string, data: any): Promise => apiFetch(`/weight-limit-rules/${id}`, { method: 'PATCH', body: JSON.stringify(data) }), - deleteWeightLimitRule: (id: string): Promise => apiFetch(`/weight-limit-rules/${id}`, { method: 'DELETE' }), -}; - -// ==================== Toast Component ==================== -const Toast = ({ message, type, onClose }: { message: string; type: string; onClose: () => void }) => { - useEffect(() => { - const timer = setTimeout(onClose, 3000); - return () => clearTimeout(timer); - }, [onClose]); - - const bgColor = type === 'success' ? 'bg-green-500' : 'bg-red-500'; - return ( -
- {message} -
- ); -}; - -// ==================== Modal Component ==================== -const Modal = ({ isOpen, onClose, title, children }: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode }) => { - if (!isOpen) return null; - - return ( -
-
-
-

{title}

- -
-
{children}
-
-
- ); -}; - -// ==================== Form Components ==================== - -// 1. Cargo Type Form -const CargoTypeForm = ({ initialData, onSubmit, onCancel, isSubmitting }: any) => { - const [formData, setFormData] = useState({ - code: initialData?.code || '', - cargoTypeName: initialData?.cargoTypeName || '', - parentGroupId: initialData?.parentGroupId || '', - showFreeTextBox: initialData?.showFreeTextBox || false, - requiresDirectorApproval: initialData?.requiresDirectorApproval || false, - isActive: initialData?.isActive !== undefined ? initialData.isActive : true, - displayOrder: initialData?.displayOrder || 1, - }); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - const submitData = { - code: formData.code.toUpperCase(), - cargoTypeName: formData.cargoTypeName, - parentGroupId: formData.parentGroupId || undefined, - showFreeTextBox: formData.showFreeTextBox, - requiresDirectorApproval: formData.requiresDirectorApproval, - isActive: formData.isActive, - displayOrder: Number(formData.displayOrder), - }; - onSubmit(submitData); - }; - - return ( -
-
-
- - setFormData({...formData, code: e.target.value})} required /> -
-
- - setFormData({...formData, cargoTypeName: e.target.value})} required /> -
-
-
- - setFormData({...formData, parentGroupId: e.target.value})} /> -
-
-
- - setFormData({...formData, displayOrder: parseInt(e.target.value)})} /> -
-
-
- - - -
-
- - -
-
- ); -}; - -// 2. Container Type Form -const ContainerTypeForm = ({ initialData, onSubmit, onCancel, isSubmitting }: any) => { - const [formData, setFormData] = useState({ - sizeCode: initialData?.sizeCode || '', - description: initialData?.description || '', - containersPerWagon: initialData?.containersPerWagon || 1, - isActive: initialData?.isActive !== undefined ? initialData.isActive : true, - }); - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - onSubmit(formData); - }; - - return ( -
-
- - setFormData({...formData, sizeCode: e.target.value})} required /> -
-
- -