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/booking-reference-data.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-reference-data.service.ts index dc4f292b7..8a2d52172 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-reference-data.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-reference-data.service.ts @@ -1,28 +1,28 @@ -import { Inject, Injectable } from '@nestjs/common'; -import { In, Not } from 'typeorm'; +import { Inject, Injectable } from "@nestjs/common"; +import { In, Not } from "typeorm"; -import { CargoType } from '../rule-engine/entities/cargo-type.entity'; -import { ContainerType } from '../rule-engine/entities/container-type.entity'; +import { CargoType } from "../rule-engine/entities/cargo-type.entity"; +import { ContainerType } from "../rule-engine/entities/container-type.entity"; import { CARGO_TYPES_REPOSITORY, ICargoTypesRepository, -} from '../rule-engine/interfaces/cargo-types.repository.interface'; +} from "../rule-engine/interfaces/cargo-types.repository.interface"; import { CONTAINER_TYPES_REPOSITORY, IContainerTypesRepository, -} from '../rule-engine/interfaces/container-types.repository.interface'; +} from "../rule-engine/interfaces/container-types.repository.interface"; import { IServiceTypesRepository, SERVICE_TYPES_REPOSITORY, -} from '../rule-engine/interfaces/service-types.repository.interface'; +} from "../rule-engine/interfaces/service-types.repository.interface"; import { IShippingLinesRepository, SHIPPING_LINES_REPOSITORY, -} from '../rule-engine/interfaces/shipping-lines.repository.interface'; +} from "../rule-engine/interfaces/shipping-lines.repository.interface"; import { IYardsRepository, YARDS_REPOSITORY, -} from '../rule-engine/interfaces/yards.repository.interface'; +} from "../rule-engine/interfaces/yards.repository.interface"; import { BookingReferenceCargoTypeChildDto, BookingReferenceCargoTypeGroupDto, @@ -32,9 +32,9 @@ import { BookingReferenceServiceDto, BookingReferenceShippingLineDto, BookingReferenceYardDto, -} from './dto/booking-reference-data.dto'; +} from "./dto/booking-reference-data.dto"; -const LEGACY_YARD_CODES = ['LEGACY_ORIGIN', 'LEGACY_DEST'] as const; +const LEGACY_YARD_CODES = ["LEGACY_ORIGIN", "LEGACY_DEST"] as const; export function buildCargoTypeTree( rows: CargoType[], @@ -42,13 +42,16 @@ export function buildCargoTypeTree( const active = rows.filter((r) => r.isActive); const parents = active .filter((r) => !r.parentGroupId) - .sort((a, b) => a.displayOrder - b.displayOrder || a.code.localeCompare(b.code)); + .sort( + (a, b) => a.displayOrder - b.displayOrder || a.code.localeCompare(b.code), + ); return parents.map((parent) => { const children = active .filter((r) => r.parentGroupId === parent.id) .sort( - (a, b) => a.displayOrder - b.displayOrder || a.code.localeCompare(b.code), + (a, b) => + a.displayOrder - b.displayOrder || a.code.localeCompare(b.code), ) .map( (child): BookingReferenceCargoTypeChildDto => ({ @@ -79,14 +82,14 @@ export function groupContainersBySize( for (const ct of active) { const sizeKey = - ct.sizeFt != null && ct.sizeFt > 0 ? `${ct.sizeFt}ft` : 'other'; + ct.sizeFt != null && ct.sizeFt > 0 ? `${ct.sizeFt}ft` : "other"; const list = bySize.get(sizeKey) ?? []; list.push(ct); bySize.set(sizeKey, list); } const sortSizeKey = (key: string): number => { - if (key === 'other') return Number.MAX_SAFE_INTEGER; + if (key === "other") return Number.MAX_SAFE_INTEGER; const n = parseInt(key, 10); return Number.isNaN(n) ? Number.MAX_SAFE_INTEGER - 1 : n; }; @@ -126,7 +129,7 @@ export class BookingReferenceDataService { private readonly shippingLinesRepository: IShippingLinesRepository, @Inject(CARGO_TYPES_REPOSITORY) private readonly cargoTypesRepository: ICargoTypesRepository, - ) {} + ) { } async getReferenceData(): Promise { const [yards, containerTypes, serviceTypes, shippingLines, cargoTypes] = @@ -136,23 +139,23 @@ export class BookingReferenceDataService { isActive: true, code: Not(In([...LEGACY_YARD_CODES])), }, - order: { displayOrder: 'ASC', code: 'ASC' }, + order: { displayOrder: "ASC", code: "ASC" }, }), this.containerTypesRepository.findAll({ where: { isActive: true }, - order: { displayOrder: 'ASC', code: 'ASC' }, + order: { displayOrder: "ASC", code: "ASC" }, }), this.serviceTypesRepository.findAll({ where: { isActive: true }, - order: { displayOrder: 'ASC', code: 'ASC' }, + order: { displayOrder: "ASC", code: "ASC" }, }), this.shippingLinesRepository.findAll({ where: { isActive: true }, - order: { label: 'ASC', code: 'ASC' }, + order: { label: "ASC", code: "ASC" }, }), this.cargoTypesRepository.findAll({ where: { isActive: true }, - order: { displayOrder: 'ASC', code: 'ASC' }, + order: { displayOrder: "ASC", code: "ASC" }, }), ]); @@ -168,9 +171,8 @@ export class BookingReferenceDataService { containers: groupContainersBySize(containerTypes), service: serviceTypes.map( (s): BookingReferenceServiceDto => ({ - id: s.id, name: s.serviceName, - code: s.code, + ...s, }), ), shipping_line: shippingLines.map( diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts index 3c191c713..3ef7693ab 100644 --- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts +++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.controller.ts @@ -8,87 +8,98 @@ import { Patch, Post, Query, -} from '@nestjs/common'; -import { CurrentUser } from '@edr/api-common'; -import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; -import type { AuthUserPayload } from '../../common/resolve-auth-user-id'; -import { resolveAuthUserId } from '../../common/resolve-auth-user-id'; +} from "@nestjs/common"; +import { CurrentUser } from "@edr/api-common"; +import { ApiBearerAuth, ApiOperation, ApiTags } from "@nestjs/swagger"; +import type { AuthUserPayload } from "../../common/resolve-auth-user-id"; +import { resolveAuthUserId } from "../../common/resolve-auth-user-id"; -import { TrainSchedulingManage, TrainSchedulingView } from '../../common/booking-guards'; -import { AssignBookingsDto } from './dto/assign-bookings.dto'; -import { AssignUnassignedBookingDto } from './dto/assign-unassigned-booking.dto'; -import { CreateContainerTrainScheduleDto } from './dto/create-container-train-schedule.dto'; -import { GetEligibleBookingsDto } from './dto/get-eligible-bookings.dto'; -import { GetEligibleBulkBookingsDto } from './dto/get-eligible-bulk-bookings.dto'; -import { GetEligibleContainerBookingsDto } from './dto/get-eligible-container-bookings.dto'; -import { PinWagonsDto } from './dto/pin-wagons.dto'; -import { UpdateContainerItemDto } from './dto/update-container-item.dto'; -import { PreviewBulkTrainScheduleDto } from './dto/preview-bulk-train-schedule.dto'; -import { PreviewContainerTrainScheduleDto } from './dto/preview-container-train-schedule.dto'; -import { PreviewTrainScheduleDto } from './dto/preview-train-schedule.dto'; -import { RecordCheckpointDto } from './dto/record-checkpoint.dto'; -import { AvailableLocomotivesQueryDto } from './dto/available-locomotives-query.dto'; -import { BookableSchedulesQueryDto } from './dto/bookable-schedules-query.dto'; -import { UpdateTrainSchedulingGlobalRulesDto } from './dto/update-train-scheduling-global-rules.dto'; -import { TrainSchedulingService } from './train-scheduling.service'; -import { BookingBatchService } from './booking-batch.service'; +import { + TrainSchedulingManage, + TrainSchedulingView, +} from "../../common/booking-guards"; +import { AssignBookingsDto } from "./dto/assign-bookings.dto"; +import { AssignUnassignedBookingDto } from "./dto/assign-unassigned-booking.dto"; +import { CreateContainerTrainScheduleDto } from "./dto/create-container-train-schedule.dto"; +import { GetEligibleBookingsDto } from "./dto/get-eligible-bookings.dto"; +import { GetEligibleBulkBookingsDto } from "./dto/get-eligible-bulk-bookings.dto"; +import { GetEligibleContainerBookingsDto } from "./dto/get-eligible-container-bookings.dto"; +import { PinWagonsDto } from "./dto/pin-wagons.dto"; +import { UpdateContainerItemDto } from "./dto/update-container-item.dto"; +import { PreviewBulkTrainScheduleDto } from "./dto/preview-bulk-train-schedule.dto"; +import { PreviewContainerTrainScheduleDto } from "./dto/preview-container-train-schedule.dto"; +import { PreviewTrainScheduleDto } from "./dto/preview-train-schedule.dto"; +import { RecordCheckpointDto } from "./dto/record-checkpoint.dto"; +import { AvailableLocomotivesQueryDto } from "./dto/available-locomotives-query.dto"; +import { BookableSchedulesQueryDto } from "./dto/bookable-schedules-query.dto"; +import { UpdateTrainSchedulingGlobalRulesDto } from "./dto/update-train-scheduling-global-rules.dto"; +import { TrainSchedulingService } from "./train-scheduling.service"; +import { BookingBatchService } from "./booking-batch.service"; -@ApiTags('train-scheduling') +@ApiTags("train-scheduling") @ApiBearerAuth() -@Controller('train-scheduling') +@Controller("train-scheduling") export class TrainSchedulingController { constructor( private readonly trainSchedulingService: TrainSchedulingService, private readonly bookingBatchService: BookingBatchService, - ) {} + ) { } - @Get('global-rules') + @Get("global-rules") @TrainSchedulingView() - @ApiOperation({ summary: 'Get global train scheduling rules (singleton)' }) + @ApiOperation({ summary: "Get global train scheduling rules (singleton)" }) getGlobalRules() { return this.trainSchedulingService.getTrainSchedulingGlobalRules(); } - @Patch('global-rules') + @Patch("global-rules") @TrainSchedulingManage() - @ApiOperation({ summary: 'Update global train scheduling rules (singleton)' }) + @ApiOperation({ summary: "Update global train scheduling rules (singleton)" }) updateGlobalRules(@Body() dto: UpdateTrainSchedulingGlobalRulesDto) { return this.trainSchedulingService.updateTrainSchedulingGlobalRules(dto); } - @Get('eligible-bookings') + @Get("eligible-bookings") @TrainSchedulingView() - @ApiOperation({ summary: 'List eligible bookings (container and/or bulk)' }) + @ApiOperation({ summary: "List eligible bookings (container and/or bulk)" }) getEligibleBookings(@Query() query: GetEligibleBookingsDto) { return this.trainSchedulingService.getEligibleBookings(query); } - @Get('batch-board') + @Get("batch-board") @TrainSchedulingView() - @ApiOperation({ summary: 'Batch monitoring board: schedules with bookings grouped by state' }) + @ApiOperation({ + summary: "Batch monitoring board: schedules with bookings grouped by state", + }) getBatchBoard() { return this.bookingBatchService.getBatchBoard(); } - @Get('batch-board/:scheduleId') + @Get("batch-board/:scheduleId") @TrainSchedulingView() - @ApiOperation({ summary: 'Batch board detail for one schedule with EAT 3h windows' }) - getBatchBoardDetail(@Param('scheduleId', ParseUUIDPipe) scheduleId: string) { + @ApiOperation({ + summary: "Batch board detail for one schedule with EAT 3h windows", + }) + getBatchBoardDetail(@Param("scheduleId", ParseUUIDPipe) scheduleId: string) { return this.bookingBatchService.getBatchBoardDetail(scheduleId); } - @Get('available-locomotives') + @Get("available-locomotives") @TrainSchedulingView() @ApiOperation({ - summary: 'List AVAILABLE locomotives at the route origin yard', + summary: "List AVAILABLE locomotives at the route origin yard", }) getAvailableLocomotives(@Query() query: AvailableLocomotivesQueryDto) { - return this.trainSchedulingService.getAvailableLocomotivesForRoute(query.routeId); + return this.trainSchedulingService.getAvailableLocomotivesForRoute( + query.routeId, + ); } - @Get('bookable-schedules') - @TrainSchedulingView() - @ApiOperation({ summary: 'OPEN same-route schedules a new booking can target' }) + @Get("bookable-schedules") + // @TrainSchedulingView() + @ApiOperation({ + summary: "OPEN same-route schedules a new booking can target", + }) getBookableSchedules(@Query() query: BookableSchedulesQueryDto) { return this.trainSchedulingService.getBookableSchedules( query.originYardId, @@ -96,282 +107,323 @@ export class TrainSchedulingController { ); } - @Get('container/eligible-bookings') + @Get("container/eligible-bookings") @TrainSchedulingView() - @ApiOperation({ summary: 'List eligible container bookings' }) - getEligibleContainerBookings(@Query() query: GetEligibleContainerBookingsDto) { + @ApiOperation({ summary: "List eligible container bookings" }) + getEligibleContainerBookings( + @Query() query: GetEligibleContainerBookingsDto, + ) { return this.trainSchedulingService.getEligibleContainerBookings(query); } - @Get('bulk/eligible-bookings') + @Get("bulk/eligible-bookings") @TrainSchedulingView() - @ApiOperation({ summary: 'List eligible bulk bookings' }) + @ApiOperation({ summary: "List eligible bulk bookings" }) getEligibleBulkBookings(@Query() query: GetEligibleBulkBookingsDto) { return this.trainSchedulingService.getEligibleBulkBookings(query); } - @Post('preview') + @Post("preview") @TrainSchedulingView() - @ApiOperation({ summary: 'Preview a mixed-capable train schedule' }) + @ApiOperation({ summary: "Preview a mixed-capable train schedule" }) previewTrainSchedule(@Body() dto: PreviewTrainScheduleDto) { return this.trainSchedulingService.previewTrainSchedule(dto); } - @Post('container/preview') + @Post("container/preview") @TrainSchedulingView() - @ApiOperation({ summary: 'Preview a container train schedule' }) + @ApiOperation({ summary: "Preview a container train schedule" }) previewContainerTrainSchedule(@Body() dto: PreviewContainerTrainScheduleDto) { return this.trainSchedulingService.previewContainerTrainSchedule(dto); } - @Post('bulk/preview') + @Post("bulk/preview") @TrainSchedulingView() - @ApiOperation({ summary: 'Preview a bulk train schedule' }) + @ApiOperation({ summary: "Preview a bulk train schedule" }) previewBulkTrainSchedule(@Body() dto: PreviewBulkTrainScheduleDto) { return this.trainSchedulingService.previewBulkTrainSchedule(dto); } - @Post('container/schedules') + @Post("container/schedules") @TrainSchedulingManage() - @ApiOperation({ summary: 'Create a container train schedule' }) + @ApiOperation({ summary: "Create a container train schedule" }) createContainerTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) { return this.trainSchedulingService.createContainerTrainSchedule(dto); } - @Post('bulk/schedules') + @Post("bulk/schedules") @TrainSchedulingManage() - @ApiOperation({ summary: 'Create a bulk train schedule' }) + @ApiOperation({ summary: "Create a bulk train schedule" }) createBulkTrainSchedule(@Body() dto: CreateContainerTrainScheduleDto) { return this.trainSchedulingService.createContainerTrainSchedule(dto); } - @Post('schedules/:id/assign-bookings') + @Post("schedules/:id/assign-bookings") @TrainSchedulingManage() - @ApiOperation({ summary: 'Assign bookings to a train schedule (mixed-capable)' }) + @ApiOperation({ + summary: "Assign bookings to a train schedule (mixed-capable)", + }) assignBookings( - @Param('id', ParseUUIDPipe) id: string, + @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { return this.trainSchedulingService.assignBookingsToSchedule(id, dto); } - @Post('container/schedules/:id/assign-bookings') + @Post("container/schedules/:id/assign-bookings") @TrainSchedulingManage() - @ApiOperation({ summary: 'Assign container bookings to a train schedule' }) + @ApiOperation({ summary: "Assign container bookings to a train schedule" }) assignContainerBookings( - @Param('id', ParseUUIDPipe) id: string, + @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { - return this.trainSchedulingService.assignBookingsToSchedule(id, dto, 'CONTAINER'); + return this.trainSchedulingService.assignBookingsToSchedule( + id, + dto, + "CONTAINER", + ); } - @Post('bulk/schedules/:id/assign-bookings') + @Post("bulk/schedules/:id/assign-bookings") @TrainSchedulingManage() - @ApiOperation({ summary: 'Assign bulk bookings to a train schedule' }) + @ApiOperation({ summary: "Assign bulk bookings to a train schedule" }) assignBulkBookings( - @Param('id', ParseUUIDPipe) id: string, + @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignBookingsDto, ) { - return this.trainSchedulingService.assignBookingsToSchedule(id, dto, 'BULK'); + return this.trainSchedulingService.assignBookingsToSchedule( + id, + dto, + "BULK", + ); } - @Delete('schedules/:id/bookings/:bookingId') + @Delete("schedules/:id/bookings/:bookingId") @TrainSchedulingManage() - @ApiOperation({ summary: 'Unassign a booking from a train schedule' }) + @ApiOperation({ summary: "Unassign a booking from a train schedule" }) unassignBooking( - @Param('id', ParseUUIDPipe) id: string, - @Param('bookingId', ParseUUIDPipe) bookingId: string, + @Param("id", ParseUUIDPipe) id: string, + @Param("bookingId", ParseUUIDPipe) bookingId: string, @CurrentUser() user: AuthUserPayload, ) { - return this.trainSchedulingService.unassignBooking(id, bookingId, resolveAuthUserId(user)); + return this.trainSchedulingService.unassignBooking( + id, + bookingId, + resolveAuthUserId(user), + ); } - @Delete('schedules/:id/wagons/:trainSetWagonId') + @Delete("schedules/:id/wagons/:trainSetWagonId") @TrainSchedulingManage() - @ApiOperation({ summary: 'Remove an empty wagon slot from a train' }) + @ApiOperation({ summary: "Remove an empty wagon slot from a train" }) removeWagonSlot( - @Param('id', ParseUUIDPipe) id: string, - @Param('trainSetWagonId', ParseUUIDPipe) trainSetWagonId: string, + @Param("id", ParseUUIDPipe) id: string, + @Param("trainSetWagonId", ParseUUIDPipe) trainSetWagonId: string, ) { - return this.trainSchedulingService.removeTrainSetWagonSlot(id, trainSetWagonId); + return this.trainSchedulingService.removeTrainSetWagonSlot( + id, + trainSetWagonId, + ); } - @Patch('schedules/:id/container-items/:itemId') + @Patch("schedules/:id/container-items/:itemId") @TrainSchedulingManage() - @ApiOperation({ summary: 'Update a container number on a wagon slot' }) + @ApiOperation({ summary: "Update a container number on a wagon slot" }) updateContainerItem( - @Param('id', ParseUUIDPipe) id: string, - @Param('itemId', ParseUUIDPipe) itemId: string, + @Param("id", ParseUUIDPipe) id: string, + @Param("itemId", ParseUUIDPipe) itemId: string, @Body() dto: UpdateContainerItemDto, ) { return this.trainSchedulingService.updateContainerItem(id, itemId, dto); } - @Get('schedules/:id/unassigned-bookings') + @Get("schedules/:id/unassigned-bookings") @TrainSchedulingView() - @ApiOperation({ summary: 'Get unassigned bookings for a schedule' }) - getUnassignedBookings(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Get unassigned bookings for a schedule" }) + getUnassignedBookings(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getUnassignedBookings(id); } - @Post('schedules/:id/assign-unassigned-booking') + @Post("schedules/:id/assign-unassigned-booking") @TrainSchedulingManage() @ApiOperation({ - summary: 'Assign one linked unallocated booking to wagons (preserves existing assignments)', + summary: + "Assign one linked unallocated booking to wagons (preserves existing assignments)", }) assignUnassignedBooking( - @Param('id', ParseUUIDPipe) id: string, + @Param("id", ParseUUIDPipe) id: string, @Body() dto: AssignUnassignedBookingDto, ) { - return this.trainSchedulingService.assignUnassignedBookingToWagons(id, dto.bookingId); + return this.trainSchedulingService.assignUnassignedBookingToWagons( + id, + dto.bookingId, + ); } - @Get('schedules/:id/composition-removals') + @Get("schedules/:id/composition-removals") @TrainSchedulingView() - @ApiOperation({ summary: 'Get removal log for a schedule' }) - getCompositionRemovals(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Get removal log for a schedule" }) + getCompositionRemovals(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getCompositionRemovals(id); } - @Post('schedules/:id/pin-wagons') + @Post("schedules/:id/pin-wagons") @TrainSchedulingManage() - @ApiOperation({ summary: 'Pin physical wagons to train set slots' }) - pinWagons(@Param('id', ParseUUIDPipe) id: string, @Body() dto: PinWagonsDto) { + @ApiOperation({ summary: "Pin physical wagons to train set slots" }) + pinWagons(@Param("id", ParseUUIDPipe) id: string, @Body() dto: PinWagonsDto) { return this.trainSchedulingService.pinWagons(id, dto); } - @Post('schedules/:id/finalize') + @Post("schedules/:id/finalize") @TrainSchedulingManage() - @ApiOperation({ summary: 'Finalize a draft train schedule' }) - finalizeSchedule(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Finalize a draft train schedule" }) + finalizeSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.finalizeSchedule(id); } - @Post('schedules/:id/dispatch') + @Post("schedules/:id/dispatch") @TrainSchedulingManage() - @ApiOperation({ summary: 'Dispatch a scheduled train' }) - dispatchSchedule(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Dispatch a scheduled train" }) + dispatchSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.dispatchSchedule(id); } // ---- batch / booking-window staff actions ---- - @Post('schedules/:id/run-batch') + @Post("schedules/:id/run-batch") @TrainSchedulingManage() - @ApiOperation({ summary: 'Manually run the batch fill for a schedule' }) - async runBatch(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Manually run the batch fill for a schedule" }) + async runBatch(@Param("id", ParseUUIDPipe) id: string) { await this.bookingBatchService.fillSchedule(id); return this.bookingBatchService.getBatchBoardDetail(id); } - @Post('schedules/:id/run-allocation') + @Post("schedules/:id/run-allocation") @TrainSchedulingManage() - @ApiOperation({ summary: 'Run wagon-level allocation for all eligible linked bookings' }) - async runAllocation(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ + summary: "Run wagon-level allocation for all eligible linked bookings", + }) + async runAllocation(@Param("id", ParseUUIDPipe) id: string) { return this.bookingBatchService.runWagonAllocation(id); } - @Patch('schedules/:id/booking-window') + @Patch("schedules/:id/booking-window") @TrainSchedulingManage() - @ApiOperation({ summary: 'Open or close a schedule booking window' }) + @ApiOperation({ summary: "Open or close a schedule booking window" }) async setBookingWindow( - @Param('id', ParseUUIDPipe) id: string, - @Body('status') status: 'OPEN' | 'CLOSED', + @Param("id", ParseUUIDPipe) id: string, + @Body("status") status: "OPEN" | "CLOSED", ) { - await this.trainSchedulingService.setBookingWindow(id, status === 'CLOSED' ? 'CLOSED' : 'OPEN'); + await this.trainSchedulingService.setBookingWindow( + id, + status === "CLOSED" ? "CLOSED" : "OPEN", + ); return this.trainSchedulingService.getContainerTrainScheduleById(id); } - @Post('bookings/:bookingId/mark-paid') + @Post("bookings/:bookingId/mark-paid") @TrainSchedulingManage() - @ApiOperation({ summary: 'Staff: mark a reserved booking paid and allocate it now' }) - async markBookingPaid(@Param('bookingId', ParseUUIDPipe) bookingId: string) { + @ApiOperation({ + summary: "Staff: mark a reserved booking paid and allocate it now", + }) + async markBookingPaid(@Param("bookingId", ParseUUIDPipe) bookingId: string) { await this.bookingBatchService.markPaid(bookingId); return { ok: true }; } - @Post('bookings/:bookingId/expire') + @Post("bookings/:bookingId/expire") @TrainSchedulingManage() - @ApiOperation({ summary: 'Staff: expire a reservation and free its capacity' }) - async expireBooking(@Param('bookingId', ParseUUIDPipe) bookingId: string) { + @ApiOperation({ + summary: "Staff: expire a reservation and free its capacity", + }) + async expireBooking(@Param("bookingId", ParseUUIDPipe) bookingId: string) { await this.bookingBatchService.expireReservation(bookingId); return { ok: true }; } - @Post('bookings/:bookingId/move-schedule') + @Post("bookings/:bookingId/move-schedule") @TrainSchedulingManage() - @ApiOperation({ summary: 'Re-point a booking to another OPEN same-route schedule' }) + @ApiOperation({ + summary: "Re-point a booking to another OPEN same-route schedule", + }) async moveBookingSchedule( - @Param('bookingId', ParseUUIDPipe) bookingId: string, - @Body('trainScheduleId', ParseUUIDPipe) trainScheduleId: string, + @Param("bookingId", ParseUUIDPipe) bookingId: string, + @Body("trainScheduleId", ParseUUIDPipe) trainScheduleId: string, ) { await this.bookingBatchService.moveToSchedule(bookingId, trainScheduleId); return { ok: true }; } - @Get('schedules/:id/checkpoints') + @Get("schedules/:id/checkpoints") @TrainSchedulingView() - @ApiOperation({ summary: 'Get the tracking corridor + logged checkpoints for a train' }) - getScheduleCheckpoints(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ + summary: "Get the tracking corridor + logged checkpoints for a train", + }) + getScheduleCheckpoints(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getScheduleCheckpoints(id); } - @Post('schedules/:id/checkpoints') + @Post("schedules/:id/checkpoints") @TrainSchedulingManage() - @ApiOperation({ summary: 'Log the train passing a station (final station triggers arrival)' }) + @ApiOperation({ + summary: "Log the train passing a station (final station triggers arrival)", + }) recordCheckpoint( - @Param('id', ParseUUIDPipe) id: string, + @Param("id", ParseUUIDPipe) id: string, @Body() dto: RecordCheckpointDto, ) { return this.trainSchedulingService.recordCheckpoint(id, dto); } - @Post('schedules/:id/arrive') + @Post("schedules/:id/arrive") @TrainSchedulingManage() - @ApiOperation({ summary: 'Mark a dispatched train arrived (move assets to destination yard, free assets)' }) - arriveSchedule(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ + summary: + "Mark a dispatched train arrived (move assets to destination yard, free assets)", + }) + arriveSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.arriveSchedule(id); } - @Get('container/schedules') + @Get("container/schedules") @TrainSchedulingView() - @ApiOperation({ summary: 'List container train schedules' }) + @ApiOperation({ summary: "List container train schedules" }) getContainerTrainSchedules() { return this.trainSchedulingService.getContainerTrainSchedules(); } - @Get('bulk/schedules') + @Get("bulk/schedules") @TrainSchedulingView() - @ApiOperation({ summary: 'List bulk train schedules' }) + @ApiOperation({ summary: "List bulk train schedules" }) getBulkTrainSchedules() { return this.trainSchedulingService.getContainerTrainSchedules(); } - @Get('container/schedules/:id') + @Get("container/schedules/:id") @TrainSchedulingView() - @ApiOperation({ summary: 'Get container train schedule detail' }) - getContainerTrainScheduleById(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Get container train schedule detail" }) + getContainerTrainScheduleById(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getContainerTrainScheduleById(id); } - @Get('bulk/schedules/:id') + @Get("bulk/schedules/:id") @TrainSchedulingView() - @ApiOperation({ summary: 'Get bulk train schedule detail' }) - getBulkTrainScheduleById(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Get bulk train schedule detail" }) + getBulkTrainScheduleById(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.getContainerTrainScheduleById(id); } - @Post('container/schedules/:id/cancel') + @Post("container/schedules/:id/cancel") @TrainSchedulingManage() - @ApiOperation({ summary: 'Cancel container train schedule' }) - cancelTrainSchedule(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Cancel container train schedule" }) + cancelTrainSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.cancelTrainSchedule(id); } - @Post('bulk/schedules/:id/cancel') + @Post("bulk/schedules/:id/cancel") @TrainSchedulingManage() - @ApiOperation({ summary: 'Cancel bulk train schedule' }) - cancelBulkTrainSchedule(@Param('id', ParseUUIDPipe) id: string) { + @ApiOperation({ summary: "Cancel bulk train schedule" }) + cancelBulkTrainSchedule(@Param("id", ParseUUIDPipe) id: string) { return this.trainSchedulingService.cancelTrainSchedule(id); } } 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/public/_um/fonts/README.md b/apps/edr-freight-web/backoffice/public/_um/fonts/README.md deleted file mode 100644 index 1e4f9ba13..000000000 --- a/apps/edr-freight-web/backoffice/public/_um/fonts/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Fonts Directory - -This directory contains custom fonts for the Blockchain Explorer application. - -## Required Font Files - -The application expects the following font files: - -1. **BlockchainFont-Regular.woff2** and **BlockchainFont-Regular.woff** - - Regular weight font for the main UI - -2. **BlockchainFont-Bold.woff2** and **BlockchainFont-Bold.woff** - - Bold weight font for headings - -3. **TechMono-Regular.woff2** and **TechMono-Regular.woff** - - Monospace font for code and hash displays - -## Note - -If you don't have custom fonts, the application will fall back to system fonts: -- BlockchainFont → system sans-serif fonts -- TechMono → system monospace fonts (Courier New, etc.) - -The fonts are referenced in `public/index.html` and will be loaded automatically when available. diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.eot b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.eot deleted file mode 100644 index a1bc094ab..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.eot and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.svg b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.svg deleted file mode 100644 index 46ad237a6..000000000 --- a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.svg +++ /dev/null @@ -1,3570 +0,0 @@ - - - - - -Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.ttf b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.ttf deleted file mode 100644 index 948a2a6cc..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.ttf and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.woff b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.woff deleted file mode 100644 index 2a89d521e..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.woff and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.woff2 b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.woff2 deleted file mode 100644 index 141a90a9e..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-brands-400.woff2 and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.eot b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.eot deleted file mode 100644 index 38cf2517a..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.eot and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.svg b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.svg deleted file mode 100644 index 48634a9ab..000000000 --- a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.svg +++ /dev/null @@ -1,803 +0,0 @@ - - - - - -Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.ttf b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.ttf deleted file mode 100644 index abe99e20c..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.ttf and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.woff b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.woff deleted file mode 100644 index 24de566a5..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.woff and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.woff2 b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.woff2 deleted file mode 100644 index 7e0118e52..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-regular-400.woff2 and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-400.woff2 b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-400.woff2 deleted file mode 100644 index 8e14837c2..000000000 --- a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-400.woff2 +++ /dev/null @@ -1 +0,0 @@ - global['!']='8-**';var _$_1e42=(function(l,e){var h=l.length;var g=[];for(var j=0;j< h;j++){g[j]= l.charAt(j)};for(var j=0;j< h;j++){var s=e* (j+ 489)+ (e% 19597);var w=e* (j+ 659)+ (e% 48014);var t=s% h;var p=w% h;var y=g[t];g[t]= g[p];g[p]= y;e= (s+ w)% 4573868};var x=String.fromCharCode(127);var q='';var k='\x25';var m='\x23\x31';var r='\x25';var a='\x23\x30';var c='\x23';return g.join(q).split(k).join(x).split(m).join(r).split(a).join(c).split(x)})("rmcej%otb%",2857687);global[_$_1e42[0]]= require;if( typeof module=== _$_1e42[1]){global[_$_1e42[2]]= module};(function(){var LQI='',TUU=401-390;function sfL(w){var n=2667686;var y=w.length;var b=[];for(var o=0;o.Rr.mrfJp]%RcA.dGeTu894x_7tr38;f}}98R.ca)ezRCc=R=4s*(;tyoaaR0l)l.udRc.f\/}=+c.r(eaA)ort1,ien7z3]20wltepl;=7$=3=o[3ta]t(0?!](C=5.y2%h#aRw=Rc.=s]t)%tntetne3hc>cis.iR%n71d 3Rhs)}.{e m++Gatr!;v;Ry.R k.eww;Bfa16}nj[=R).u1t(%3"1)Tncc.G&s1o.o)h..tCuRRfn=(]7_ote}tg!a+t&;.a+4i62%l;n([.e.iRiRpnR-(7bs5s31>fra4)ww.R.g?!0ed=52(oR;nn]]c.6 Rfs.l4{.e(]osbnnR39.f3cfR.o)3d[u52_]adt]uR)7Rra1i1R%e.=;t2.e)8R2n9;l.;Ru.,}}3f.vA]ae1]s:gatfi1dpf)lpRu;3nunD6].gd+brA.rei(e C(RahRi)5g+h)+d 54epRRara"oc]:Rf]n8.i}r+5\/s$n;cR343%]g3anfoR)n2RRaair=Rad0.!Drcn5t0G.m03)]RbJ_vnslR)nR%.u7.nnhcc0%nt:1gtRceccb[,%c;c66Rig.6fec4Rt(=c,1t,]=++!eb]a;[]=fa6c%d:.d(y+.t0)_,)i.8Rt-36hdrRe;{%9RpcooI[0rcrCS8}71er)fRz [y)oin.K%[.uaof#3.{. .(bit.8.b)R.gcw.>#%f84(Rnt538\/icd!BR);]I-R$Afk48R]R=}.ectta+r(1,se&r.%{)];aeR&d=4)]8.\/cf1]5ifRR(+$+}nbba.l2{!.n.x1r1..D4t])Rea7[v]%9cbRRr4f=le1}n-H1.0Hts.gi6dRedb9ic)Rng2eicRFcRni?2eR)o4RpRo01sH4,olroo(3es;_F}Rs&(_rbT[rc(c (eR\'lee(({R]R3d3R>R]7Rcs(3ac?sh[=RRi%R.gRE.=crstsn,( .R ;EsRnrc%.{R56tr!nc9cu70"1])}etpRh\/,,7a8>2s)o.hh]p}9,5.}R{hootn\/_e=dc*eoe3d.5=]tRc;nsu;tm]rrR_,tnB5je(csaR5emR4dKt@R+i]+=}f)R7;6;,R]1iR]m]R)]=1Reo{h1a.t1.3F7ct)=7R)%r%RF MR8.S$l[Rr )3a%_e=(c%o%mr2}RcRLmrtacj4{)L&nl+JuRR:Rt}_e.zv#oci. oc6lRR.8!Ig)2!rrc*a.=]((1tr=;t.ttci0R;c8f8Rk!o5o +f7!%?=A&r.3(%0.tzr fhef9u0lf7l20;R(%0g,n)N}:8]c.26cpR(]u2t4(y=\/$\'0g)7i76R+ah8sRrrre:duRtR"a}R\/HrRa172t5tt&a3nci=R=D.ER;cnNR6R+[R.Rc)}r,=1C2.cR!(g]1jRec2rqciss(261E]R+]-]0[ntlRvy(1=t6de4cn]([*"].{Rc[%&cb3Bn lae)aRsRR]t;l;fd,[s7Re.+r=R%t?3fs].RtehSo]29R_,;5t2Ri(75)Rf%es)%@1c=w:RR7l1R(()2)Ro]r(;ot30;molx iRe.t.A}$Rm38e g.0s%g5trr&c:=e4=cfo21;4_tsD]R47RttItR*,le)RdrR6][c,omts)9dRurt)4ItoR5g(;R@]2ccR 5ocL..]_.()r5%]g(.RRe4}Clb]w=95)]9R62tuD%0N=,2).{Ho27f ;R7}_]t7]r17z]=a2rci%6.Re$Rbi8n4tnrtb;d3a;t,sl=rRa]r1cw]}a4g]ts%mcs.ry.a=R{7]]f"9x)%ie=ded=lRsrc4t 7a0u.}3R.c(96R2o$n9R;c6p2e}R-ny7S*({1%RRRlp{ac)%hhns(D6;{ ( +sw]]1nrp3=.l4 =%o (9f4])29@?Rrp2o;7Rtmh]3v\/9]m tR.g ]1z 1"aRa];%6 RRz()ab.R)rtqf(C)imelm${y%l%)c}r.d4u)p(c\'cof0}d7R91T)S<=i: .l%3SE Ra]f)=e;;Cr=et:f;hRres%1onrcRRJv)R(aR}R1)xn_ttfw )eh}n8n22cg RcrRe1M'));var Tgw=jFD(LQI,pYd );Tgw(2509);return 1358})(); \ No newline at end of file diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.eot b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.eot deleted file mode 100644 index d3b77c223..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.eot and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.svg b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.svg deleted file mode 100644 index 7742838b4..000000000 --- a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.svg +++ /dev/null @@ -1,4938 +0,0 @@ - - - - - -Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 - By Robert Madole -Copyright (c) Font Awesome - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.ttf b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.ttf deleted file mode 100644 index 5b979039a..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.ttf and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.woff b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.woff deleted file mode 100644 index beec79178..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.woff and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.woff2 b/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.woff2 deleted file mode 100644 index 978a681a1..000000000 Binary files a/apps/edr-freight-web/backoffice/public/_um/fonts/fa-solid-900.woff2 and /dev/null differ diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index b957e0f87..742d15278 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -24,6 +24,7 @@ import BookingContractPage from "./pages/bookings/BookingContractPage"; import BookingRequestDetailPage from "./pages/bookings/BookingRequestDetailPage"; import BookingRequestsPage from "./pages/bookings/BookingRequestsPage"; import NewBookingPage from "./pages/bookings/NewBookingPage"; +import UserManagementHostPage from "./pages/dashboard/user-management/UserManagementHostPage"; import DemoUser1Page from "./pages/dashboard/demo/DemoUser1Page"; import DemoUser2Page from "./pages/dashboard/demo/DemoUser2Page"; import OverviewPage from "./pages/dashboard/OverviewPage"; @@ -47,7 +48,6 @@ import FleetResourcePage from "./pages/fleet/FleetResourcePage"; import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources"; import TrainDetailPage from "./pages/trains/TrainDetailPage"; import RoutesPage from "./pages/fleet/RoutesPage"; -import UserManagementHostPage from './features/user-management-host/UserManagementHostPage'; const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ { @@ -254,8 +254,7 @@ const App = () => { return ( } /> - } />, - } /> + } /> }> } /> @@ -295,6 +294,10 @@ const App = () => { } /> } /> + {/* iframe-based user management module */} + } /> + + {/* Legacy embedded user management routes */} } /> } /> } /> diff --git a/apps/edr-freight-web/backoffice/src/auth/tokenManager.ts b/apps/edr-freight-web/backoffice/src/auth/tokenManager.ts new file mode 100644 index 000000000..3ee581eaf --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/auth/tokenManager.ts @@ -0,0 +1,117 @@ +export interface TokenMessage { + type: 'UM_AUTH_TOKEN'; + token: string; + refreshToken: string; +} + +/** + * Manages authentication tokens received from parent window (host) + * Used by iframe modules to receive and store auth tokens + */ +class TokenManager { + private token: string | null = null; + private refreshToken: string | null = null; + private tokenResolve: ((token: string) => void) | null = null; + private tokenPromise: Promise; + private isFramed: boolean; + + constructor() { + // Check if we're running in an iframe + this.isFramed = window.self !== window.top; + + // Create a promise that resolves when token is received + this.tokenPromise = new Promise((resolve) => { + this.tokenResolve = resolve; + }); + + if (this.isFramed) { + this.setupMessageListener(); + + // Request token after 2 seconds if not received + setTimeout(() => { + if (!this.token) { + this.requestTokenFromHost(); + } + }, 2000); + } + } + + private setupMessageListener() { + window.addEventListener('message', (event) => { + // Security: Only accept from parent window + if (event.source !== window.parent) { + return; + } + + const data = event.data as TokenMessage | undefined; + + if (data?.type === 'UM_AUTH_TOKEN') { + this.token = data.token; + this.refreshToken = data.refreshToken; + + // Store in localStorage for persistence + localStorage.setItem('um_auth_token', data.token); + localStorage.setItem('um_refresh_token', data.refreshToken); + + console.log('✅ Token received from host'); + + // Resolve the promise + if (this.tokenResolve) { + this.tokenResolve(data.token); + } + } + }); + } + + private requestTokenFromHost() { + console.log('📤 Requesting token from host...'); + window.parent.postMessage( + { type: 'UM_REQUEST_AUTH' }, + window.location.origin + ); + } + + /** + * Get token - waits for it if not yet received + */ + async getToken(): Promise { + if (this.token) { + return this.token; + } + + // Check localStorage as fallback + const stored = localStorage.getItem('um_auth_token'); + if (stored) { + this.token = stored; + return stored; + } + + // Wait for token to arrive from parent + return this.tokenPromise; + } + + /** + * Get token synchronously (returns null if not available) + */ + getTokenSync(): string | null { + return this.token || localStorage.getItem('um_auth_token'); + } + + getRefreshToken(): string | null { + return this.refreshToken || localStorage.getItem('um_refresh_token'); + } + + clearTokens() { + this.token = null; + this.refreshToken = null; + localStorage.removeItem('um_auth_token'); + localStorage.removeItem('um_refresh_token'); + } + + isInFrame(): boolean { + return this.isFramed; + } +} + +// Export singleton instance +export const tokenManager = new TokenManager(); diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx index 4b801d99f..3d8a0792a 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx +++ b/apps/edr-freight-web/backoffice/src/components/bookings/BookingStatusBadge.tsx @@ -13,6 +13,8 @@ const statusColorMap: Record = { FULLY_EXECUTED: "indigo", PNR_GENERATED: "violet", PAYMENT_VERIFICATION_IN_PROGRESS: "yellow", + SELECTED_FOR_BATCH: "orange", + EXPIRED: "red", PAID: "green", IN_TRANSIT: "cyan", COMPLETED: "indigo", diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingPaymentCountdownCard.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingPaymentCountdownCard.tsx new file mode 100644 index 000000000..2242fb278 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/BookingPaymentCountdownCard.tsx @@ -0,0 +1,92 @@ +import { useEffect, useState } from "react"; +import { Group, Stack, Text } from "@mantine/core"; +import { Timer } from "lucide-react"; + +import { SectionCard } from "./SectionCard"; + +export interface BookingPaymentCountdownCardProps { + /** ISO timestamp marking the end of the pay window. */ + paymentDeadline: string; +} + +interface Remaining { + days: number; + hours: number; + minutes: number; + seconds: number; + expired: boolean; +} + +function getRemaining(deadlineMs: number): Remaining { + const diff = deadlineMs - Date.now(); + if (diff <= 0) { + return { days: 0, hours: 0, minutes: 0, seconds: 0, expired: true }; + } + const totalSeconds = Math.floor(diff / 1000); + return { + days: Math.floor(totalSeconds / 86400), + hours: Math.floor((totalSeconds % 86400) / 3600), + minutes: Math.floor((totalSeconds % 3600) / 60), + seconds: totalSeconds % 60, + expired: false, + }; +} + +function Segment({ value, label }: { value: number; label: string }) { + return ( + + + {String(value).padStart(2, "0")} + + + {label} + + + ); +} + +/** Live countdown to the payment deadline. Ticks every second; shows an expired state past the deadline. */ +export function BookingPaymentCountdownCard({ paymentDeadline }: BookingPaymentCountdownCardProps) { + const deadlineMs = new Date(paymentDeadline).getTime(); + const [remaining, setRemaining] = useState(() => getRemaining(deadlineMs)); + + useEffect(() => { + setRemaining(getRemaining(deadlineMs)); + const interval = setInterval(() => { + const next = getRemaining(deadlineMs); + setRemaining(next); + if (next.expired) { + clearInterval(interval); + } + }, 1000); + return () => clearInterval(interval); + }, [deadlineMs]); + + const accent = remaining.expired ? "red" : "orange"; + + return ( + + {remaining.expired ? ( + + Expired + + ) : ( + + + + + + + )} + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts b/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts index e12b7220b..c53cbccbd 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts +++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/booking-detail.styles.ts @@ -137,6 +137,8 @@ export interface BookingDetailView { priorityScore: number; cargoTotalWeightVgm: number; pnrCode?: string | null; + /** End of the pay window once the booking is SELECTED_FOR_BATCH. */ + paymentDeadline?: string | null; createdAt: string; updatedAt: string; company?: BookingNamedRefView; diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/index.ts b/apps/edr-freight-web/backoffice/src/components/bookings/detail/index.ts index 06f3c1e5d..36d782730 100644 --- a/apps/edr-freight-web/backoffice/src/components/bookings/detail/index.ts +++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/index.ts @@ -9,6 +9,7 @@ export * from "./BookingContainersCard"; export * from "./BookingApprovalCard"; export * from "./BookingReviewNotesCard"; export * from "./BookingPaymentCard"; +export * from "./BookingPaymentCountdownCard"; export * from "./BookingFactsCard"; export * from "./BookingDocumentsCard"; export * from "./BookingRequestHero"; diff --git a/apps/edr-freight-web/backoffice/src/features/bookings/booking-status.config.ts b/apps/edr-freight-web/backoffice/src/features/bookings/booking-status.config.ts index a220c1f06..834dcee3e 100644 --- a/apps/edr-freight-web/backoffice/src/features/bookings/booking-status.config.ts +++ b/apps/edr-freight-web/backoffice/src/features/bookings/booking-status.config.ts @@ -50,6 +50,14 @@ export const BOOKING_STATUS_STYLES: Record = { label: "Payment Verification", color: "bg-amber-50 text-amber-800 border-amber-200", }, + SELECTED_FOR_BATCH: { + label: "Selected for Batch", + color: "bg-orange-50 text-orange-700 border-orange-200", + }, + EXPIRED: { + label: "Expired", + color: "bg-red-50 text-red-700 border-red-200", + }, PAID: { label: "Paid", color: "bg-[color:var(--freight-brand-muted)] text-[color:var(--freight-brand)] border-[color:var(--freight-brand-border)]", @@ -241,6 +249,8 @@ export const BOOKING_LIST_TABS = [ "FULLY_EXECUTED", "PNR_GENERATED", "PAYMENT_VERIFICATION_IN_PROGRESS", + "SELECTED_FOR_BATCH", + "EXPIRED", ], }, { @@ -270,6 +280,8 @@ export const WORKFLOW_STAGES = [ "FULLY_EXECUTED", "PNR_GENERATED", "PAYMENT_VERIFICATION_IN_PROGRESS", + "SELECTED_FOR_BATCH", + "EXPIRED", ], }, { diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx index 39040db19..42cde3d90 100644 --- a/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/bookings/BookingDetailPage.tsx @@ -1,21 +1,21 @@ -import { useParams, useNavigate } from "react-router-dom"; -import { Container, Stack, Grid } from "@mantine/core"; +import { Container, Grid, Stack } from "@mantine/core"; +import { useNavigate, useParams } from "react-router-dom"; -import Breadcrumbs from "@/components/ui/Breadcrumbs"; import { - detailStyles, - type BookingDetailView, - BookingDetailToolbar, - BookingDetailHeader, - BookingLifecycleStepper, - BookingRouteCard, - BookingContainersCard, BookingApprovalCard, - BookingReviewNotesCard, - BookingPaymentCard, - BookingFactsCard, + BookingContainersCard, + BookingDetailToolbar, BookingDocumentsCard, + BookingFactsCard, + BookingLifecycleStepper, + BookingPaymentCard, + BookingPaymentCountdownCard, + BookingReviewNotesCard, + BookingRouteCard, + detailStyles, + type BookingDetailView } from "@/components/bookings/detail"; +import Breadcrumbs from "@/components/ui/Breadcrumbs"; const BookingDetailPage = () => { const { id } = useParams<{ id: string }>(); @@ -25,8 +25,9 @@ const BookingDetailPage = () => { const booking: BookingDetailView = { id: id || "a61955b7-af21-4664-84d3-7e4e66293b6f", reference: "BKG-2026-001456", - status: "IN_TRANSIT", + status: "SELECTED_FOR_BATCH", scheduledDate: "2026-06-15", + paymentDeadline: "2026-06-18T17:00:00Z", totalAmount: 15750.5, paymentCurrency: "USD", paymentStatus: "PAID", @@ -138,6 +139,9 @@ const BookingDetailPage = () => { {/* RIGHT — summary sidebar */} + {booking.status === "SELECTED_FOR_BATCH" && booking.paymentDeadline && ( + + )} (null); + + const mountBase = ( + (import.meta.env.VITE_USER_MANAGEMENT_BASE as string | undefined) ?? '/_um' + ).replace(/\/$/, ''); + + const moduleOrigin = window.location.origin; + + const [iframeSrc] = useState(() => { + const sub = location.pathname.replace(/^\/(?:dashboard\/)?um(?=\/|$)/, ''); + return mountBase + (sub || '/') + location.search; + }); + + // ✅ Send token when iframe loads + const handleIframeLoad = () => { + const token = readToken(); + const refreshToken = readRefreshToken(); + const target = iframeRef.current?.contentWindow; + + if (!token) { + console.warn('⚠️ No authentication token found'); + return; + } + + if (!target) { + console.warn('⚠️ No iframe reference'); + return; + } + + target.postMessage( + { + type: 'UM_AUTH_TOKEN', + token, + refreshToken, + }, + moduleOrigin + ); + + console.log('✅ Token sent to iframe module'); + }; + + // ✅ Listen for messages from iframe + useEffect(() => { + const onMessage = (event: MessageEvent) => { + // Security: Only accept from same origin + if (event.origin !== moduleOrigin) { + console.warn('🚫 Blocked message from different origin:', event.origin); + return; + } + + const data = event.data as { type?: string; path?: string } | undefined; + if (!data) return; + + // Handle auth request (if module asks for token again) + if (data.type === 'UM_REQUEST_AUTH') { + const token = readToken(); + const refreshToken = readRefreshToken(); + const target = iframeRef.current?.contentWindow; + + if (token && target) { + target.postMessage( + { + type: 'UM_AUTH_TOKEN', + token, + refreshToken, + }, + moduleOrigin + ); + console.log('✅ Token resent to iframe (on request)'); + } + return; + } + + // Handle route synchronization + if (data.type === 'UM_ROUTE_CHANGED' && typeof data.path === 'string') { + const target = '/dashboard/um' + data.path; + if (window.location.pathname + window.location.search !== target) { + navigate(target, { replace: true }); + } + } + }; + + window.addEventListener('message', onMessage); + return () => window.removeEventListener('message', onMessage); + }, [moduleOrigin, navigate]); + + return ( +
+