mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 11:08:12 +00:00
implement freight permissions for trains, wagons, and routes
This commit is contained in:
@@ -29,9 +29,22 @@ export const TrainSchedulingView = () =>
|
|||||||
export const TrainSchedulingManage = () =>
|
export const TrainSchedulingManage = () =>
|
||||||
BookingStaff(FREIGHT_PERMS.trainScheduling.manage);
|
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). */
|
/** Requester creates a wagon-transfer request (count-only, no wagon picks). */
|
||||||
export const WagonTransferRequest = () =>
|
export const WagonTransferRequest = () =>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { FleetManage, FleetView } from '../../common/booking-guards';
|
import { FleetManage, FleetView } from '../../common/booking-guards';
|
||||||
|
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||||
import { CreateCargoDto } from './dto/create-cargo.dto';
|
import { CreateCargoDto } from './dto/create-cargo.dto';
|
||||||
import { UpdateCargoDto } from './dto/update-cargo.dto';
|
import { UpdateCargoDto } from './dto/update-cargo.dto';
|
||||||
import { LoadCargoDto } from './dto/load-cargo.dto';
|
import { LoadCargoDto } from './dto/load-cargo.dto';
|
||||||
@@ -19,12 +20,12 @@ import { CargoesService } from './cargoes.service';
|
|||||||
|
|
||||||
@ApiTags('cargoes')
|
@ApiTags('cargoes')
|
||||||
@Controller('cargoes')
|
@Controller('cargoes')
|
||||||
@FleetView()
|
@FleetView(FREIGHT_PERMS.cargoes.view)
|
||||||
export class CargoesController {
|
export class CargoesController {
|
||||||
constructor(private readonly cargoesService: CargoesService) {}
|
constructor(private readonly cargoesService: CargoesService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.cargoes.create)
|
||||||
@ApiOperation({ summary: 'Create a new cargo' })
|
@ApiOperation({ summary: 'Create a new cargo' })
|
||||||
create(@Body() dto: CreateCargoDto) {
|
create(@Body() dto: CreateCargoDto) {
|
||||||
return this.cargoesService.create(dto);
|
return this.cargoesService.create(dto);
|
||||||
@@ -43,35 +44,35 @@ export class CargoesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.cargoes.update)
|
||||||
@ApiOperation({ summary: 'Update a cargo' })
|
@ApiOperation({ summary: 'Update a cargo' })
|
||||||
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateCargoDto) {
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateCargoDto) {
|
||||||
return this.cargoesService.update(id, dto);
|
return this.cargoesService.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.cargoes.delete)
|
||||||
@ApiOperation({ summary: 'Delete a cargo' })
|
@ApiOperation({ summary: 'Delete a cargo' })
|
||||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.cargoesService.remove(id);
|
return this.cargoesService.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/load')
|
@Post(':id/load')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.cargoes.update)
|
||||||
@ApiOperation({ summary: 'Load cargo into a container' })
|
@ApiOperation({ summary: 'Load cargo into a container' })
|
||||||
load(@Param('id', ParseUUIDPipe) id: string, @Body() dto: LoadCargoDto) {
|
load(@Param('id', ParseUUIDPipe) id: string, @Body() dto: LoadCargoDto) {
|
||||||
return this.cargoesService.loadCargo(id, dto);
|
return this.cargoesService.loadCargo(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/unload')
|
@Post(':id/unload')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.cargoes.update)
|
||||||
@ApiOperation({ summary: 'Unload cargo from container' })
|
@ApiOperation({ summary: 'Unload cargo from container' })
|
||||||
unload(@Param('id', ParseUUIDPipe) id: string) {
|
unload(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.cargoesService.unloadCargo(id);
|
return this.cargoesService.unloadCargo(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/deliver')
|
@Post(':id/deliver')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.cargoes.update)
|
||||||
@ApiOperation({ summary: 'Mark cargo as delivered' })
|
@ApiOperation({ summary: 'Mark cargo as delivered' })
|
||||||
deliver(@Param('id', ParseUUIDPipe) id: string, @Body() dto?: DeliverCargoDto) {
|
deliver(@Param('id', ParseUUIDPipe) id: string, @Body() dto?: DeliverCargoDto) {
|
||||||
return this.cargoesService.deliverCargo(id, dto);
|
return this.cargoesService.deliverCargo(id, dto);
|
||||||
|
|||||||
@@ -10,18 +10,19 @@ import {
|
|||||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { FleetManage, FleetView } from "../../common/booking-guards";
|
import { FleetManage, FleetView } from "../../common/booking-guards";
|
||||||
|
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||||
import { ConsignmentsService } from "./consignments.service";
|
import { ConsignmentsService } from "./consignments.service";
|
||||||
import { CreateConsignmentDto } from "./dto/create-consignment.dto";
|
import { CreateConsignmentDto } from "./dto/create-consignment.dto";
|
||||||
import { FilterConsignmentDto } from "./dto/filter-consignment.dto";
|
import { FilterConsignmentDto } from "./dto/filter-consignment.dto";
|
||||||
|
|
||||||
@ApiTags("consignments")
|
@ApiTags("consignments")
|
||||||
@Controller("consignments")
|
@Controller("consignments")
|
||||||
@FleetView()
|
@FleetView(FREIGHT_PERMS.consignments.view)
|
||||||
export class ConsignmentsController {
|
export class ConsignmentsController {
|
||||||
constructor(private readonly consignmentsService: ConsignmentsService) {}
|
constructor(private readonly consignmentsService: ConsignmentsService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.consignments.create)
|
||||||
@ApiOperation({ summary: "Create a new consignment" })
|
@ApiOperation({ summary: "Create a new consignment" })
|
||||||
create(@Body() dto: CreateConsignmentDto) {
|
create(@Body() dto: CreateConsignmentDto) {
|
||||||
return this.consignmentsService.create(dto);
|
return this.consignmentsService.create(dto);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import { FleetManage, FleetView } from '../../common/booking-guards';
|
import { FleetManage, FleetView } from '../../common/booking-guards';
|
||||||
|
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||||
import { CreateContainerDto } from './dto/create-container.dto';
|
import { CreateContainerDto } from './dto/create-container.dto';
|
||||||
import { UpdateContainerDto } from './dto/update-container.dto';
|
import { UpdateContainerDto } from './dto/update-container.dto';
|
||||||
import { AssignContainerToWagonDto } from './dto/assign-container-to-wagon.dto';
|
import { AssignContainerToWagonDto } from './dto/assign-container-to-wagon.dto';
|
||||||
@@ -18,12 +19,12 @@ import { ContainersService } from './containers.service';
|
|||||||
|
|
||||||
@ApiTags('containers')
|
@ApiTags('containers')
|
||||||
@Controller('containers')
|
@Controller('containers')
|
||||||
@FleetView()
|
@FleetView(FREIGHT_PERMS.containers.view)
|
||||||
export class ContainersController {
|
export class ContainersController {
|
||||||
constructor(private readonly containersService: ContainersService) {}
|
constructor(private readonly containersService: ContainersService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.containers.create)
|
||||||
@ApiOperation({ summary: 'Create a new container' })
|
@ApiOperation({ summary: 'Create a new container' })
|
||||||
create(@Body() dto: CreateContainerDto) {
|
create(@Body() dto: CreateContainerDto) {
|
||||||
return this.containersService.create(dto);
|
return this.containersService.create(dto);
|
||||||
@@ -42,28 +43,28 @@ export class ContainersController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.containers.update)
|
||||||
@ApiOperation({ summary: 'Update a container' })
|
@ApiOperation({ summary: 'Update a container' })
|
||||||
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateContainerDto) {
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateContainerDto) {
|
||||||
return this.containersService.update(id, dto);
|
return this.containersService.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.containers.delete)
|
||||||
@ApiOperation({ summary: 'Delete a container' })
|
@ApiOperation({ summary: 'Delete a container' })
|
||||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.containersService.remove(id);
|
return this.containersService.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/assign-wagon')
|
@Post(':id/assign-wagon')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.containers.update)
|
||||||
@ApiOperation({ summary: 'Assign container to a wagon' })
|
@ApiOperation({ summary: 'Assign container to a wagon' })
|
||||||
assignToWagon(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignContainerToWagonDto) {
|
assignToWagon(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignContainerToWagonDto) {
|
||||||
return this.containersService.assignToWagon(id, dto);
|
return this.containersService.assignToWagon(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/unassign-wagon')
|
@Post(':id/unassign-wagon')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.containers.update)
|
||||||
@ApiOperation({ summary: 'Unassign container from wagon' })
|
@ApiOperation({ summary: 'Unassign container from wagon' })
|
||||||
unassignFromWagon(@Param('id', ParseUUIDPipe) id: string) {
|
unassignFromWagon(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.containersService.unassignFromWagon(id);
|
return this.containersService.unassignFromWagon(id);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query } from
|
|||||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { FleetManage, StaffReference } from '../../common/booking-guards';
|
import { FleetManage, StaffReference } from '../../common/booking-guards';
|
||||||
|
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||||
import { CreateLocomotiveDto } from './dto/create-locomotive.dto';
|
import { CreateLocomotiveDto } from './dto/create-locomotive.dto';
|
||||||
import { FilterLocomotivesDto } from './dto/filter-locomotives.dto';
|
import { FilterLocomotivesDto } from './dto/filter-locomotives.dto';
|
||||||
import { UpdateLocomotiveDto } from './dto/update-locomotive.dto';
|
import { UpdateLocomotiveDto } from './dto/update-locomotive.dto';
|
||||||
@@ -31,21 +32,21 @@ export class LocomotivesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.locomotives.create)
|
||||||
@ApiOperation({ summary: 'Create a locomotive' })
|
@ApiOperation({ summary: 'Create a locomotive' })
|
||||||
create(@Body() dto: CreateLocomotiveDto) {
|
create(@Body() dto: CreateLocomotiveDto) {
|
||||||
return this.locomotivesService.create(dto);
|
return this.locomotivesService.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.locomotives.update)
|
||||||
@ApiOperation({ summary: 'Update a locomotive' })
|
@ApiOperation({ summary: 'Update a locomotive' })
|
||||||
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateLocomotiveDto) {
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateLocomotiveDto) {
|
||||||
return this.locomotivesService.update(id, dto);
|
return this.locomotivesService.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/decommission')
|
@Post(':id/decommission')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.locomotives.delete)
|
||||||
@ApiOperation({ summary: 'Decommission a locomotive' })
|
@ApiOperation({ summary: 'Decommission a locomotive' })
|
||||||
decommission(@Param('id', ParseUUIDPipe) id: string) {
|
decommission(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.locomotivesService.decommission(id);
|
return this.locomotivesService.decommission(id);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query
|
|||||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { FleetManage, FleetView } from '../../common/booking-guards';
|
import { FleetManage, FleetView } from '../../common/booking-guards';
|
||||||
|
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||||
import { CreateRouteDto } from './dto/create-route.dto';
|
import { CreateRouteDto } from './dto/create-route.dto';
|
||||||
import { FilterRoutesDto } from './dto/filter-routes.dto';
|
import { FilterRoutesDto } from './dto/filter-routes.dto';
|
||||||
import { UpdateRouteDto } from './dto/update-route.dto';
|
import { UpdateRouteDto } from './dto/update-route.dto';
|
||||||
@@ -10,7 +11,7 @@ import { RoutesService } from './routes.service';
|
|||||||
@ApiTags('routes')
|
@ApiTags('routes')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Controller('routes')
|
@Controller('routes')
|
||||||
@FleetView()
|
@FleetView(FREIGHT_PERMS.routes.view)
|
||||||
export class RoutesController {
|
export class RoutesController {
|
||||||
constructor(private readonly routesService: RoutesService) {}
|
constructor(private readonly routesService: RoutesService) {}
|
||||||
|
|
||||||
@@ -27,21 +28,21 @@ export class RoutesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.routes.create)
|
||||||
@ApiOperation({ summary: 'Create route' })
|
@ApiOperation({ summary: 'Create route' })
|
||||||
create(@Body() dto: CreateRouteDto) {
|
create(@Body() dto: CreateRouteDto) {
|
||||||
return this.routesService.create(dto);
|
return this.routesService.create(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.routes.update)
|
||||||
@ApiOperation({ summary: 'Update route' })
|
@ApiOperation({ summary: 'Update route' })
|
||||||
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateRouteDto) {
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateRouteDto) {
|
||||||
return this.routesService.update(id, dto);
|
return this.routesService.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.routes.delete)
|
||||||
@ApiOperation({ summary: 'Deactivate route' })
|
@ApiOperation({ summary: 'Deactivate route' })
|
||||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.routesService.deactivate(id);
|
return this.routesService.deactivate(id);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
|
|
||||||
import { FleetManage, FleetView } from '../../common/booking-guards';
|
import { FleetManage, FleetView } from '../../common/booking-guards';
|
||||||
|
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||||
import { AssignTrainWagonsDto } from './dto/assign-train-wagons.dto';
|
import { AssignTrainWagonsDto } from './dto/assign-train-wagons.dto';
|
||||||
import { BuildTrainDto } from './dto/build-train.dto';
|
import { BuildTrainDto } from './dto/build-train.dto';
|
||||||
import { ListBuiltTrainsQueryDto } from './dto/list-built-trains-query.dto';
|
import { ListBuiltTrainsQueryDto } from './dto/list-built-trains-query.dto';
|
||||||
@@ -27,12 +28,12 @@ import { TrainBuilderService } from './train-builder.service';
|
|||||||
@ApiTags('train-builder')
|
@ApiTags('train-builder')
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@Controller('train-builder')
|
@Controller('train-builder')
|
||||||
@FleetView()
|
@FleetView(FREIGHT_PERMS.trains.view)
|
||||||
export class TrainBuilderController {
|
export class TrainBuilderController {
|
||||||
constructor(private readonly trainBuilderService: TrainBuilderService) {}
|
constructor(private readonly trainBuilderService: TrainBuilderService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.create)
|
||||||
@ApiOperation({ summary: 'Build a train: code + yard + 2+ locomotives (+ optional wagons)' })
|
@ApiOperation({ summary: 'Build a train: code + yard + 2+ locomotives (+ optional wagons)' })
|
||||||
build(@Body() dto: BuildTrainDto) {
|
build(@Body() dto: BuildTrainDto) {
|
||||||
return this.trainBuilderService.buildTrain(dto);
|
return this.trainBuilderService.buildTrain(dto);
|
||||||
@@ -60,7 +61,7 @@ export class TrainBuilderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Put(':id/locomotives')
|
@Put(':id/locomotives')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.update)
|
||||||
@ApiOperation({ summary: 'Replace the locomotive set (minimum 2, same yard)' })
|
@ApiOperation({ summary: 'Replace the locomotive set (minimum 2, same yard)' })
|
||||||
setLocomotives(
|
setLocomotives(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@@ -70,7 +71,7 @@ export class TrainBuilderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/details')
|
@Patch(':id/details')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.update)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: "Edit the train's name and fixed import/export run numbers",
|
summary: "Edit the train's name and fixed import/export run numbers",
|
||||||
})
|
})
|
||||||
@@ -82,7 +83,7 @@ export class TrainBuilderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id/yard')
|
@Patch(':id/yard')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.update)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Relocate the train — its locomotives and wagons move to the new yard with it',
|
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')
|
@Post(':id/wagons')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.assignWagons)
|
||||||
@ApiOperation({ summary: "Append AVAILABLE wagons from the train's yard to the consist" })
|
@ApiOperation({ summary: "Append AVAILABLE wagons from the train's yard to the consist" })
|
||||||
assignWagons(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignTrainWagonsDto) {
|
assignWagons(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignTrainWagonsDto) {
|
||||||
return this.trainBuilderService.assignWagons(id, dto);
|
return this.trainBuilderService.assignWagons(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id/wagons/:wagonId')
|
@Delete(':id/wagons/:wagonId')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.assignWagons)
|
||||||
@ApiOperation({ summary: 'Detach one wagon from the consist' })
|
@ApiOperation({ summary: 'Detach one wagon from the consist' })
|
||||||
removeWagon(
|
removeWagon(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@@ -108,7 +109,7 @@ export class TrainBuilderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/wagons/:wagonId/maintenance')
|
@Post(':id/wagons/:wagonId/maintenance')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.assignWagons)
|
||||||
@ApiOperation({ summary: 'Detach one wagon and move it to MAINTENANCE status' })
|
@ApiOperation({ summary: 'Detach one wagon and move it to MAINTENANCE status' })
|
||||||
sendWagonToMaintenance(
|
sendWagonToMaintenance(
|
||||||
@Param('id', ParseUUIDPipe) id: string,
|
@Param('id', ParseUUIDPipe) id: string,
|
||||||
@@ -118,14 +119,14 @@ export class TrainBuilderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/reorder-wagons')
|
@Post(':id/reorder-wagons')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.assignWagons)
|
||||||
@ApiOperation({ summary: 'Persist a drag-reorder of the full consist' })
|
@ApiOperation({ summary: 'Persist a drag-reorder of the full consist' })
|
||||||
reorderWagons(@Param('id', ParseUUIDPipe) id: string, @Body() dto: ReorderTrainWagonsDto) {
|
reorderWagons(@Param('id', ParseUUIDPipe) id: string, @Body() dto: ReorderTrainWagonsDto) {
|
||||||
return this.trainBuilderService.reorderWagons(id, dto);
|
return this.trainBuilderService.reorderWagons(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/deactivate')
|
@Post(':id/deactivate')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.update)
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
summary: 'Deactivate the train (park it) — only allowed with no active schedule',
|
summary: 'Deactivate the train (park it) — only allowed with no active schedule',
|
||||||
})
|
})
|
||||||
@@ -134,14 +135,14 @@ export class TrainBuilderController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/activate')
|
@Post(':id/activate')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.update)
|
||||||
@ApiOperation({ summary: 'Reactivate a deactivated train back to AVAILABLE' })
|
@ApiOperation({ summary: 'Reactivate a deactivated train back to AVAILABLE' })
|
||||||
activate(@Param('id', ParseUUIDPipe) id: string) {
|
activate(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.trainBuilderService.activate(id);
|
return this.trainBuilderService.activate(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.delete)
|
||||||
@HttpCode(HttpStatus.NO_CONTENT)
|
@HttpCode(HttpStatus.NO_CONTENT)
|
||||||
@ApiOperation({ summary: 'Disband the train (release wagons and locomotives)' })
|
@ApiOperation({ summary: 'Disband the train (release wagons and locomotives)' })
|
||||||
disband(@Param('id', ParseUUIDPipe) id: string) {
|
disband(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
|
|||||||
@@ -12,18 +12,19 @@ import {
|
|||||||
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
import { ApiOperation, ApiTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
import { FleetManage, FleetView } from "../../common/booking-guards";
|
import { FleetManage, FleetView } from "../../common/booking-guards";
|
||||||
|
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||||
import { CreateTrainDto } from "./dto/create-train.dto";
|
import { CreateTrainDto } from "./dto/create-train.dto";
|
||||||
import { UpdateTrainDto } from "./dto/update-train.dto";
|
import { UpdateTrainDto } from "./dto/update-train.dto";
|
||||||
import { TrainsService } from "./trains.service";
|
import { TrainsService } from "./trains.service";
|
||||||
|
|
||||||
@ApiTags("trains")
|
@ApiTags("trains")
|
||||||
@Controller("trains")
|
@Controller("trains")
|
||||||
@FleetView()
|
@FleetView(FREIGHT_PERMS.trains.view)
|
||||||
export class TrainsController {
|
export class TrainsController {
|
||||||
constructor(private readonly trainsService: TrainsService) {}
|
constructor(private readonly trainsService: TrainsService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.create)
|
||||||
@ApiOperation({ summary: "Register a new train" })
|
@ApiOperation({ summary: "Register a new train" })
|
||||||
create(@Body() dto: CreateTrainDto) {
|
create(@Body() dto: CreateTrainDto) {
|
||||||
return this.trainsService.create(dto);
|
return this.trainsService.create(dto);
|
||||||
@@ -42,14 +43,14 @@ export class TrainsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(":id")
|
@Patch(":id")
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.update)
|
||||||
@ApiOperation({ summary: "Update a train" })
|
@ApiOperation({ summary: "Update a train" })
|
||||||
update(@Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateTrainDto) {
|
update(@Param("id", ParseUUIDPipe) id: string, @Body() dto: UpdateTrainDto) {
|
||||||
return this.trainsService.update(id, dto);
|
return this.trainsService.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(":id")
|
@Delete(":id")
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.delete)
|
||||||
@ApiOperation({ summary: "Delete a train" })
|
@ApiOperation({ summary: "Delete a train" })
|
||||||
remove(@Param("id", ParseUUIDPipe) id: string) {
|
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
return this.trainsService.remove(id);
|
return this.trainsService.remove(id);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
WagonTransferHistoryAll,
|
WagonTransferHistoryAll,
|
||||||
WagonTransferRequest,
|
WagonTransferRequest,
|
||||||
} from '../../common/booking-guards';
|
} from '../../common/booking-guards';
|
||||||
|
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||||
import { BulkFulfillTransferRequestsDto } from './dto/bulk-fulfill-transfer-requests.dto';
|
import { BulkFulfillTransferRequestsDto } from './dto/bulk-fulfill-transfer-requests.dto';
|
||||||
import { CreateTransferRequestDto } from './dto/create-transfer-request.dto';
|
import { CreateTransferRequestDto } from './dto/create-transfer-request.dto';
|
||||||
import { FulfillTransferRequestDto } from './dto/fulfill-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')
|
@ApiTags('wagon-transfer-requests')
|
||||||
@Controller('wagon-transfer-requests')
|
@Controller('wagon-transfer-requests')
|
||||||
@FleetView()
|
@FleetView(FREIGHT_PERMS.wagons.view)
|
||||||
export class WagonTransferRequestsController {
|
export class WagonTransferRequestsController {
|
||||||
constructor(private readonly service: WagonTransferRequestsService) {}
|
constructor(private readonly service: WagonTransferRequestsService) {}
|
||||||
|
|
||||||
@@ -110,7 +111,7 @@ export class WagonTransferRequestsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/cancel')
|
@Post(':id/cancel')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.wagons.transferRequest)
|
||||||
@ApiOperation({ summary: 'Withdraw a pending transfer request' })
|
@ApiOperation({ summary: 'Withdraw a pending transfer request' })
|
||||||
cancel(@Param('id', ParseUUIDPipe) id: string) {
|
cancel(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.service.cancelRequest(id);
|
return this.service.cancelRequest(id);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
|||||||
import { CurrentUser } from '@edr/api-common';
|
import { CurrentUser } from '@edr/api-common';
|
||||||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
||||||
import { FleetManage, FleetView, StaffReference } from '../../common/booking-guards';
|
import { FleetManage, FleetView, StaffReference } from '../../common/booking-guards';
|
||||||
|
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||||||
import { CreateWagonDto } from './dto/create-wagon.dto';
|
import { CreateWagonDto } from './dto/create-wagon.dto';
|
||||||
import { ListWagonsQueryDto } from './dto/list-wagons-query.dto';
|
import { ListWagonsQueryDto } from './dto/list-wagons-query.dto';
|
||||||
import { UpdateWagonDto } from './dto/update-wagon.dto';
|
import { UpdateWagonDto } from './dto/update-wagon.dto';
|
||||||
@@ -31,7 +32,7 @@ export class WagonsController {
|
|||||||
constructor(private readonly wagonsService: WagonsService) {}
|
constructor(private readonly wagonsService: WagonsService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.wagons.create)
|
||||||
@ApiOperation({ summary: 'Create a new wagon' })
|
@ApiOperation({ summary: 'Create a new wagon' })
|
||||||
create(@Body() dto: CreateWagonDto) {
|
create(@Body() dto: CreateWagonDto) {
|
||||||
return this.wagonsService.create(dto);
|
return this.wagonsService.create(dto);
|
||||||
@@ -61,42 +62,42 @@ export class WagonsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.wagons.update)
|
||||||
@ApiOperation({ summary: 'Update a wagon' })
|
@ApiOperation({ summary: 'Update a wagon' })
|
||||||
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWagonDto) {
|
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWagonDto) {
|
||||||
return this.wagonsService.update(id, dto);
|
return this.wagonsService.update(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@Delete(':id')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.wagons.delete)
|
||||||
@ApiOperation({ summary: 'Delete a wagon' })
|
@ApiOperation({ summary: 'Delete a wagon' })
|
||||||
remove(@Param('id', ParseUUIDPipe) id: string) {
|
remove(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.wagonsService.remove(id);
|
return this.wagonsService.remove(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/assign-train')
|
@Post(':id/assign-train')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.assignWagons)
|
||||||
@ApiOperation({ summary: 'Assign wagon to a train' })
|
@ApiOperation({ summary: 'Assign wagon to a train' })
|
||||||
assignToTrain(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignWagonToTrainDto) {
|
assignToTrain(@Param('id', ParseUUIDPipe) id: string, @Body() dto: AssignWagonToTrainDto) {
|
||||||
return this.wagonsService.assignToTrain(id, dto);
|
return this.wagonsService.assignToTrain(id, dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post(':id/unassign-train')
|
@Post(':id/unassign-train')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.assignWagons)
|
||||||
@ApiOperation({ summary: 'Unassign wagon from train' })
|
@ApiOperation({ summary: 'Unassign wagon from train' })
|
||||||
unassignFromTrain(@Param('id', ParseUUIDPipe) id: string) {
|
unassignFromTrain(@Param('id', ParseUUIDPipe) id: string) {
|
||||||
return this.wagonsService.unassignFromTrain(id);
|
return this.wagonsService.unassignFromTrain(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('bulk-transfer')
|
@Post('bulk-transfer')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.wagons.update)
|
||||||
@ApiOperation({ summary: 'Transfer multiple wagons to a destination yard' })
|
@ApiOperation({ summary: 'Transfer multiple wagons to a destination yard' })
|
||||||
bulkTransfer(@Body() dto: BulkTransferWagonsDto, @CurrentUser() user: TCurrentUser) {
|
bulkTransfer(@Body() dto: BulkTransferWagonsDto, @CurrentUser() user: TCurrentUser) {
|
||||||
return this.wagonsService.bulkTransfer(dto, user?.id);
|
return this.wagonsService.bulkTransfer(dto, user?.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('bulk-status')
|
@Post('bulk-status')
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.wagons.update)
|
||||||
@ApiOperation({ summary: 'Set the status of multiple wagons' })
|
@ApiOperation({ summary: 'Set the status of multiple wagons' })
|
||||||
bulkSetStatus(@Body() dto: BulkSetWagonStatusDto) {
|
bulkSetStatus(@Body() dto: BulkSetWagonStatusDto) {
|
||||||
return this.wagonsService.bulkSetStatus(dto);
|
return this.wagonsService.bulkSetStatus(dto);
|
||||||
@@ -105,12 +106,12 @@ export class WagonsController {
|
|||||||
|
|
||||||
// Separate controller for train‑specific reorder (registered in module)
|
// Separate controller for train‑specific reorder (registered in module)
|
||||||
@Controller('trains/:trainId/reorder-wagons')
|
@Controller('trains/:trainId/reorder-wagons')
|
||||||
@FleetView()
|
@FleetView(FREIGHT_PERMS.trains.view)
|
||||||
export class TrainWagonsReorderController {
|
export class TrainWagonsReorderController {
|
||||||
constructor(private readonly wagonsService: WagonsService) {}
|
constructor(private readonly wagonsService: WagonsService) {}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@FleetManage()
|
@FleetManage(FREIGHT_PERMS.trains.assignWagons)
|
||||||
@ApiOperation({ summary: 'Reorder wagons of a train' })
|
@ApiOperation({ summary: 'Reorder wagons of a train' })
|
||||||
reorder(@Param('trainId', ParseUUIDPipe) trainId: string, @Body() dto: ReorderWagonsDto) {
|
reorder(@Param('trainId', ParseUUIDPipe) trainId: string, @Body() dto: ReorderWagonsDto) {
|
||||||
return this.wagonsService.reorderWagons(trainId, dto);
|
return this.wagonsService.reorderWagons(trainId, dto);
|
||||||
|
|||||||
@@ -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-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-000000000003', 'edr_freight_app:cargoes:update', 'Update cargo'),
|
||||||
perm('e1f00001-0001-4000-8000-000000000004', 'edr_freight_app:cargoes:delete', 'Delete 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
|
// G. Fleet — road & telemetry
|
||||||
@@ -493,6 +495,10 @@ export const FREIGHT_PERMS = {
|
|||||||
update: 'edr_freight_app:cargoes:update',
|
update: 'edr_freight_app:cargoes:update',
|
||||||
delete: 'edr_freight_app:cargoes:delete',
|
delete: 'edr_freight_app:cargoes:delete',
|
||||||
},
|
},
|
||||||
|
consignments: {
|
||||||
|
view: 'edr_freight_app:consignments:view',
|
||||||
|
create: 'edr_freight_app:consignments:create',
|
||||||
|
},
|
||||||
vehicles: {
|
vehicles: {
|
||||||
view: 'edr_freight_app:vehicles:view',
|
view: 'edr_freight_app:vehicles:view',
|
||||||
create: 'edr_freight_app:vehicles:create',
|
create: 'edr_freight_app:vehicles:create',
|
||||||
|
|||||||
@@ -271,19 +271,19 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
|||||||
label: "Routes",
|
label: "Routes",
|
||||||
href: "/dashboard/routes",
|
href: "/dashboard/routes",
|
||||||
icon: <Network />,
|
icon: <Network />,
|
||||||
permission: FREIGHT_PERMS.fleet.view,
|
permission: [FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Locomotives",
|
label: "Locomotives",
|
||||||
href: "/dashboard/locomotives",
|
href: "/dashboard/locomotives",
|
||||||
icon: <Train />,
|
icon: <Train />,
|
||||||
permission: FREIGHT_PERMS.fleet.view,
|
permission: [FREIGHT_PERMS.locomotives.view, FREIGHT_PERMS.fleet.view],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Train Builder",
|
label: "Train Builder",
|
||||||
href: "/dashboard/train-builder",
|
href: "/dashboard/train-builder",
|
||||||
icon: <Hammer />,
|
icon: <Hammer />,
|
||||||
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",
|
label: "Wagons",
|
||||||
href: "/dashboard/wagons",
|
href: "/dashboard/wagons",
|
||||||
icon: <Truck />,
|
icon: <Truck />,
|
||||||
permission: FREIGHT_PERMS.fleet.view,
|
permission: [FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Vehicles",
|
label: "Vehicles",
|
||||||
@@ -1111,7 +1111,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="routes"
|
path="routes"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<RoutesPage />
|
<RoutesPage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1119,7 +1119,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="locomotives"
|
path="locomotives"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.locomotives.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<FleetResourcePage />
|
<FleetResourcePage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1127,7 +1127,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="trains"
|
path="trains"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<FleetResourcePage />
|
<FleetResourcePage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1135,7 +1135,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="trains/:id"
|
path="trains/:id"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<TrainDetailPage />
|
<TrainDetailPage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1143,7 +1143,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="train-builder"
|
path="train-builder"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<TrainBuilderListPage />
|
<TrainBuilderListPage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1151,7 +1151,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="train-builder/:id"
|
path="train-builder/:id"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<TrainBuilderDetailPage />
|
<TrainBuilderDetailPage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1159,7 +1159,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="wagons"
|
path="wagons"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<FleetResourcePage />
|
<FleetResourcePage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1167,7 +1167,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="containers"
|
path="containers"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.containers.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<FleetResourcePage />
|
<FleetResourcePage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1175,7 +1175,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="cargoes"
|
path="cargoes"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.cargoes.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<FleetResourcePage />
|
<FleetResourcePage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1261,7 +1261,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="routes"
|
path="routes"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.routes.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<RoutesPage />
|
<RoutesPage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1349,7 +1349,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="locomotives"
|
path="locomotives"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.locomotives.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<FleetResourcePage />
|
<FleetResourcePage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1357,7 +1357,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="trains"
|
path="trains"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<FleetResourcePage />
|
<FleetResourcePage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1365,7 +1365,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="trains/:id"
|
path="trains/:id"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<TrainDetailPage />
|
<TrainDetailPage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1373,7 +1373,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="train-builder"
|
path="train-builder"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<TrainBuilderListPage />
|
<TrainBuilderListPage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1381,7 +1381,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="train-builder/:id"
|
path="train-builder/:id"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.trains.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<TrainBuilderDetailPage />
|
<TrainBuilderDetailPage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1389,7 +1389,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="wagons"
|
path="wagons"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.wagons.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<FleetResourcePage />
|
<FleetResourcePage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1397,7 +1397,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="containers"
|
path="containers"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.containers.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<FleetResourcePage />
|
<FleetResourcePage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
@@ -1405,7 +1405,7 @@ const App = () => {
|
|||||||
<Route
|
<Route
|
||||||
path="cargoes"
|
path="cargoes"
|
||||||
element={
|
element={
|
||||||
<RequirePermission permission={FREIGHT_PERMS.fleet.view}>
|
<RequirePermission permission={[FREIGHT_PERMS.cargoes.view, FREIGHT_PERMS.fleet.view]}>
|
||||||
<FleetResourcePage />
|
<FleetResourcePage />
|
||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,8 +19,9 @@ export interface FleetCardGridProps {
|
|||||||
pageCount: number;
|
pageCount: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
onPaginationChange: OnChangeFn<PaginationState>;
|
onPaginationChange: OnChangeFn<PaginationState>;
|
||||||
onEdit: (record: FleetRecord) => void;
|
/** Omit to hide the action (caller lacks the update/delete permission). */
|
||||||
onRemove: (record: FleetRecord) => void;
|
onEdit?: (record: FleetRecord) => void;
|
||||||
|
onRemove?: (record: FleetRecord) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FleetCardGrid = ({
|
const FleetCardGrid = ({
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import type { FleetRecord } from "@/services/fleet/fleet.service";
|
|||||||
export interface FleetRecordActionsProps {
|
export interface FleetRecordActionsProps {
|
||||||
record: FleetRecord;
|
record: FleetRecord;
|
||||||
config: FleetResourceConfig;
|
config: FleetResourceConfig;
|
||||||
onEdit: (record: FleetRecord) => void;
|
/** Omit to hide the action (caller lacks the update/delete permission). */
|
||||||
onRemove: (record: FleetRecord) => void;
|
onEdit?: (record: FleetRecord) => void;
|
||||||
|
onRemove?: (record: FleetRecord) => void;
|
||||||
onAssignDriver?: (record: FleetRecord) => void;
|
onAssignDriver?: (record: FleetRecord) => void;
|
||||||
onHistory?: (record: FleetRecord) => void;
|
onHistory?: (record: FleetRecord) => void;
|
||||||
onViewDetail?: (record: FleetRecord) => void;
|
onViewDetail?: (record: FleetRecord) => void;
|
||||||
@@ -42,6 +43,17 @@ const FleetRecordActions = ({
|
|||||||
navigate(config.detailPath.replace(":id", String(record.id)));
|
navigate(config.detailPath.replace(":id", String(record.id)));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
!onEdit &&
|
||||||
|
!onRemove &&
|
||||||
|
!showDetail &&
|
||||||
|
!showViewDetail &&
|
||||||
|
!showHistory &&
|
||||||
|
!(isVehicle && onAssignDriver)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (layout === "compact") {
|
if (layout === "compact") {
|
||||||
return (
|
return (
|
||||||
<Menu position="bottom-end" withinPortal shadow="md">
|
<Menu position="bottom-end" withinPortal shadow="md">
|
||||||
@@ -61,12 +73,14 @@ const FleetRecordActions = ({
|
|||||||
Assign Driver
|
Assign Driver
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
<MenuItem
|
{onEdit ? (
|
||||||
onClick={() => onEdit(record)}
|
<MenuItem
|
||||||
leftSection={<Edit2 size={14} strokeWidth={2} />}
|
onClick={() => onEdit(record)}
|
||||||
>
|
leftSection={<Edit2 size={14} strokeWidth={2} />}
|
||||||
Edit
|
>
|
||||||
</MenuItem>
|
Edit
|
||||||
|
</MenuItem>
|
||||||
|
) : null}
|
||||||
{showViewDetail ? (
|
{showViewDetail ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => onViewDetail?.(record)}
|
onClick={() => onViewDetail?.(record)}
|
||||||
@@ -91,13 +105,15 @@ const FleetRecordActions = ({
|
|||||||
View details
|
View details
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
<MenuItem
|
{onRemove ? (
|
||||||
color="red"
|
<MenuItem
|
||||||
onClick={() => onRemove(record)}
|
color="red"
|
||||||
leftSection={<Trash2 size={14} strokeWidth={2} />}
|
onClick={() => onRemove(record)}
|
||||||
>
|
leftSection={<Trash2 size={14} strokeWidth={2} />}
|
||||||
{removeLabel}
|
>
|
||||||
</MenuItem>
|
{removeLabel}
|
||||||
|
</MenuItem>
|
||||||
|
) : null}
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
@@ -121,12 +137,14 @@ const FleetRecordActions = ({
|
|||||||
Assign Driver
|
Assign Driver
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
<MenuItem
|
{onEdit ? (
|
||||||
onClick={() => onEdit(record)}
|
<MenuItem
|
||||||
leftSection={<Edit2 size={14} strokeWidth={2} />}
|
onClick={() => onEdit(record)}
|
||||||
>
|
leftSection={<Edit2 size={14} strokeWidth={2} />}
|
||||||
Edit
|
>
|
||||||
</MenuItem>
|
Edit
|
||||||
|
</MenuItem>
|
||||||
|
) : null}
|
||||||
{showViewDetail ? (
|
{showViewDetail ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => onViewDetail?.(record)}
|
onClick={() => onViewDetail?.(record)}
|
||||||
@@ -143,13 +161,15 @@ const FleetRecordActions = ({
|
|||||||
View details
|
View details
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
) : null}
|
) : null}
|
||||||
<MenuItem
|
{onRemove ? (
|
||||||
color="red"
|
<MenuItem
|
||||||
onClick={() => onRemove(record)}
|
color="red"
|
||||||
leftSection={<Trash2 size={14} strokeWidth={2} />}
|
onClick={() => onRemove(record)}
|
||||||
>
|
leftSection={<Trash2 size={14} strokeWidth={2} />}
|
||||||
{removeLabel}
|
>
|
||||||
</MenuItem>
|
{removeLabel}
|
||||||
|
</MenuItem>
|
||||||
|
) : null}
|
||||||
</Menu.Dropdown>
|
</Menu.Dropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -507,6 +507,31 @@ export function canViewFleet(user: AuthUser | null | undefined): boolean {
|
|||||||
return hasPermission(user, FREIGHT_PERMS.fleet.view);
|
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 {
|
export function isFreightAdmin(user: AuthUser | null | undefined): boolean {
|
||||||
return hasPermission(user, FREIGHT_PERMS.admin);
|
return hasPermission(user, FREIGHT_PERMS.admin);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import { Box, Button, Card, Container, Group, Modal, Select, Stack, Text, Title
|
|||||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
|
import { useAuth } from "@/auth/useAuth";
|
||||||
|
import { canFleetAction } from "@/lib/permissions";
|
||||||
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
import Breadcrumbs from "@/components/ui/Breadcrumbs";
|
||||||
import { Inbox, Plus, Warehouse } from "lucide-react";
|
import { Inbox, Plus, Warehouse } from "lucide-react";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
@@ -37,6 +39,10 @@ const FleetResourcePage = () => {
|
|||||||
const slug = getFleetSlugFromPath(location.pathname) ?? DEFAULT_SLUG;
|
const slug = getFleetSlugFromPath(location.pathname) ?? DEFAULT_SLUG;
|
||||||
const config = getFleetResource(slug);
|
const config = getFleetResource(slug);
|
||||||
const { toast } = useToast();
|
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 { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -278,12 +284,16 @@ const FleetResourcePage = () => {
|
|||||||
record={row.original}
|
record={row.original}
|
||||||
config={config}
|
config={config}
|
||||||
layout="compact"
|
layout="compact"
|
||||||
onEdit={(record) => {
|
onEdit={
|
||||||
setEditing(record);
|
canUpdate
|
||||||
setFormOpen(true);
|
? (record) => {
|
||||||
}}
|
setEditing(record);
|
||||||
onRemove={setRemoveTarget}
|
setFormOpen(true);
|
||||||
onAssignDriver={setAssigningDriver}
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onRemove={canDelete ? setRemoveTarget : undefined}
|
||||||
|
onAssignDriver={canUpdate ? setAssigningDriver : undefined}
|
||||||
onHistory={setHistoryTarget}
|
onHistory={setHistoryTarget}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -291,7 +301,7 @@ const FleetResourcePage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return base;
|
return base;
|
||||||
}, [config, dynamicOptions.yards]);
|
}, [config, dynamicOptions.yards, canUpdate, canDelete]);
|
||||||
|
|
||||||
const tableStatus = isLoading ? "loading" : isError ? "error" : "success";
|
const tableStatus = isLoading ? "loading" : isError ? "error" : "success";
|
||||||
|
|
||||||
@@ -390,15 +400,17 @@ const FleetResourcePage = () => {
|
|||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
{slug === "wagons" ? (
|
{slug === "wagons" ? (
|
||||||
<>
|
<>
|
||||||
<Button
|
{canUpdate ? (
|
||||||
variant="light"
|
<Button
|
||||||
color="edr-green"
|
variant="light"
|
||||||
leftSection={<Warehouse size={16} />}
|
color="edr-green"
|
||||||
styles={{ label: { fontWeight: 500 } }}
|
leftSection={<Warehouse size={16} />}
|
||||||
onClick={() => setWagonWorkspaceOpen(true)}
|
styles={{ label: { fontWeight: 500 } }}
|
||||||
>
|
onClick={() => setWagonWorkspaceOpen(true)}
|
||||||
Yard Workspace
|
>
|
||||||
</Button>
|
Yard Workspace
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
color="grape"
|
color="grape"
|
||||||
@@ -410,12 +422,14 @@ const FleetResourcePage = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<Button leftSection={<Plus size={16} />} styles={{ label: { fontWeight: 500 } }} onClick={() => {
|
{canCreate ? (
|
||||||
setEditing(null);
|
<Button leftSection={<Plus size={16} />} styles={{ label: { fontWeight: 500 } }} onClick={() => {
|
||||||
setFormOpen(true);
|
setEditing(null);
|
||||||
}}>
|
setFormOpen(true);
|
||||||
{config.addLabel}
|
}}>
|
||||||
</Button>
|
{config.addLabel}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
@@ -529,11 +543,15 @@ const FleetResourcePage = () => {
|
|||||||
pageCount={pageCount}
|
pageCount={pageCount}
|
||||||
totalCount={filteredRows.length}
|
totalCount={filteredRows.length}
|
||||||
onPaginationChange={setPagination}
|
onPaginationChange={setPagination}
|
||||||
onEdit={(record) => {
|
onEdit={
|
||||||
setEditing(record);
|
canUpdate
|
||||||
setFormOpen(true);
|
? (record) => {
|
||||||
}}
|
setEditing(record);
|
||||||
onRemove={setRemoveTarget}
|
setFormOpen(true);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onRemove={canDelete ? setRemoveTarget : undefined}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ import RuleEngineListFooter from "@/components/ruleEngine/RuleEngineListFooter";
|
|||||||
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
import { ruleEngineTable } from "@/components/ruleEngine/ruleEngineStyles";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import { ruleEngineService } from "@/services/ruleEngine/ruleEngine.service";
|
import { ruleEngineService } from "@/services/ruleEngine/ruleEngine.service";
|
||||||
|
import { useAuth } from "@/auth/useAuth";
|
||||||
|
import { canFleetAction } from "@/lib/permissions";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
import {
|
import {
|
||||||
formatRouteLabel,
|
formatRouteLabel,
|
||||||
@@ -160,6 +162,10 @@ export default function RoutesPage() {
|
|||||||
const { viewMode, setViewMode } = useFleetViewMode("routes");
|
const { viewMode, setViewMode } = useFleetViewMode("routes");
|
||||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||||
const { toast } = useToast();
|
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 routesQuery = useQuery(api.routes.list.queryOptions());
|
||||||
const yardsQuery = useQuery(api.routes.yards.queryOptions());
|
const yardsQuery = useQuery(api.routes.yards.queryOptions());
|
||||||
@@ -441,26 +447,30 @@ export default function RoutesPage() {
|
|||||||
<Eye size={16} />
|
<Eye size={16} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="Edit">
|
{canUpdate ? (
|
||||||
<ActionIcon variant="subtle" color="gray" onClick={() => openEdit(row.original)}>
|
<Tooltip label="Edit">
|
||||||
<Edit size={16} />
|
<ActionIcon variant="subtle" color="gray" onClick={() => openEdit(row.original)}>
|
||||||
</ActionIcon>
|
<Edit size={16} />
|
||||||
</Tooltip>
|
</ActionIcon>
|
||||||
<Tooltip label="Mark stop working">
|
</Tooltip>
|
||||||
<ActionIcon
|
) : null}
|
||||||
variant="subtle"
|
{canDelete ? (
|
||||||
color="red"
|
<Tooltip label="Mark stop working">
|
||||||
disabled={row.original.status === "STOP_WORKING" || deactivateMutation.isPending}
|
<ActionIcon
|
||||||
onClick={() => handleDeactivate(row.original)}
|
variant="subtle"
|
||||||
>
|
color="red"
|
||||||
<Trash2 size={16} />
|
disabled={row.original.status === "STOP_WORKING" || deactivateMutation.isPending}
|
||||||
</ActionIcon>
|
onClick={() => handleDeactivate(row.original)}
|
||||||
</Tooltip>
|
>
|
||||||
|
<Trash2 size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [deactivateMutation.isPending]);
|
}, [deactivateMutation.isPending, canUpdate, canDelete]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
@@ -468,9 +478,11 @@ export default function RoutesPage() {
|
|||||||
title="Routes"
|
title="Routes"
|
||||||
subtitle="Define rail corridors, segment distances, and operational status for train scheduling."
|
subtitle="Define rail corridors, segment distances, and operational status for train scheduling."
|
||||||
action={
|
action={
|
||||||
<Button leftSection={<Plus size={18} />} onClick={openCreate}>
|
canCreate ? (
|
||||||
Add route
|
<Button leftSection={<Plus size={18} />} onClick={openCreate}>
|
||||||
</Button>
|
Add route
|
||||||
|
</Button>
|
||||||
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -555,9 +567,11 @@ export default function RoutesPage() {
|
|||||||
<Button variant="light" size="compact-sm" onClick={() => setViewing(route)}>
|
<Button variant="light" size="compact-sm" onClick={() => setViewing(route)}>
|
||||||
View
|
View
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="light" size="compact-sm" onClick={() => openEdit(route)}>
|
{canUpdate ? (
|
||||||
Edit
|
<Button variant="light" size="compact-sm" onClick={() => openEdit(route)}>
|
||||||
</Button>
|
Edit
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ import {
|
|||||||
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
|
import { TrainCompositionDiagram } from "@/components/trainScheduling/TrainCompositionDiagram";
|
||||||
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
|
import { useAuth } from "@/auth/useAuth";
|
||||||
|
import { canFleetAction, FREIGHT_PERMS, hasPermission } from "@/lib/permissions";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
const parseError = (error: unknown, fallback: string) => {
|
const parseError = (error: unknown, fallback: string) => {
|
||||||
@@ -76,6 +78,12 @@ export default function TrainBuilderDetailPage() {
|
|||||||
const [yardModalOpen, setYardModalOpen] = useState(false);
|
const [yardModalOpen, setYardModalOpen] = useState(false);
|
||||||
const [disbandOpen, setDisbandOpen] = useState(false);
|
const [disbandOpen, setDisbandOpen] = useState(false);
|
||||||
const [deactivateOpen, setDeactivateOpen] = 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(
|
const compositionQuery = useQuery(
|
||||||
api.trainBuilder.composition.queryOptions({ input: { id }, enabled: Boolean(id) }),
|
api.trainBuilder.composition.queryOptions({ input: { id }, enabled: Boolean(id) }),
|
||||||
@@ -162,59 +170,67 @@ export default function TrainBuilderDetailPage() {
|
|||||||
</Group>
|
</Group>
|
||||||
}
|
}
|
||||||
action={
|
action={
|
||||||
<Menu position="bottom-end" withinPortal shadow="md" width={220}>
|
canUpdate || canDelete ? (
|
||||||
<Menu.Target>
|
<Menu position="bottom-end" withinPortal shadow="md" width={220}>
|
||||||
<Button variant="default" rightSection={<MoreHorizontal size={16} />}>
|
<Menu.Target>
|
||||||
Actions
|
<Button variant="default" rightSection={<MoreHorizontal size={16} />}>
|
||||||
</Button>
|
Actions
|
||||||
</Menu.Target>
|
</Button>
|
||||||
<Menu.Dropdown>
|
</Menu.Target>
|
||||||
<Menu.Item
|
<Menu.Dropdown>
|
||||||
leftSection={<Replace size={15} />}
|
{canUpdate ? (
|
||||||
disabled={!composition.editable}
|
<>
|
||||||
onClick={() => setLocoModalOpen(true)}
|
<Menu.Item
|
||||||
>
|
leftSection={<Replace size={15} />}
|
||||||
Change locomotives
|
disabled={!composition.editable}
|
||||||
</Menu.Item>
|
onClick={() => setLocoModalOpen(true)}
|
||||||
<Menu.Item
|
>
|
||||||
leftSection={<MapPin size={15} />}
|
Change locomotives
|
||||||
disabled={!composition.editable}
|
</Menu.Item>
|
||||||
onClick={() => setYardModalOpen(true)}
|
<Menu.Item
|
||||||
>
|
leftSection={<MapPin size={15} />}
|
||||||
Change yard
|
disabled={!composition.editable}
|
||||||
</Menu.Item>
|
onClick={() => setYardModalOpen(true)}
|
||||||
{composition.status === "DEACTIVATED" ? (
|
>
|
||||||
<Menu.Item
|
Change yard
|
||||||
leftSection={<Power size={15} />}
|
</Menu.Item>
|
||||||
disabled={blockingLocomotives.length > 0}
|
{composition.status === "DEACTIVATED" ? (
|
||||||
onClick={() =>
|
<Menu.Item
|
||||||
void withToast(async () => {
|
leftSection={<Power size={15} />}
|
||||||
await activate.mutateAsync(composition.id);
|
disabled={blockingLocomotives.length > 0}
|
||||||
toast({ title: `Train ${composition.code} reactivated` });
|
onClick={() =>
|
||||||
}, "Could not reactivate train")
|
void withToast(async () => {
|
||||||
}
|
await activate.mutateAsync(composition.id);
|
||||||
>
|
toast({ title: `Train ${composition.code} reactivated` });
|
||||||
Reactivate train
|
}, "Could not reactivate train")
|
||||||
</Menu.Item>
|
}
|
||||||
) : (
|
>
|
||||||
<Menu.Item
|
Reactivate train
|
||||||
leftSection={<PowerOff size={15} />}
|
</Menu.Item>
|
||||||
disabled={composition.activeSchedules.length > 0}
|
) : (
|
||||||
onClick={() => setDeactivateOpen(true)}
|
<Menu.Item
|
||||||
>
|
leftSection={<PowerOff size={15} />}
|
||||||
Deactivate train
|
disabled={composition.activeSchedules.length > 0}
|
||||||
</Menu.Item>
|
onClick={() => setDeactivateOpen(true)}
|
||||||
)}
|
>
|
||||||
<Menu.Item
|
Deactivate train
|
||||||
color="red"
|
</Menu.Item>
|
||||||
leftSection={<Trash2 size={15} />}
|
)}
|
||||||
disabled={composition.activeSchedules.length > 0}
|
</>
|
||||||
onClick={() => setDisbandOpen(true)}
|
) : null}
|
||||||
>
|
{canDelete ? (
|
||||||
Disband train
|
<Menu.Item
|
||||||
</Menu.Item>
|
color="red"
|
||||||
</Menu.Dropdown>
|
leftSection={<Trash2 size={15} />}
|
||||||
</Menu>
|
disabled={composition.activeSchedules.length > 0}
|
||||||
|
onClick={() => setDisbandOpen(true)}
|
||||||
|
>
|
||||||
|
Disband train
|
||||||
|
</Menu.Item>
|
||||||
|
) : null}
|
||||||
|
</Menu.Dropdown>
|
||||||
|
</Menu>
|
||||||
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -259,16 +275,18 @@ export default function TrainBuilderDetailPage() {
|
|||||||
.
|
.
|
||||||
</Text>
|
</Text>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Button
|
{canUpdate ? (
|
||||||
size="compact-sm"
|
<Button
|
||||||
variant="light"
|
size="compact-sm"
|
||||||
color="red"
|
variant="light"
|
||||||
leftSection={<Replace size={14} />}
|
color="red"
|
||||||
disabled={!composition.editable}
|
leftSection={<Replace size={14} />}
|
||||||
onClick={() => setLocoModalOpen(true)}
|
disabled={!composition.editable}
|
||||||
>
|
onClick={() => setLocoModalOpen(true)}
|
||||||
Detach & replace locomotives
|
>
|
||||||
</Button>
|
Detach & replace locomotives
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
<Button
|
<Button
|
||||||
size="compact-sm"
|
size="compact-sm"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
@@ -330,7 +348,7 @@ export default function TrainBuilderDetailPage() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Grid gap="lg" align="stretch">
|
<Grid gap="lg" align="stretch">
|
||||||
{composition.editable ? (
|
{composition.editable && canAssign ? (
|
||||||
<Grid.Col span={{ base: 12, md: 5 }}>
|
<Grid.Col span={{ base: 12, md: 5 }}>
|
||||||
<Card h="100%">
|
<Card h="100%">
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
@@ -355,7 +373,7 @@ export default function TrainBuilderDetailPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
</Grid.Col>
|
</Grid.Col>
|
||||||
) : null}
|
) : null}
|
||||||
<Grid.Col span={{ base: 12, md: composition.editable ? 7 : 12 }}>
|
<Grid.Col span={{ base: 12, md: composition.editable && canAssign ? 7 : 12 }}>
|
||||||
<Card h="100%">
|
<Card h="100%">
|
||||||
<Stack gap="sm">
|
<Stack gap="sm">
|
||||||
<Text fw={600}>Wagon order</Text>
|
<Text fw={600}>Wagon order</Text>
|
||||||
@@ -364,7 +382,7 @@ export default function TrainBuilderDetailPage() {
|
|||||||
</Text>
|
</Text>
|
||||||
<ConsistWagonList
|
<ConsistWagonList
|
||||||
wagons={composition.wagons}
|
wagons={composition.wagons}
|
||||||
editable={composition.editable}
|
editable={composition.editable && canAssign}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
onReorder={(wagonIds) =>
|
onReorder={(wagonIds) =>
|
||||||
void withToast(
|
void withToast(
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ import {
|
|||||||
trainStatusLabel,
|
trainStatusLabel,
|
||||||
} from "@/components/trainBuilder/trainStatus";
|
} from "@/components/trainBuilder/trainStatus";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
|
import { useAuth } from "@/auth/useAuth";
|
||||||
|
import { canFleetAction } from "@/lib/permissions";
|
||||||
import type {
|
import type {
|
||||||
BuiltTrainListFilters,
|
BuiltTrainListFilters,
|
||||||
BuiltTrainStatus,
|
BuiltTrainStatus,
|
||||||
@@ -54,6 +56,9 @@ export default function TrainBuilderListPage() {
|
|||||||
const [yardFilter, setYardFilter] = useState("ALL");
|
const [yardFilter, setYardFilter] = useState("ALL");
|
||||||
const [buildOpen, setBuildOpen] = useState(false);
|
const [buildOpen, setBuildOpen] = useState(false);
|
||||||
const [editTarget, setEditTarget] = useState<BuiltTrainSummary | null>(null);
|
const [editTarget, setEditTarget] = useState<BuiltTrainSummary | null>(null);
|
||||||
|
const { user } = useAuth();
|
||||||
|
const canCreate = canFleetAction(user, "trains", "create");
|
||||||
|
const canUpdate = canFleetAction(user, "trains", "update");
|
||||||
|
|
||||||
const resetPage = useCallback(() => {
|
const resetPage = useCallback(() => {
|
||||||
setPagination((prev) =>
|
setPagination((prev) =>
|
||||||
@@ -240,24 +245,25 @@ export default function TrainBuilderListPage() {
|
|||||||
id: "actions",
|
id: "actions",
|
||||||
header: "",
|
header: "",
|
||||||
meta: { headerClassName, cellClassName },
|
meta: { headerClassName, cellClassName },
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) =>
|
||||||
<ActionIcon
|
canUpdate ? (
|
||||||
variant="subtle"
|
<ActionIcon
|
||||||
color="gray"
|
variant="subtle"
|
||||||
aria-label={`Edit train ${row.original.code}`}
|
color="gray"
|
||||||
title="Edit name & train numbers"
|
aria-label={`Edit train ${row.original.code}`}
|
||||||
onClick={(e) => {
|
title="Edit name & train numbers"
|
||||||
// Row click navigates to the detail page — keep the edit local.
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
// Row click navigates to the detail page — keep the edit local.
|
||||||
setEditTarget(row.original);
|
e.stopPropagation();
|
||||||
}}
|
setEditTarget(row.original);
|
||||||
>
|
}}
|
||||||
<Pencil size={15} />
|
>
|
||||||
</ActionIcon>
|
<Pencil size={15} />
|
||||||
),
|
</ActionIcon>
|
||||||
|
) : null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, []);
|
}, [canUpdate]);
|
||||||
|
|
||||||
const tableStatus = trainsQuery.isLoading
|
const tableStatus = trainsQuery.isLoading
|
||||||
? "loading"
|
? "loading"
|
||||||
@@ -271,9 +277,11 @@ export default function TrainBuilderListPage() {
|
|||||||
title="Train Builder"
|
title="Train Builder"
|
||||||
subtitle="Assemble coded trains from locomotives and wagons in a yard, ready to schedule as a unit."
|
subtitle="Assemble coded trains from locomotives and wagons in a yard, ready to schedule as a unit."
|
||||||
action={
|
action={
|
||||||
<Button leftSection={<Hammer size={18} />} onClick={() => setBuildOpen(true)}>
|
canCreate ? (
|
||||||
Build train
|
<Button leftSection={<Hammer size={18} />} onClick={() => setBuildOpen(true)}>
|
||||||
</Button>
|
Build train
|
||||||
|
</Button>
|
||||||
|
) : undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user