From 648f4f2ee4f5ef7d36300c1bda97c7f80c915141 Mon Sep 17 00:00:00 2001 From: Marshal Date: Wed, 17 Jun 2026 01:56:56 +0000 Subject: [PATCH] Add fleet management and admin permissions, and update controllers to enforce new permissions --- .../src/common/booking-guards.ts | 7 ++ .../backoffice/backoffice.controller.ts | 2 + .../src/modules/billing/billing.controller.ts | 2 + .../src/modules/cargoes/cargoes.controller.ts | 8 ++ .../modules/companies/companies.controller.ts | 7 ++ .../consignments/consignments.controller.ts | 3 + .../containers.controller.ts | 7 ++ .../modules/customers/customers.controller.ts | 2 + .../dropdown-settings.controller.ts | 11 +++ .../file-upload-settings.controller.ts | 11 +++ .../locomotives/locomotives.controller.ts | 5 ++ .../src/modules/payment/payment.controller.ts | 3 + .../src/modules/routes/routes.controller.ts | 5 ++ .../src/modules/trains/trains.controller.ts | 5 ++ .../src/modules/wagons/wagons.controller.ts | 9 ++ .../src/seed/freight-permissions.registry.ts | 13 ++- apps/edr-freight-web/backoffice/src/App.tsx | 87 +++++++++++++++++-- .../backoffice/src/lib/permissions.ts | 13 +++ 18 files changed, 190 insertions(+), 10 deletions(-) diff --git a/apps/edr-freight-api/src/common/booking-guards.ts b/apps/edr-freight-api/src/common/booking-guards.ts index 393a97f9f..8d55f1dc4 100644 --- a/apps/edr-freight-api/src/common/booking-guards.ts +++ b/apps/edr-freight-api/src/common/booking-guards.ts @@ -21,3 +21,10 @@ export const TrainSchedulingView = () => export const TrainSchedulingManage = () => BookingStaff(FREIGHT_PERMS.trainScheduling.manage); + +export const FleetView = () => BookingStaff(FREIGHT_PERMS.fleet.view); + +export const FleetManage = () => BookingStaff(FREIGHT_PERMS.fleet.manage); + +/** Org-administration endpoints (user mgmt, billing config, company CRUD, settings). */ +export const FreightAdmin = () => BookingStaff(FREIGHT_PERMS.admin); diff --git a/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts b/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts index 395ff0386..b3305ba68 100644 --- a/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts +++ b/apps/edr-freight-api/src/modules/backoffice/backoffice.controller.ts @@ -10,12 +10,14 @@ import { } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { FreightAdmin } from "../../common/booking-guards"; import { BackofficeService } from "./backoffice.service"; import { CreateOrganizationUserDto } from "./dto/create-organization-user.dto"; import { UpdateEmployeeUserRolesDto } from "./dto/update-employee-user-roles.dto"; @ApiTags("backoffice") @Controller("backoffice") +@FreightAdmin() export class BackofficeController { constructor(private readonly backofficeService: BackofficeService) {} diff --git a/apps/edr-freight-api/src/modules/billing/billing.controller.ts b/apps/edr-freight-api/src/modules/billing/billing.controller.ts index 0091b5341..5a801cf73 100644 --- a/apps/edr-freight-api/src/modules/billing/billing.controller.ts +++ b/apps/edr-freight-api/src/modules/billing/billing.controller.ts @@ -1,10 +1,12 @@ import { Controller, Get, Param, ParseUUIDPipe } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { FreightAdmin } from "../../common/booking-guards"; import { BillingService } from "./billing.service"; @ApiTags("billing") @Controller("billing") +@FreightAdmin() export class BillingController { constructor(private readonly billingService: BillingService) {} diff --git a/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts b/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts index b0babb06f..7f3f06ec2 100644 --- a/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts +++ b/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts @@ -10,6 +10,7 @@ import { Query, } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { FleetManage, FleetView } from '../../common/booking-guards'; import { CreateCargoDto } from './dto/create-cargo.dto'; import { UpdateCargoDto } from './dto/update-cargo.dto'; import { LoadCargoDto } from './dto/load-cargo.dto'; @@ -18,10 +19,12 @@ import { CargoesService } from './cargoes.service'; @ApiTags('cargoes') @Controller('cargoes') +@FleetView() export class CargoesController { constructor(private readonly cargoesService: CargoesService) {} @Post() + @FleetManage() @ApiOperation({ summary: 'Create a new cargo' }) create(@Body() dto: CreateCargoDto) { return this.cargoesService.create(dto); @@ -40,30 +43,35 @@ export class CargoesController { } @Patch(':id') + @FleetManage() @ApiOperation({ summary: 'Update a cargo' }) update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateCargoDto) { return this.cargoesService.update(id, dto); } @Delete(':id') + @FleetManage() @ApiOperation({ summary: 'Delete a cargo' }) remove(@Param('id', ParseUUIDPipe) id: string) { return this.cargoesService.remove(id); } @Post(':id/load') + @FleetManage() @ApiOperation({ summary: 'Load cargo into a container' }) load(@Param('id', ParseUUIDPipe) id: string, @Body() dto: LoadCargoDto) { return this.cargoesService.loadCargo(id, dto); } @Post(':id/unload') + @FleetManage() @ApiOperation({ summary: 'Unload cargo from container' }) unload(@Param('id', ParseUUIDPipe) id: string) { return this.cargoesService.unloadCargo(id); } @Post(':id/deliver') + @FleetManage() @ApiOperation({ summary: 'Mark cargo as delivered' }) deliver(@Param('id', ParseUUIDPipe) id: string, @Body() dto?: DeliverCargoDto) { return this.cargoesService.deliverCargo(id, dto); diff --git a/apps/edr-freight-api/src/modules/companies/companies.controller.ts b/apps/edr-freight-api/src/modules/companies/companies.controller.ts index 5b38c4d65..81fba19fb 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Post, Patch, Delete, Body, Param, Query, ParseUUIDPipe import { AnyFilesInterceptor } from '@nestjs/platform-express'; import { ApiOperation, ApiTags, ApiConsumes } from '@nestjs/swagger'; import { CurrentUser } from '@edr/api-common'; +import { FreightAdmin } from '../../common/booking-guards'; import { FilesService } from '../files/files.service'; import { CompaniesService } from './companies.service'; import { CreateCompanyDto } from './dto/create-company.dto'; @@ -82,6 +83,7 @@ export class CompaniesController { } @Post() + @FreightAdmin() @ApiOperation({ summary: 'Create a new company (customer, forwarder, transporter, broker)' }) async create(@Body() dto: CreateCompanyDto): Promise { const company = await this.companiesService.createCompany(dto); @@ -119,6 +121,7 @@ export class CompaniesController { } @Patch(':id') + @FreightAdmin() @ApiOperation({ summary: 'Update a company' }) async update( @Param('id', ParseUUIDPipe) id: string, @@ -129,6 +132,7 @@ export class CompaniesController { } @Delete(':id') + @FreightAdmin() @ApiOperation({ summary: 'Soft-delete a company' }) @HttpCode(HttpStatus.NO_CONTENT) async remove(@Param('id', ParseUUIDPipe) id: string): Promise { @@ -147,6 +151,7 @@ export class CompaniesController { } @Post(':companyId/profiles') + @FreightAdmin() @ApiOperation({ summary: 'Add a profile (employee) to a company' }) async createProfile( @Param('companyId', ParseUUIDPipe) companyId: string, @@ -175,6 +180,7 @@ export class CompaniesController { } @Post('ff-clients') + @FreightAdmin() @ApiOperation({ summary: 'Link a forwarder to a client company' }) async createFFClient(@Body() dto: CreateFFClientDto): Promise { const client = await this.companiesService.createFFClient(dto); @@ -191,6 +197,7 @@ export class CompaniesController { } @Delete('ff-clients/:id') + @FreightAdmin() @ApiOperation({ summary: 'Remove a forwarder-client relationship' }) @HttpCode(HttpStatus.NO_CONTENT) async removeFFClient(@Param('id', ParseUUIDPipe) id: string): Promise { diff --git a/apps/edr-freight-api/src/modules/consignments/consignments.controller.ts b/apps/edr-freight-api/src/modules/consignments/consignments.controller.ts index 46ef22bf8..b107e8935 100644 --- a/apps/edr-freight-api/src/modules/consignments/consignments.controller.ts +++ b/apps/edr-freight-api/src/modules/consignments/consignments.controller.ts @@ -9,16 +9,19 @@ import { } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { FleetManage, FleetView } from "../../common/booking-guards"; import { ConsignmentsService } from "./consignments.service"; import { CreateConsignmentDto } from "./dto/create-consignment.dto"; import { FilterConsignmentDto } from "./dto/filter-consignment.dto"; @ApiTags("consignments") @Controller("consignments") +@FleetView() export class ConsignmentsController { constructor(private readonly consignmentsService: ConsignmentsService) {} @Post() + @FleetManage() @ApiOperation({ summary: "Create a new consignment" }) create(@Body() dto: CreateConsignmentDto) { return this.consignmentsService.create(dto); diff --git a/apps/edr-freight-api/src/modules/container-management/containers.controller.ts b/apps/edr-freight-api/src/modules/container-management/containers.controller.ts index efffb075e..1a0cdb14f 100644 --- a/apps/edr-freight-api/src/modules/container-management/containers.controller.ts +++ b/apps/edr-freight-api/src/modules/container-management/containers.controller.ts @@ -10,6 +10,7 @@ import { Query, } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { FleetManage, FleetView } from '../../common/booking-guards'; import { CreateContainerDto } from './dto/create-container.dto'; import { UpdateContainerDto } from './dto/update-container.dto'; import { AssignContainerToWagonDto } from './dto/assign-container-to-wagon.dto'; @@ -17,10 +18,12 @@ import { ContainersService } from './containers.service'; @ApiTags('containers') @Controller('containers') +@FleetView() export class ContainersController { constructor(private readonly containersService: ContainersService) {} @Post() + @FleetManage() @ApiOperation({ summary: 'Create a new container' }) create(@Body() dto: CreateContainerDto) { return this.containersService.create(dto); @@ -39,24 +42,28 @@ export class ContainersController { } @Patch(':id') + @FleetManage() @ApiOperation({ summary: 'Update a container' }) update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateContainerDto) { return this.containersService.update(id, dto); } @Delete(':id') + @FleetManage() @ApiOperation({ summary: 'Delete a container' }) remove(@Param('id', ParseUUIDPipe) id: string) { return this.containersService.remove(id); } @Post(':id/assign-wagon') + @FleetManage() @ApiOperation({ summary: 'Assign container to a wagon' }) assignToWagon(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignContainerToWagonDto) { return this.containersService.assignToWagon(id, dto); } @Post(':id/unassign-wagon') + @FleetManage() @ApiOperation({ summary: 'Unassign container from wagon' }) unassignFromWagon(@Param('id', ParseUUIDPipe) id: string) { return this.containersService.unassignFromWagon(id); diff --git a/apps/edr-freight-api/src/modules/customers/customers.controller.ts b/apps/edr-freight-api/src/modules/customers/customers.controller.ts index 404e4d27b..7451bd6b6 100644 --- a/apps/edr-freight-api/src/modules/customers/customers.controller.ts +++ b/apps/edr-freight-api/src/modules/customers/customers.controller.ts @@ -16,12 +16,14 @@ import { import { ApiOperation } from "@nestjs/swagger"; +import { FreightAdmin } from "../../common/booking-guards"; import { CustomersService } from "./customers.service"; import { CreateCustomerDto } from "./dto/create-customer.dto"; import { UpdateCustomerDto } from "./dto/update-customer.dto"; import { Customer } from "./entities/customer.entity"; @Controller("customers") +@FreightAdmin() export class CustomersController { constructor(private readonly customersService: CustomersService) {} diff --git a/apps/edr-freight-api/src/modules/dropdown-settings/dropdown-settings.controller.ts b/apps/edr-freight-api/src/modules/dropdown-settings/dropdown-settings.controller.ts index e8c3fbba0..7a63964d8 100644 --- a/apps/edr-freight-api/src/modules/dropdown-settings/dropdown-settings.controller.ts +++ b/apps/edr-freight-api/src/modules/dropdown-settings/dropdown-settings.controller.ts @@ -13,6 +13,7 @@ import { } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { FreightAdmin } from "../../common/booking-guards"; import { CreateDropdownOptionDto } from "./dto/create-dropdown-option.dto"; import { CreateDropdownSettingDto } from "./dto/create-dropdown-setting.dto"; import { UpdateDropdownOptionDto } from "./dto/update-dropdown-option.dto"; @@ -24,6 +25,9 @@ import { DropdownSettingsService } from "./dropdown-settings.service"; export class DropdownSettingsController { constructor(private readonly service: DropdownSettingsService) {} + // Reads stay open: the customer portal fetches these to render dynamic + // dropdowns (by-code). Only writes are admin-guarded. + @Get() @ApiOperation({ summary: "List all dropdown settings" }) list() { @@ -43,12 +47,14 @@ export class DropdownSettingsController { } @Post() + @FreightAdmin() @ApiOperation({ summary: "Create a new dropdown setting" }) create(@Body() dto: CreateDropdownSettingDto) { return this.service.create(dto); } @Patch(":id") + @FreightAdmin() @ApiOperation({ summary: "Update a dropdown setting's metadata" }) update( @Param("id", ParseUUIDPipe) id: string, @@ -58,6 +64,7 @@ export class DropdownSettingsController { } @Delete(":id") + @FreightAdmin() @ApiOperation({ summary: "Soft-delete a dropdown setting" }) @HttpCode(HttpStatus.NO_CONTENT) remove(@Param("id", ParseUUIDPipe) id: string) { @@ -67,6 +74,7 @@ export class DropdownSettingsController { /* ------------------------- option routes ------------------------- */ @Put(":id/options") + @FreightAdmin() @ApiOperation({ summary: "Replace the full option list for a setting" }) replaceOptions( @Param("id", ParseUUIDPipe) id: string, @@ -76,6 +84,7 @@ export class DropdownSettingsController { } @Post(":id/options") + @FreightAdmin() @ApiOperation({ summary: "Append a single option to a setting" }) addOption( @Param("id", ParseUUIDPipe) id: string, @@ -85,6 +94,7 @@ export class DropdownSettingsController { } @Patch("options/:optionId") + @FreightAdmin() @ApiOperation({ summary: "Update a single option" }) updateOption( @Param("optionId", ParseUUIDPipe) optionId: string, @@ -94,6 +104,7 @@ export class DropdownSettingsController { } @Delete("options/:optionId") + @FreightAdmin() @ApiOperation({ summary: "Soft-delete a single option" }) @HttpCode(HttpStatus.NO_CONTENT) removeOption(@Param("optionId", ParseUUIDPipe) optionId: string) { diff --git a/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.controller.ts b/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.controller.ts index ecdecffc3..661339902 100644 --- a/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.controller.ts +++ b/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.controller.ts @@ -13,6 +13,7 @@ import { } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { FreightAdmin } from "../../common/booking-guards"; import { CreateFileUploadFieldDto } from "./dto/create-file-upload-field.dto"; import { CreateFileUploadSettingDto } from "./dto/create-file-upload-setting.dto"; import { UpdateFileUploadFieldDto } from "./dto/update-file-upload-field.dto"; @@ -24,6 +25,9 @@ import { FileUploadSettingsService } from "./file-upload-settings.service"; export class FileUploadSettingsController { constructor(private readonly service: FileUploadSettingsService) {} + // Reads stay open: the customer portal fetches these to render dynamic + // upload forms (by-code / by-entity). Only writes are admin-guarded. + @Get() @ApiOperation({ summary: "List all file upload settings" }) list() { @@ -49,12 +53,14 @@ export class FileUploadSettingsController { } @Post() + @FreightAdmin() @ApiOperation({ summary: "Create a new file upload setting" }) create(@Body() dto: CreateFileUploadSettingDto) { return this.service.create(dto); } @Patch(":id") + @FreightAdmin() @ApiOperation({ summary: "Update a file upload setting's metadata" }) update( @Param("id", ParseUUIDPipe) id: string, @@ -64,6 +70,7 @@ export class FileUploadSettingsController { } @Delete(":id") + @FreightAdmin() @ApiOperation({ summary: "Soft-delete a file upload setting" }) @HttpCode(HttpStatus.NO_CONTENT) remove(@Param("id", ParseUUIDPipe) id: string) { @@ -73,6 +80,7 @@ export class FileUploadSettingsController { /* ------------------------- field routes ------------------------- */ @Put(":id/fields") + @FreightAdmin() @ApiOperation({ summary: "Replace the full field list for a setting" }) replaceFields( @Param("id", ParseUUIDPipe) id: string, @@ -82,6 +90,7 @@ export class FileUploadSettingsController { } @Post(":id/fields") + @FreightAdmin() @ApiOperation({ summary: "Append a single field to a setting" }) addField( @Param("id", ParseUUIDPipe) id: string, @@ -91,6 +100,7 @@ export class FileUploadSettingsController { } @Patch("fields/:fieldId") + @FreightAdmin() @ApiOperation({ summary: "Update a single field" }) updateField( @Param("fieldId", ParseUUIDPipe) fieldId: string, @@ -100,6 +110,7 @@ export class FileUploadSettingsController { } @Delete("fields/:fieldId") + @FreightAdmin() @ApiOperation({ summary: "Soft-delete a single field" }) @HttpCode(HttpStatus.NO_CONTENT) removeField(@Param("fieldId", ParseUUIDPipe) fieldId: string) { diff --git a/apps/edr-freight-api/src/modules/locomotives/locomotives.controller.ts b/apps/edr-freight-api/src/modules/locomotives/locomotives.controller.ts index f7ccdde1d..c907af717 100644 --- a/apps/edr-freight-api/src/modules/locomotives/locomotives.controller.ts +++ b/apps/edr-freight-api/src/modules/locomotives/locomotives.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { FleetManage, FleetView } from '../../common/booking-guards'; import { CreateLocomotiveDto } from './dto/create-locomotive.dto'; import { FilterLocomotivesDto } from './dto/filter-locomotives.dto'; import { UpdateLocomotiveDto } from './dto/update-locomotive.dto'; @@ -9,6 +10,7 @@ import { LocomotivesService } from './locomotives.service'; @ApiTags('locomotives') @ApiBearerAuth() @Controller('locomotives') +@FleetView() export class LocomotivesController { constructor(private readonly locomotivesService: LocomotivesService) {} @@ -25,18 +27,21 @@ export class LocomotivesController { } @Post() + @FleetManage() @ApiOperation({ summary: 'Create a locomotive' }) create(@Body() dto: CreateLocomotiveDto) { return this.locomotivesService.create(dto); } @Patch(':id') + @FleetManage() @ApiOperation({ summary: 'Update a locomotive' }) update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateLocomotiveDto) { return this.locomotivesService.update(id, dto); } @Post(':id/decommission') + @FleetManage() @ApiOperation({ summary: 'Decommission a locomotive' }) decommission(@Param('id', ParseUUIDPipe) id: string) { return this.locomotivesService.decommission(id); diff --git a/apps/edr-freight-api/src/modules/payment/payment.controller.ts b/apps/edr-freight-api/src/modules/payment/payment.controller.ts index 736f5d274..384876705 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.controller.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.controller.ts @@ -17,6 +17,7 @@ import { } from "@nestjs/swagger"; import { Response } from "express"; import { Public } from "@edr/api-common"; +import { FreightAdmin } from "../../common/booking-guards"; import { PaymentService } from "./payment.service"; import { InitiatePaymentDto, @@ -33,6 +34,7 @@ export class PaymentController { constructor(private readonly paymentService: PaymentService) { } @Get("all") + @FreightAdmin() @ApiOperation({ summary: "Get all payments with filters (staff/admin only)" }) @ApiQuery({ name: "search", required: false }) @ApiQuery({ name: "status", required: false }) @@ -73,6 +75,7 @@ export class PaymentController { } @Post("refund") + @FreightAdmin() @ApiOperation({ summary: "Refund a paid booking (staff/admin only)" }) refund(@Body() dto: RefundDto) { return this.paymentService.refund(dto); diff --git a/apps/edr-freight-api/src/modules/routes/routes.controller.ts b/apps/edr-freight-api/src/modules/routes/routes.controller.ts index 4af088727..8c25d67b3 100644 --- a/apps/edr-freight-api/src/modules/routes/routes.controller.ts +++ b/apps/edr-freight-api/src/modules/routes/routes.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { FleetManage, FleetView } from '../../common/booking-guards'; import { CreateRouteDto } from './dto/create-route.dto'; import { FilterRoutesDto } from './dto/filter-routes.dto'; import { UpdateRouteDto } from './dto/update-route.dto'; @@ -9,6 +10,7 @@ import { RoutesService } from './routes.service'; @ApiTags('routes') @ApiBearerAuth() @Controller('routes') +@FleetView() export class RoutesController { constructor(private readonly routesService: RoutesService) {} @@ -25,18 +27,21 @@ export class RoutesController { } @Post() + @FleetManage() @ApiOperation({ summary: 'Create route' }) create(@Body() dto: CreateRouteDto) { return this.routesService.create(dto); } @Patch(':id') + @FleetManage() @ApiOperation({ summary: 'Update route' }) update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateRouteDto) { return this.routesService.update(id, dto); } @Delete(':id') + @FleetManage() @ApiOperation({ summary: 'Deactivate route' }) remove(@Param('id', ParseUUIDPipe) id: string) { return this.routesService.deactivate(id); diff --git a/apps/edr-freight-api/src/modules/trains/trains.controller.ts b/apps/edr-freight-api/src/modules/trains/trains.controller.ts index c58fc086e..0217bc161 100644 --- a/apps/edr-freight-api/src/modules/trains/trains.controller.ts +++ b/apps/edr-freight-api/src/modules/trains/trains.controller.ts @@ -11,16 +11,19 @@ import { } from "@nestjs/common"; import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { FleetManage, FleetView } from "../../common/booking-guards"; import { CreateTrainDto } from "./dto/create-train.dto"; import { UpdateTrainDto } from "./dto/update-train.dto"; import { TrainsService } from "./trains.service"; @ApiTags("trains") @Controller("trains") +@FleetView() export class TrainsController { constructor(private readonly trainsService: TrainsService) {} @Post() + @FleetManage() @ApiOperation({ summary: "Register a new train" }) create(@Body() dto: CreateTrainDto) { return this.trainsService.create(dto); @@ -39,12 +42,14 @@ export class TrainsController { } @Patch(":id") + @FleetManage() @ApiOperation({ summary: "Update a train" }) update(@Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateTrainDto) { return this.trainsService.update(id, dto); } @Delete(":id") + @FleetManage() @ApiOperation({ summary: "Delete a train" }) remove(@Param("id", ParseUUIDPipe) id: string) { return this.trainsService.remove(id); diff --git a/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts b/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts index c70208052..ec98a4a4b 100644 --- a/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts +++ b/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts @@ -10,6 +10,7 @@ import { Query, } from '@nestjs/common'; import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { FleetManage, FleetView } from '../../common/booking-guards'; import { CreateWagonDto } from './dto/create-wagon.dto'; import { ListWagonsQueryDto } from './dto/list-wagons-query.dto'; import { UpdateWagonDto } from './dto/update-wagon.dto'; @@ -19,10 +20,12 @@ import { WagonsService } from './wagons.service'; @ApiTags('wagons') @Controller('wagons') +@FleetView() export class WagonsController { constructor(private readonly wagonsService: WagonsService) {} @Post() + @FleetManage() @ApiOperation({ summary: 'Create a new wagon' }) create(@Body() dto: CreateWagonDto) { return this.wagonsService.create(dto); @@ -41,24 +44,28 @@ export class WagonsController { } @Patch(':id') + @FleetManage() @ApiOperation({ summary: 'Update a wagon' }) update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWagonDto) { return this.wagonsService.update(id, dto); } @Delete(':id') + @FleetManage() @ApiOperation({ summary: 'Delete a wagon' }) remove(@Param('id', ParseUUIDPipe) id: string) { return this.wagonsService.remove(id); } @Post(':id/assign-train') + @FleetManage() @ApiOperation({ summary: 'Assign wagon to a train' }) assignToTrain(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignWagonToTrainDto) { return this.wagonsService.assignToTrain(id, dto); } @Post(':id/unassign-train') + @FleetManage() @ApiOperation({ summary: 'Unassign wagon from train' }) unassignFromTrain(@Param('id', ParseUUIDPipe) id: string) { return this.wagonsService.unassignFromTrain(id); @@ -67,10 +74,12 @@ export class WagonsController { // Separate controller for train‑specific reorder (registered in module) @Controller('trains/:trainId/reorder-wagons') +@FleetView() export class TrainWagonsReorderController { constructor(private readonly wagonsService: WagonsService) {} @Post() + @FleetManage() @ApiOperation({ summary: 'Reorder wagons of a train' }) reorder(@Param('trainId', ParseUUIDPipe) trainId: string, @Body() dto: ReorderWagonsDto) { return this.wagonsService.reorderWagons(trainId, dto); diff --git a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts index 9d8cd382f..ed0a494ab 100644 --- a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts +++ b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts @@ -54,6 +54,9 @@ export const BOOKING_PERMISSIONS: FreightPermissionSeed[] = [ perm('a1000001-0001-4000-8000-00000000000e', 'edr_freight_app:bookings:cancel', 'Cancel booking'), perm('a1000001-0001-4000-8000-00000000000f', 'edr_freight_app:train_scheduling:view', 'View train scheduling'), perm('a1000001-0001-4000-8000-000000000010', 'edr_freight_app:train_scheduling:manage', 'Manage train scheduling'), + perm('a1000001-0001-4000-8000-000000000011', 'edr_freight_app:fleet:view', 'View fleet'), + perm('a1000001-0001-4000-8000-000000000012', 'edr_freight_app:fleet:manage', 'Manage fleet'), + perm('a1000001-0001-4000-8000-000000000013', 'edr_freight_app:admin', 'Freight administration'), ]; const RULE_ENGINE_PERMISSION_IDS: Record = { @@ -109,6 +112,11 @@ export const FREIGHT_PERMS = { view: 'edr_freight_app:train_scheduling:view', manage: 'edr_freight_app:train_scheduling:manage', }, + fleet: { + view: 'edr_freight_app:fleet:view', + manage: 'edr_freight_app:fleet:manage', + }, + admin: 'edr_freight_app:admin', ruleEngine: { view: (slug: RuleEngineResourceSlug) => `edr_freight_app:rule_engine:${slugToResourceKey(slug)}:view`, @@ -134,12 +142,15 @@ export const ROLE_PERMISSION_PRESETS = { FREIGHT_PERMS.bookings.cancel, ...allRuleEngineViewKeys(), ], - // Operations Officer: train scheduling + wagon allocation + transit/complete. + // Operations Officer: train scheduling + wagon allocation + transit/complete + // + fleet management (wagons, trains, locomotives, routes, containers, cargo). operationsOfficer: [ FREIGHT_PERMS.bookings.view, FREIGHT_PERMS.bookings.operations, FREIGHT_PERMS.trainScheduling.view, FREIGHT_PERMS.trainScheduling.manage, + FREIGHT_PERMS.fleet.view, + FREIGHT_PERMS.fleet.manage, ...allRuleEngineViewKeys(), ], director: [ diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 9468fe568..8a1409fbe 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -99,11 +99,13 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ label: "Routes", href: "/dashboard/routes", icon: , + permission: FREIGHT_PERMS.fleet.view, }, { label: "Locomotives", href: "/dashboard/locomotives", icon: , + permission: FREIGHT_PERMS.fleet.view, }, // { // label: "Trains", @@ -119,6 +121,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ label: "Wagons", href: "/dashboard/wagons", icon: , + permission: FREIGHT_PERMS.fleet.view, }, // { // label: "Containers", @@ -139,6 +142,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ label: "User management", href: "/dashboard/user-management", icon: , + permission: FREIGHT_PERMS.admin, children: [ { label: "Users", @@ -166,11 +170,13 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [ label: "File settings", href: "/dashboard/file-settings", icon: , + permission: FREIGHT_PERMS.admin, }, { label: "Dropdown settings", href: "/dashboard/dropdown-settings", icon: , + permission: FREIGHT_PERMS.admin, }, ], }, @@ -325,13 +331,62 @@ const App = () => { } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> {/* iframe-based user management module */} } /> @@ -344,8 +399,22 @@ const App = () => { } /> } /> - } /> - } /> + + + + } + /> + + + + } + /> @@ -82,6 +87,14 @@ export function canManageScheduling(user: AuthUser | null | undefined): boolean return hasPermission(user, FREIGHT_PERMS.trainScheduling.manage); } +export function canViewFleet(user: AuthUser | null | undefined): boolean { + return hasPermission(user, FREIGHT_PERMS.fleet.view); +} + +export function isFreightAdmin(user: AuthUser | null | undefined): boolean { + return hasPermission(user, FREIGHT_PERMS.admin); +} + export function ruleEngineViewKey(slug: RuleEngineResourceSlug): string { return `edr_freight_app:rule_engine:${slugToResourceKey(slug)}:view`; }