diff --git a/apps/edr-freight-api/src/app.module.ts b/apps/edr-freight-api/src/app.module.ts index 72038216e..6dd250283 100644 --- a/apps/edr-freight-api/src/app.module.ts +++ b/apps/edr-freight-api/src/app.module.ts @@ -46,6 +46,7 @@ import { PaymentModule } from "./modules/payment/payment.module"; import { DemoBookingsSeeder } from "./seed/demo-bookings.seeder"; import { PricingDataSeeder } from "./seed/pricing-data.seeder"; import { FileUploadSettingsSeeder } from "./seed/file-upload-settings.seeder"; +import { FreightPermissionKeyMigrationSeeder } from "./seed/freight-permission-key-migration.seeder"; //New Trains, Wagons, Container and Cargo management modules import { TrainsModule } from "./modules/trains/trains.module"; import { WagonsModule } from './modules/wagons/wagons.module'; @@ -110,7 +111,15 @@ import { OverviewModule } from './modules/overview/overview.module'; RoutesModule, OverviewModule, ], - providers: [EdrOrgSeeder, DemoUsersSeeder,FreightStaffUsersSeeder, DemoBookingsSeeder, PricingDataSeeder, FileUploadSettingsSeeder], + providers: [ + EdrOrgSeeder, + DemoUsersSeeder, + FreightStaffUsersSeeder, + DemoBookingsSeeder, + PricingDataSeeder, + FileUploadSettingsSeeder, + FreightPermissionKeyMigrationSeeder, + ], }) export class AppModule implements OnApplicationBootstrap { constructor( @@ -121,9 +130,11 @@ export class AppModule implements OnApplicationBootstrap { private readonly demoBookingsSeeder: DemoBookingsSeeder, private readonly pricingDataSeeder: PricingDataSeeder, private readonly fileUploadSettingsSeeder: FileUploadSettingsSeeder, + private readonly freightPermissionKeyMigrationSeeder: FreightPermissionKeyMigrationSeeder, ) { } async onApplicationBootstrap() { + await this.freightPermissionKeyMigrationSeeder.run(); await this.seeder.run(); await this.edrOrgSeeder.run(); await this.demoUsersSeeder.run(); diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts index 8631aa9e4..77d60b439 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts @@ -128,6 +128,20 @@ export class BookingsController { return this.bookingsService.getListSummary(filter); } + @Get('my') + @ApiOperation({ + summary: "List the current customer's bookings ready for payment", + description: + 'Bookings owned by the authenticated user\'s company that are payable ' + + '(FULLY_EXECUTED, SELECTED_FOR_BATCH, AWAITING_PAYMENT) and not yet PAID.', + }) + findMyPayable( + @CurrentUser() user: AuthUserPayload, + @Query() filter: FilterBookingDto, + ) { + return this.bookingsService.findMyPayable(resolveAuthUserId(user), filter); + } + @Get('queues/:queue') @ApiOperation({ summary: 'List bookings for a dashboard queue', diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts index d1084fbab..3e95b917d 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.repository.ts @@ -31,6 +31,8 @@ export interface BookingListFilterOptions { freightType?: string; tradeDirection?: string; paymentCurrency?: string; + paymentStatus?: string; + excludePaymentStatus?: string; allowConsolidation?: boolean; consolidationPaired?: string; } @@ -570,6 +572,16 @@ export class BookingsRepository extends BaseRepository { paymentCurrency: options.paymentCurrency, }); } + if (options.paymentStatus) { + qb.andWhere('booking.payment_status = :paymentStatus', { + paymentStatus: options.paymentStatus, + }); + } + if (options.excludePaymentStatus) { + qb.andWhere('booking.payment_status != :excludePaymentStatus', { + excludePaymentStatus: options.excludePaymentStatus, + }); + } if (options.allowConsolidation !== undefined) { qb.andWhere('booking.allow_consolidation = :allowConsolidation', { allowConsolidation: options.allowConsolidation, diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index c07a658da..d1c135152 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -552,6 +552,7 @@ export class BookingsService { freightType: filter.freightType, tradeDirection: filter.tradeDirection, paymentCurrency: filter.paymentCurrency, + paymentStatus: filter.paymentStatus, allowConsolidation: filter.allowConsolidation, consolidationPaired: filter.consolidationPaired, sortBy: filter.sortBy, @@ -559,6 +560,35 @@ export class BookingsService { }); } + /** Booking statuses at which a customer can pay (mirrors booking-payment.service). */ + private static readonly PAYABLE_STATUSES = [ + 'FULLY_EXECUTED', + 'SELECTED_FOR_BATCH', + 'AWAITING_PAYMENT', + ]; + + /** + * List the current customer's bookings that are ready for payment: + * payable status AND not yet PAID. Company scope is derived from the + * authenticated user and cannot be widened by the caller. + */ + async findMyPayable( + userId: string, + filter: FilterBookingDto, + ): Promise<{ items: Booking[]; total: number }> { + const { company } = await this.companiesService.getCompanyInfoByUserId(userId); + + return this.bookingsRepository.findAllPaginated({ + page: filter.page ?? 1, + pageSize: filter.pageSize ?? 20, + statuses: BookingsService.PAYABLE_STATUSES, + excludePaymentStatus: 'PAID', + companyId: company.id, + sortBy: filter.sortBy, + sortOrder: filter.sortOrder, + }); + } + /** Aggregate metrics and tab counts for the backoffice booking list. */ async getListSummary(filter: FilterBookingDto): Promise { const page = filter.page ?? 1; @@ -573,6 +603,7 @@ export class BookingsService { freightType: filter.freightType, tradeDirection: filter.tradeDirection, paymentCurrency: filter.paymentCurrency, + paymentStatus: filter.paymentStatus, allowConsolidation: filter.allowConsolidation, consolidationPaired: filter.consolidationPaired, }; diff --git a/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts b/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts index b88c381ae..9ce90d2b9 100644 --- a/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts +++ b/apps/edr-freight-api/src/modules/bookings/dto/filter-booking.dto.ts @@ -7,6 +7,7 @@ import { PAYMENT_CURRENCIES, TRADE_DIRECTIONS, } from './create-booking.dto'; +import { PAYMENT_STATUSES } from '../entities/booking.entity'; export class FilterBookingDto { @ApiPropertyOptional({ enum: BOOKING_STATUSES }) @@ -65,6 +66,11 @@ export class FilterBookingDto { @IsIn([...PAYMENT_CURRENCIES]) paymentCurrency?: string; + @ApiPropertyOptional({ enum: PAYMENT_STATUSES }) + @IsOptional() + @IsIn([...PAYMENT_STATUSES]) + paymentStatus?: string; + @ApiPropertyOptional() @IsOptional() @Transform(({ value }) => value === 'true' || value === true) diff --git a/apps/edr-freight-api/src/seed/freight-permission-key-migration.seeder.ts b/apps/edr-freight-api/src/seed/freight-permission-key-migration.seeder.ts new file mode 100644 index 000000000..0a0f86a64 --- /dev/null +++ b/apps/edr-freight-api/src/seed/freight-permission-key-migration.seeder.ts @@ -0,0 +1,48 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { Permission } from '@tria-plc/iamapi-common'; +import { DataSource } from 'typeorm'; + +/** Renamed rule-engine resources: old key -> new key (same permission id). */ +const PERMISSION_KEY_RENAMES: ReadonlyArray<{ from: string; to: string }> = [ + { + from: 'edr_freight_app:rule_engine:priority_rules:view', + to: 'edr_freight_app:rule_engine:priority_configs:view', + }, + { + from: 'edr_freight_app:rule_engine:priority_rules:manage', + to: 'edr_freight_app:rule_engine:priority_configs:manage', + }, +]; + +@Injectable() +export class FreightPermissionKeyMigrationSeeder { + private readonly logger = new Logger(FreightPermissionKeyMigrationSeeder.name); + + constructor(private readonly dataSource: DataSource) {} + + async run() { + const permissionRepository = this.dataSource.getRepository(Permission); + + for (const { from, to } of PERMISSION_KEY_RENAMES) { + const existing = await permissionRepository.findOne({ + where: { key: from }, + select: { id: true, key: true }, + }); + + if (!existing) { + continue; + } + + const targetExists = await permissionRepository.existsBy({ key: to }); + if (targetExists) { + this.logger.warn( + `Skipping permission key rename ${from} -> ${to}: target key already exists`, + ); + continue; + } + + await permissionRepository.update({ id: existing.id }, { key: to }); + this.logger.log(`Renamed permission key ${from} -> ${to}`); + } + } +} diff --git a/apps/edr-freight-web/backoffice/package.json b/apps/edr-freight-web/backoffice/package.json index e0982f815..d680625ac 100644 --- a/apps/edr-freight-web/backoffice/package.json +++ b/apps/edr-freight-web/backoffice/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite --port 5183", - "build": "cd ./user-management-config && npm run build && tsc -b && vite build", + "build": "vite build", "preview": "vite preview --port 5183", "lint": "eslint src", "test": "vitest run", @@ -17,10 +17,10 @@ "dependencies": { "@edr/types": "workspace:*", "@edr/ui-common": "workspace:*", + "@hello-pangea/dnd": "^18.0.1", "@mantine/core": "^9.3.0", "@mantine/hooks": "^9.3.0", "@tabler/icons-react": "^3.44.0", - "@hello-pangea/dnd": "^18.0.1", "@tanstack/react-query": "^5.100.11", "@tria-plc/iamui-common": "1.1.2", "axios": "^1.7.7", @@ -36,6 +36,7 @@ "recharts": "^3.8.1", "sonner": "^2.0.7", "tailwind-merge": "^3.6.0", + "tinymce": "^8.6.0", "zustand": "^5.0.0" }, "devDependencies": { diff --git a/apps/edr-freight-web/backoffice/user-management-config/package-lock.json b/apps/edr-freight-web/backoffice/user-management-config/package-lock.json new file mode 100644 index 000000000..87f006ce4 --- /dev/null +++ b/apps/edr-freight-web/backoffice/user-management-config/package-lock.json @@ -0,0 +1,12 @@ +{ + "name": "user-management-host", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "user-management-host", + "version": "0.0.0" + } + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9477430a..c2e68f72e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -249,6 +249,9 @@ importers: tailwind-merge: specifier: ^3.6.0 version: 3.6.0 + tinymce: + specifier: ^8.6.0 + version: 8.6.0 zustand: specifier: ^5.0.0 version: 5.0.14(@types/react@18.3.31)(immer@11.1.8)(react@19.2.6)(use-sync-external-store@1.6.0(react@19.2.6)) @@ -11690,6 +11693,9 @@ packages: tinymce@7.9.3: resolution: {integrity: sha512-Mtm54U5YJ6Pyo/GaAx+JSHXTGEuxrg2AowVWCD9zy1eBolp5Ub7S1rTtsyQdxhPegfhLuR3VLiTKGw1tacv09g==} + tinymce@8.6.0: + resolution: {integrity: sha512-qODYoNL4cPIzNFpkEEQDm3DWI78I/nQXIPwUMHRN+q/LyT9Wzt7xis2Nfhk88kV6sibp6MJXPSaUDVNe02ZB/w==} + tinypool@1.1.1: resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -26602,6 +26608,8 @@ snapshots: tinymce@7.9.3: {} + tinymce@8.6.0: {} + tinypool@1.1.1: {} tinyrainbow@1.2.0: {}