diff --git a/apps/edr-freight-api/src/common/booking-guards.ts b/apps/edr-freight-api/src/common/booking-guards.ts
index d478da8f3..a412bf990 100644
--- a/apps/edr-freight-api/src/common/booking-guards.ts
+++ b/apps/edr-freight-api/src/common/booking-guards.ts
@@ -29,9 +29,22 @@ export const TrainSchedulingView = () =>
export const TrainSchedulingManage = () =>
BookingStaff(FREIGHT_PERMS.trainScheduling.manage);
-export const FleetView = () => BookingStaff(FREIGHT_PERMS.fleet.view);
+/**
+ * Fleet guards take an optional granular per-resource key (locomotives:create,
+ * wagons:delete, …). The legacy coarse fleet:view / fleet:manage keys remain
+ * valid as a one-of fallback so existing role grants keep working.
+ */
+export const FleetView = (granular?: string) =>
+ BookingStaff(
+ granular ? [granular, FREIGHT_PERMS.fleet.view] : FREIGHT_PERMS.fleet.view,
+ );
-export const FleetManage = () => BookingStaff(FREIGHT_PERMS.fleet.manage);
+export const FleetManage = (granular?: string) =>
+ BookingStaff(
+ granular
+ ? [granular, FREIGHT_PERMS.fleet.manage]
+ : FREIGHT_PERMS.fleet.manage,
+ );
/** Requester creates a wagon-transfer request (count-only, no wagon picks). */
export const WagonTransferRequest = () =>
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 7f3f06ec2..ac3adb7e8 100644
--- a/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts
+++ b/apps/edr-freight-api/src/modules/cargoes/cargoes.controller.ts
@@ -11,6 +11,7 @@ import {
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { FleetManage, FleetView } from '../../common/booking-guards';
+import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
import { CreateCargoDto } from './dto/create-cargo.dto';
import { UpdateCargoDto } from './dto/update-cargo.dto';
import { LoadCargoDto } from './dto/load-cargo.dto';
@@ -19,12 +20,12 @@ import { CargoesService } from './cargoes.service';
@ApiTags('cargoes')
@Controller('cargoes')
-@FleetView()
+@FleetView(FREIGHT_PERMS.cargoes.view)
export class CargoesController {
constructor(private readonly cargoesService: CargoesService) {}
@Post()
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.cargoes.create)
@ApiOperation({ summary: 'Create a new cargo' })
create(@Body() dto: CreateCargoDto) {
return this.cargoesService.create(dto);
@@ -43,35 +44,35 @@ export class CargoesController {
}
@Patch(':id')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.cargoes.update)
@ApiOperation({ summary: 'Update a cargo' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateCargoDto) {
return this.cargoesService.update(id, dto);
}
@Delete(':id')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.cargoes.delete)
@ApiOperation({ summary: 'Delete a cargo' })
remove(@Param('id', ParseUUIDPipe) id: string) {
return this.cargoesService.remove(id);
}
@Post(':id/load')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.cargoes.update)
@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()
+ @FleetManage(FREIGHT_PERMS.cargoes.update)
@ApiOperation({ summary: 'Unload cargo from container' })
unload(@Param('id', ParseUUIDPipe) id: string) {
return this.cargoesService.unloadCargo(id);
}
@Post(':id/deliver')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.cargoes.update)
@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/consignments/consignments.controller.ts b/apps/edr-freight-api/src/modules/consignments/consignments.controller.ts
index b107e8935..579b9ee26 100644
--- a/apps/edr-freight-api/src/modules/consignments/consignments.controller.ts
+++ b/apps/edr-freight-api/src/modules/consignments/consignments.controller.ts
@@ -10,18 +10,19 @@ import {
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { FleetManage, FleetView } from "../../common/booking-guards";
+import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
import { ConsignmentsService } from "./consignments.service";
import { CreateConsignmentDto } from "./dto/create-consignment.dto";
import { FilterConsignmentDto } from "./dto/filter-consignment.dto";
@ApiTags("consignments")
@Controller("consignments")
-@FleetView()
+@FleetView(FREIGHT_PERMS.consignments.view)
export class ConsignmentsController {
constructor(private readonly consignmentsService: ConsignmentsService) {}
@Post()
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.consignments.create)
@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 1a0cdb14f..0a5e6bb0f 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
@@ -11,6 +11,7 @@ import {
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { FleetManage, FleetView } from '../../common/booking-guards';
+import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
import { CreateContainerDto } from './dto/create-container.dto';
import { UpdateContainerDto } from './dto/update-container.dto';
import { AssignContainerToWagonDto } from './dto/assign-container-to-wagon.dto';
@@ -18,12 +19,12 @@ import { ContainersService } from './containers.service';
@ApiTags('containers')
@Controller('containers')
-@FleetView()
+@FleetView(FREIGHT_PERMS.containers.view)
export class ContainersController {
constructor(private readonly containersService: ContainersService) {}
@Post()
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.containers.create)
@ApiOperation({ summary: 'Create a new container' })
create(@Body() dto: CreateContainerDto) {
return this.containersService.create(dto);
@@ -42,28 +43,28 @@ export class ContainersController {
}
@Patch(':id')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.containers.update)
@ApiOperation({ summary: 'Update a container' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateContainerDto) {
return this.containersService.update(id, dto);
}
@Delete(':id')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.containers.delete)
@ApiOperation({ summary: 'Delete a container' })
remove(@Param('id', ParseUUIDPipe) id: string) {
return this.containersService.remove(id);
}
@Post(':id/assign-wagon')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.containers.update)
@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()
+ @FleetManage(FREIGHT_PERMS.containers.update)
@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/locomotives/locomotives.controller.ts b/apps/edr-freight-api/src/modules/locomotives/locomotives.controller.ts
index fc80b238a..77d8b0df2 100644
--- a/apps/edr-freight-api/src/modules/locomotives/locomotives.controller.ts
+++ b/apps/edr-freight-api/src/modules/locomotives/locomotives.controller.ts
@@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query } from
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { FleetManage, StaffReference } from '../../common/booking-guards';
+import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
import { CreateLocomotiveDto } from './dto/create-locomotive.dto';
import { FilterLocomotivesDto } from './dto/filter-locomotives.dto';
import { UpdateLocomotiveDto } from './dto/update-locomotive.dto';
@@ -31,21 +32,21 @@ export class LocomotivesController {
}
@Post()
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.locomotives.create)
@ApiOperation({ summary: 'Create a locomotive' })
create(@Body() dto: CreateLocomotiveDto) {
return this.locomotivesService.create(dto);
}
@Patch(':id')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.locomotives.update)
@ApiOperation({ summary: 'Update a locomotive' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateLocomotiveDto) {
return this.locomotivesService.update(id, dto);
}
@Post(':id/decommission')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.locomotives.delete)
@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/routes/routes.controller.ts b/apps/edr-freight-api/src/modules/routes/routes.controller.ts
index 8c25d67b3..cf2314156 100644
--- a/apps/edr-freight-api/src/modules/routes/routes.controller.ts
+++ b/apps/edr-freight-api/src/modules/routes/routes.controller.ts
@@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { FleetManage, FleetView } from '../../common/booking-guards';
+import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
import { CreateRouteDto } from './dto/create-route.dto';
import { FilterRoutesDto } from './dto/filter-routes.dto';
import { UpdateRouteDto } from './dto/update-route.dto';
@@ -10,7 +11,7 @@ import { RoutesService } from './routes.service';
@ApiTags('routes')
@ApiBearerAuth()
@Controller('routes')
-@FleetView()
+@FleetView(FREIGHT_PERMS.routes.view)
export class RoutesController {
constructor(private readonly routesService: RoutesService) {}
@@ -27,21 +28,21 @@ export class RoutesController {
}
@Post()
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.routes.create)
@ApiOperation({ summary: 'Create route' })
create(@Body() dto: CreateRouteDto) {
return this.routesService.create(dto);
}
@Patch(':id')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.routes.update)
@ApiOperation({ summary: 'Update route' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateRouteDto) {
return this.routesService.update(id, dto);
}
@Delete(':id')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.routes.delete)
@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/train-builder.controller.ts b/apps/edr-freight-api/src/modules/trains/train-builder.controller.ts
index babf11ef1..19d631fbc 100644
--- a/apps/edr-freight-api/src/modules/trains/train-builder.controller.ts
+++ b/apps/edr-freight-api/src/modules/trains/train-builder.controller.ts
@@ -15,6 +15,7 @@ import {
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { FleetManage, FleetView } from '../../common/booking-guards';
+import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
import { AssignTrainWagonsDto } from './dto/assign-train-wagons.dto';
import { BuildTrainDto } from './dto/build-train.dto';
import { ListBuiltTrainsQueryDto } from './dto/list-built-trains-query.dto';
@@ -27,12 +28,12 @@ import { TrainBuilderService } from './train-builder.service';
@ApiTags('train-builder')
@ApiBearerAuth()
@Controller('train-builder')
-@FleetView()
+@FleetView(FREIGHT_PERMS.trains.view)
export class TrainBuilderController {
constructor(private readonly trainBuilderService: TrainBuilderService) {}
@Post()
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.create)
@ApiOperation({ summary: 'Build a train: code + yard + 2+ locomotives (+ optional wagons)' })
build(@Body() dto: BuildTrainDto) {
return this.trainBuilderService.buildTrain(dto);
@@ -60,7 +61,7 @@ export class TrainBuilderController {
}
@Put(':id/locomotives')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.update)
@ApiOperation({ summary: 'Replace the locomotive set (minimum 2, same yard)' })
setLocomotives(
@Param('id', ParseUUIDPipe) id: string,
@@ -70,7 +71,7 @@ export class TrainBuilderController {
}
@Patch(':id/details')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.update)
@ApiOperation({
summary: "Edit the train's name and fixed import/export run numbers",
})
@@ -82,7 +83,7 @@ export class TrainBuilderController {
}
@Patch(':id/yard')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.update)
@ApiOperation({
summary: 'Relocate the train — its locomotives and wagons move to the new yard with it',
})
@@ -91,14 +92,14 @@ export class TrainBuilderController {
}
@Post(':id/wagons')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.assignWagons)
@ApiOperation({ summary: "Append AVAILABLE wagons from the train's yard to the consist" })
assignWagons(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignTrainWagonsDto) {
return this.trainBuilderService.assignWagons(id, dto);
}
@Delete(':id/wagons/:wagonId')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.assignWagons)
@ApiOperation({ summary: 'Detach one wagon from the consist' })
removeWagon(
@Param('id', ParseUUIDPipe) id: string,
@@ -108,7 +109,7 @@ export class TrainBuilderController {
}
@Post(':id/wagons/:wagonId/maintenance')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.assignWagons)
@ApiOperation({ summary: 'Detach one wagon and move it to MAINTENANCE status' })
sendWagonToMaintenance(
@Param('id', ParseUUIDPipe) id: string,
@@ -118,14 +119,14 @@ export class TrainBuilderController {
}
@Post(':id/reorder-wagons')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.assignWagons)
@ApiOperation({ summary: 'Persist a drag-reorder of the full consist' })
reorderWagons(@Param('id', ParseUUIDPipe) id: string, @Body() dto: ReorderTrainWagonsDto) {
return this.trainBuilderService.reorderWagons(id, dto);
}
@Post(':id/deactivate')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.update)
@ApiOperation({
summary: 'Deactivate the train (park it) — only allowed with no active schedule',
})
@@ -134,14 +135,14 @@ export class TrainBuilderController {
}
@Post(':id/activate')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.update)
@ApiOperation({ summary: 'Reactivate a deactivated train back to AVAILABLE' })
activate(@Param('id', ParseUUIDPipe) id: string) {
return this.trainBuilderService.activate(id);
}
@Delete(':id')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.delete)
@HttpCode(HttpStatus.NO_CONTENT)
@ApiOperation({ summary: 'Disband the train (release wagons and locomotives)' })
disband(@Param('id', ParseUUIDPipe) id: string) {
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 0217bc161..173a738fa 100644
--- a/apps/edr-freight-api/src/modules/trains/trains.controller.ts
+++ b/apps/edr-freight-api/src/modules/trains/trains.controller.ts
@@ -12,18 +12,19 @@ import {
import { ApiOperation, ApiTags } from "@nestjs/swagger";
import { FleetManage, FleetView } from "../../common/booking-guards";
+import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
import { CreateTrainDto } from "./dto/create-train.dto";
import { UpdateTrainDto } from "./dto/update-train.dto";
import { TrainsService } from "./trains.service";
@ApiTags("trains")
@Controller("trains")
-@FleetView()
+@FleetView(FREIGHT_PERMS.trains.view)
export class TrainsController {
constructor(private readonly trainsService: TrainsService) {}
@Post()
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.create)
@ApiOperation({ summary: "Register a new train" })
create(@Body() dto: CreateTrainDto) {
return this.trainsService.create(dto);
@@ -42,14 +43,14 @@ export class TrainsController {
}
@Patch(":id")
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.update)
@ApiOperation({ summary: "Update a train" })
update(@Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateTrainDto) {
return this.trainsService.update(id, dto);
}
@Delete(":id")
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.delete)
@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/wagon-transfer-requests.controller.ts b/apps/edr-freight-api/src/modules/wagons/wagon-transfer-requests.controller.ts
index d6925b71b..b00e4a64f 100644
--- a/apps/edr-freight-api/src/modules/wagons/wagon-transfer-requests.controller.ts
+++ b/apps/edr-freight-api/src/modules/wagons/wagon-transfer-requests.controller.ts
@@ -19,6 +19,7 @@ import {
WagonTransferHistoryAll,
WagonTransferRequest,
} from '../../common/booking-guards';
+import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
import { BulkFulfillTransferRequestsDto } from './dto/bulk-fulfill-transfer-requests.dto';
import { CreateTransferRequestDto } from './dto/create-transfer-request.dto';
import { FulfillTransferRequestDto } from './dto/fulfill-transfer-request.dto';
@@ -31,7 +32,7 @@ import { WagonTransferRequestsService } from './wagon-transfer-requests.service'
*/
@ApiTags('wagon-transfer-requests')
@Controller('wagon-transfer-requests')
-@FleetView()
+@FleetView(FREIGHT_PERMS.wagons.view)
export class WagonTransferRequestsController {
constructor(private readonly service: WagonTransferRequestsService) {}
@@ -110,7 +111,7 @@ export class WagonTransferRequestsController {
}
@Post(':id/cancel')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.wagons.transferRequest)
@ApiOperation({ summary: 'Withdraw a pending transfer request' })
cancel(@Param('id', ParseUUIDPipe) id: string) {
return this.service.cancelRequest(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 cd501362d..2ef7f00a5 100644
--- a/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts
+++ b/apps/edr-freight-api/src/modules/wagons/wagons.controller.ts
@@ -13,6 +13,7 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { CurrentUser } from '@edr/api-common';
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
import { FleetManage, FleetView, StaffReference } from '../../common/booking-guards';
+import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
import { CreateWagonDto } from './dto/create-wagon.dto';
import { ListWagonsQueryDto } from './dto/list-wagons-query.dto';
import { UpdateWagonDto } from './dto/update-wagon.dto';
@@ -31,7 +32,7 @@ export class WagonsController {
constructor(private readonly wagonsService: WagonsService) {}
@Post()
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.wagons.create)
@ApiOperation({ summary: 'Create a new wagon' })
create(@Body() dto: CreateWagonDto) {
return this.wagonsService.create(dto);
@@ -61,42 +62,42 @@ export class WagonsController {
}
@Patch(':id')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.wagons.update)
@ApiOperation({ summary: 'Update a wagon' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWagonDto) {
return this.wagonsService.update(id, dto);
}
@Delete(':id')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.wagons.delete)
@ApiOperation({ summary: 'Delete a wagon' })
remove(@Param('id', ParseUUIDPipe) id: string) {
return this.wagonsService.remove(id);
}
@Post(':id/assign-train')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.assignWagons)
@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()
+ @FleetManage(FREIGHT_PERMS.trains.assignWagons)
@ApiOperation({ summary: 'Unassign wagon from train' })
unassignFromTrain(@Param('id', ParseUUIDPipe) id: string) {
return this.wagonsService.unassignFromTrain(id);
}
@Post('bulk-transfer')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.wagons.update)
@ApiOperation({ summary: 'Transfer multiple wagons to a destination yard' })
bulkTransfer(@Body() dto: BulkTransferWagonsDto, @CurrentUser() user: TCurrentUser) {
return this.wagonsService.bulkTransfer(dto, user?.id);
}
@Post('bulk-status')
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.wagons.update)
@ApiOperation({ summary: 'Set the status of multiple wagons' })
bulkSetStatus(@Body() dto: BulkSetWagonStatusDto) {
return this.wagonsService.bulkSetStatus(dto);
@@ -105,12 +106,12 @@ export class WagonsController {
// Separate controller for train‑specific reorder (registered in module)
@Controller('trains/:trainId/reorder-wagons')
-@FleetView()
+@FleetView(FREIGHT_PERMS.trains.view)
export class TrainWagonsReorderController {
constructor(private readonly wagonsService: WagonsService) {}
@Post()
- @FleetManage()
+ @FleetManage(FREIGHT_PERMS.trains.assignWagons)
@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 94f978dc5..aaebbe52a 100644
--- a/apps/edr-freight-api/src/seed/freight-permissions.registry.ts
+++ b/apps/edr-freight-api/src/seed/freight-permissions.registry.ts
@@ -212,6 +212,8 @@ export const FLEET_RAIL_PERMISSIONS: FreightPermissionSeed[] = [
perm('e1f00001-0001-4000-8000-000000000002', 'edr_freight_app:cargoes:create', 'Create cargo'),
perm('e1f00001-0001-4000-8000-000000000003', 'edr_freight_app:cargoes:update', 'Update cargo'),
perm('e1f00001-0001-4000-8000-000000000004', 'edr_freight_app:cargoes:delete', 'Delete cargo'),
+ perm('e1g00001-0001-4000-8000-000000000001', 'edr_freight_app:consignments:view', 'View consignments'),
+ perm('e1g00001-0001-4000-8000-000000000002', 'edr_freight_app:consignments:create', 'Create consignment'),
];
// G. Fleet — road & telemetry
@@ -493,6 +495,10 @@ export const FREIGHT_PERMS = {
update: 'edr_freight_app:cargoes:update',
delete: 'edr_freight_app:cargoes:delete',
},
+ consignments: {
+ view: 'edr_freight_app:consignments:view',
+ create: 'edr_freight_app:consignments:create',
+ },
vehicles: {
view: 'edr_freight_app:vehicles:view',
create: 'edr_freight_app:vehicles:create',
diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx
index e1c4de1a2..04a26e3fe 100644
--- a/apps/edr-freight-web/backoffice/src/App.tsx
+++ b/apps/edr-freight-web/backoffice/src/App.tsx
@@ -271,19 +271,19 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
label: "Routes",
href: "/dashboard/routes",
icon: ,
- permission: FREIGHT_PERMS.fleet.view,
+ permission: [FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view],
},
{
label: "Locomotives",
href: "/dashboard/locomotives",
icon: ,
- permission: FREIGHT_PERMS.fleet.view,
+ permission: [FREIGHT_PERMS.locomotives.view, FREIGHT_PERMS.fleet.view],
},
{
label: "Train Builder",
href: "/dashboard/train-builder",
icon: ,
- permission: FREIGHT_PERMS.fleet.view,
+ permission: [FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view],
},
// {
@@ -295,7 +295,7 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
label: "Wagons",
href: "/dashboard/wagons",
icon: ,
- permission: FREIGHT_PERMS.fleet.view,
+ permission: [FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view],
},
{
label: "Vehicles",
@@ -1113,7 +1113,7 @@ const App = () => {
+
}
@@ -1121,7 +1121,7 @@ const App = () => {
+
}
@@ -1129,7 +1129,7 @@ const App = () => {
+
}
@@ -1137,7 +1137,7 @@ const App = () => {
+
}
@@ -1145,7 +1145,7 @@ const App = () => {
+
}
@@ -1153,7 +1153,7 @@ const App = () => {
+
}
@@ -1161,7 +1161,7 @@ const App = () => {
+
}
@@ -1169,7 +1169,7 @@ const App = () => {
+
}
@@ -1177,7 +1177,7 @@ const App = () => {
+
}
@@ -1263,7 +1263,7 @@ const App = () => {
+
}
@@ -1351,7 +1351,7 @@ const App = () => {
+
}
@@ -1359,7 +1359,7 @@ const App = () => {
+
}
@@ -1367,7 +1367,7 @@ const App = () => {
+
}
@@ -1375,7 +1375,7 @@ const App = () => {
+
}
@@ -1383,7 +1383,7 @@ const App = () => {
+
}
@@ -1391,7 +1391,7 @@ const App = () => {
+
}
@@ -1399,7 +1399,7 @@ const App = () => {
+
}
@@ -1407,7 +1407,7 @@ const App = () => {
+
}
diff --git a/apps/edr-freight-web/backoffice/src/components/fleet/FleetCardGrid.tsx b/apps/edr-freight-web/backoffice/src/components/fleet/FleetCardGrid.tsx
index b2eef655f..1f8ab3f9f 100644
--- a/apps/edr-freight-web/backoffice/src/components/fleet/FleetCardGrid.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/fleet/FleetCardGrid.tsx
@@ -19,8 +19,9 @@ export interface FleetCardGridProps {
pageCount: number;
totalCount: number;
onPaginationChange: OnChangeFn;
- onEdit: (record: FleetRecord) => void;
- onRemove: (record: FleetRecord) => void;
+ /** Omit to hide the action (caller lacks the update/delete permission). */
+ onEdit?: (record: FleetRecord) => void;
+ onRemove?: (record: FleetRecord) => void;
}
const FleetCardGrid = ({
diff --git a/apps/edr-freight-web/backoffice/src/components/fleet/FleetRecordActions.tsx b/apps/edr-freight-web/backoffice/src/components/fleet/FleetRecordActions.tsx
index 887b235c4..6d9978f2c 100644
--- a/apps/edr-freight-web/backoffice/src/components/fleet/FleetRecordActions.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/fleet/FleetRecordActions.tsx
@@ -8,8 +8,9 @@ import type { FleetRecord } from "@/services/fleet/fleet.service";
export interface FleetRecordActionsProps {
record: FleetRecord;
config: FleetResourceConfig;
- onEdit: (record: FleetRecord) => void;
- onRemove: (record: FleetRecord) => void;
+ /** Omit to hide the action (caller lacks the update/delete permission). */
+ onEdit?: (record: FleetRecord) => void;
+ onRemove?: (record: FleetRecord) => void;
onAssignDriver?: (record: FleetRecord) => void;
onHistory?: (record: FleetRecord) => void;
onViewDetail?: (record: FleetRecord) => void;
@@ -42,6 +43,17 @@ const FleetRecordActions = ({
navigate(config.detailPath.replace(":id", String(record.id)));
};
+ if (
+ !onEdit &&
+ !onRemove &&
+ !showDetail &&
+ !showViewDetail &&
+ !showHistory &&
+ !(isVehicle && onAssignDriver)
+ ) {
+ return null;
+ }
+
if (layout === "compact") {
return (
);
@@ -121,12 +137,14 @@ const FleetRecordActions = ({
Assign Driver
) : null}
-
+ {onEdit ? (
+
+ ) : null}
{showViewDetail ? (
) : null}
-
+ {onRemove ? (
+
+ ) : null}
);
diff --git a/apps/edr-freight-web/backoffice/src/lib/permissions.ts b/apps/edr-freight-web/backoffice/src/lib/permissions.ts
index c2b82b765..dfc55d115 100644
--- a/apps/edr-freight-web/backoffice/src/lib/permissions.ts
+++ b/apps/edr-freight-web/backoffice/src/lib/permissions.ts
@@ -507,6 +507,31 @@ export function canViewFleet(user: AuthUser | null | undefined): boolean {
return hasPermission(user, FREIGHT_PERMS.fleet.view);
}
+export type FleetCrudResource =
+ | "locomotives"
+ | "wagons"
+ | "trains"
+ | "routes"
+ | "containers"
+ | "cargoes"
+ | "vehicles"
+ | "drivers";
+
+/**
+ * Per-resource fleet CRUD check. The legacy coarse fleet:manage key still
+ * grants every action (mirrors the API's one-of guard fallback).
+ */
+export function canFleetAction(
+ user: AuthUser | null | undefined,
+ resource: FleetCrudResource,
+ action: "create" | "update" | "delete",
+): boolean {
+ return (
+ hasPermission(user, FREIGHT_PERMS[resource][action]) ||
+ hasPermission(user, FREIGHT_PERMS.fleet.manage)
+ );
+}
+
export function isFreightAdmin(user: AuthUser | null | undefined): boolean {
return hasPermission(user, FREIGHT_PERMS.admin);
}
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
index 95dff563a..63a722550 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/FleetResourcePage.tsx
@@ -3,6 +3,8 @@ import { Box, Button, Card, Container, Group, Modal, Select, Stack, Text, Title
import { useMutation, useQuery } from "@tanstack/react-query";
import { api } from "@/services/api";
+import { useAuth } from "@/auth/useAuth";
+import { canFleetAction } from "@/lib/permissions";
import Breadcrumbs from "@/components/ui/Breadcrumbs";
import { Inbox, Plus, Warehouse } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
@@ -37,6 +39,10 @@ const FleetResourcePage = () => {
const slug = getFleetSlugFromPath(location.pathname) ?? DEFAULT_SLUG;
const config = getFleetResource(slug);
const { toast } = useToast();
+ const { user } = useAuth();
+ const canCreate = canFleetAction(user, slug, "create");
+ const canUpdate = canFleetAction(user, slug, "update");
+ const canDelete = canFleetAction(user, slug, "delete");
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const [search, setSearch] = useState("");
@@ -278,12 +284,16 @@ const FleetResourcePage = () => {
record={row.original}
config={config}
layout="compact"
- onEdit={(record) => {
- setEditing(record);
- setFormOpen(true);
- }}
- onRemove={setRemoveTarget}
- onAssignDriver={setAssigningDriver}
+ onEdit={
+ canUpdate
+ ? (record) => {
+ setEditing(record);
+ setFormOpen(true);
+ }
+ : undefined
+ }
+ onRemove={canDelete ? setRemoveTarget : undefined}
+ onAssignDriver={canUpdate ? setAssigningDriver : undefined}
onHistory={setHistoryTarget}
/>
@@ -291,7 +301,7 @@ const FleetResourcePage = () => {
});
return base;
- }, [config, dynamicOptions.yards]);
+ }, [config, dynamicOptions.yards, canUpdate, canDelete]);
const tableStatus = isLoading ? "loading" : isError ? "error" : "success";
@@ -390,15 +400,17 @@ const FleetResourcePage = () => {
{slug === "wagons" ? (
<>
- }
- styles={{ label: { fontWeight: 500 } }}
- onClick={() => setWagonWorkspaceOpen(true)}
- >
- Yard Workspace
-
+ {canUpdate ? (
+ }
+ styles={{ label: { fontWeight: 500 } }}
+ onClick={() => setWagonWorkspaceOpen(true)}
+ >
+ Yard Workspace
+
+ ) : null}
>
) : null}
- } styles={{ label: { fontWeight: 500 } }} onClick={() => {
- setEditing(null);
- setFormOpen(true);
- }}>
- {config.addLabel}
-
+ {canCreate ? (
+ } styles={{ label: { fontWeight: 500 } }} onClick={() => {
+ setEditing(null);
+ setFormOpen(true);
+ }}>
+ {config.addLabel}
+
+ ) : null}
@@ -529,11 +543,15 @@ const FleetResourcePage = () => {
pageCount={pageCount}
totalCount={filteredRows.length}
onPaginationChange={setPagination}
- onEdit={(record) => {
- setEditing(record);
- setFormOpen(true);
- }}
- onRemove={setRemoveTarget}
+ onEdit={
+ canUpdate
+ ? (record) => {
+ setEditing(record);
+ setFormOpen(true);
+ }
+ : undefined
+ }
+ onRemove={canDelete ? setRemoveTarget : undefined}
/>
)}
diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx
index 3ea10df7d..d4a26133f 100644
--- a/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/fleet/RoutesPage.tsx
@@ -36,6 +36,8 @@ import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
import { api } from "@/services/api";
import { ruleEngineService } from "@/services/ruleEngine/ruleEngine.service";
+import { useAuth } from "@/auth/useAuth";
+import { canFleetAction } from "@/lib/permissions";
import { useToast } from "@/hooks/use-toast";
import {
formatRouteLabel,
@@ -160,6 +162,10 @@ export default function RoutesPage() {
const { viewMode, setViewMode } = useFleetViewMode("routes");
const { pagination, setPagination } = usePagination({ pageSize: 10 });
const { toast } = useToast();
+ const { user } = useAuth();
+ const canCreate = canFleetAction(user, "routes", "create");
+ const canUpdate = canFleetAction(user, "routes", "update");
+ const canDelete = canFleetAction(user, "routes", "delete");
const routesQuery = useQuery(api.routes.list.queryOptions());
const yardsQuery = useQuery(api.routes.yards.queryOptions());
@@ -441,26 +447,30 @@ export default function RoutesPage() {
-
- openEdit(row.original)}>
-
-
-
-
- handleDeactivate(row.original)}
- >
-
-
-
+ {canUpdate ? (
+
+ openEdit(row.original)}>
+
+
+
+ ) : null}
+ {canDelete ? (
+
+ handleDeactivate(row.original)}
+ >
+
+
+
+ ) : null}
),
},
];
- }, [deactivateMutation.isPending]);
+ }, [deactivateMutation.isPending, canUpdate, canDelete]);
return (
@@ -468,9 +478,11 @@ export default function RoutesPage() {
title="Routes"
subtitle="Define rail corridors, segment distances, and operational status for train scheduling."
action={
- } onClick={openCreate}>
- Add route
-
+ canCreate ? (
+ } onClick={openCreate}>
+ Add route
+
+ ) : undefined
}
/>
@@ -555,9 +567,11 @@ export default function RoutesPage() {
-
+ {canUpdate ? (
+
+ ) : null}
diff --git a/apps/edr-freight-web/backoffice/src/pages/trainBuilder/TrainBuilderDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainBuilder/TrainBuilderDetailPage.tsx
index 3841bc989..f9e4555ae 100644
--- a/apps/edr-freight-web/backoffice/src/pages/trainBuilder/TrainBuilderDetailPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/trainBuilder/TrainBuilderDetailPage.tsx
@@ -45,6 +45,8 @@ import {
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
import { api } from "@/services/api";
+import { useAuth } from "@/auth/useAuth";
+import { canFleetAction, FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
import { useToast } from "@/hooks/use-toast";
const parseError = (error: unknown, fallback: string) => {
@@ -76,6 +78,12 @@ export default function TrainBuilderDetailPage() {
const [yardModalOpen, setYardModalOpen] = useState(false);
const [disbandOpen, setDisbandOpen] = useState(false);
const [deactivateOpen, setDeactivateOpen] = useState(false);
+ const { user } = useAuth();
+ const canUpdate = canFleetAction(user, "trains", "update");
+ const canDelete = canFleetAction(user, "trains", "delete");
+ const canAssign =
+ hasPermission(user, FREIGHT_PERMS.trains.assignWagons) ||
+ hasPermission(user, FREIGHT_PERMS.fleet.manage);
const compositionQuery = useQuery(
api.trainBuilder.composition.queryOptions({ input: { id }, enabled: Boolean(id) }),
@@ -162,59 +170,67 @@ export default function TrainBuilderDetailPage() {
}
action={
-
+ canUpdate || canDelete ? (
+
+ ) : undefined
}
/>
@@ -259,16 +275,18 @@ export default function TrainBuilderDetailPage() {
.
- }
- disabled={!composition.editable}
- onClick={() => setLocoModalOpen(true)}
- >
- Detach & replace locomotives
-
+ {canUpdate ? (
+ }
+ disabled={!composition.editable}
+ onClick={() => setLocoModalOpen(true)}
+ >
+ Detach & replace locomotives
+
+ ) : null}
+ canCreate ? (
+ } onClick={() => setBuildOpen(true)}>
+ Build train
+
+ ) : undefined
}
/>