wagon work space, container validation

This commit is contained in:
Marshal
2026-07-10 23:29:28 +00:00
parent 5d8658b5d6
commit dd87c2a722
12 changed files with 878 additions and 47 deletions

View File

@@ -0,0 +1,12 @@
import { WagonStatus } from '@edr/types';
import { ArrayNotEmpty, IsArray, IsEnum, IsUUID } from 'class-validator';
export class BulkSetWagonStatusDto {
@IsArray()
@ArrayNotEmpty()
@IsUUID('4', { each: true })
wagonIds!: string[];
@IsEnum(WagonStatus)
status!: WagonStatus;
}

View File

@@ -0,0 +1,11 @@
import { ArrayNotEmpty, IsArray, IsUUID } from 'class-validator';
export class BulkTransferWagonsDto {
@IsArray()
@ArrayNotEmpty()
@IsUUID('4', { each: true })
wagonIds!: string[];
@IsUUID()
toYardId!: string;
}

View File

@@ -10,12 +10,16 @@ import {
Query,
} from '@nestjs/common';
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 } from '../../common/booking-guards';
import { CreateWagonDto } from './dto/create-wagon.dto';
import { ListWagonsQueryDto } from './dto/list-wagons-query.dto';
import { UpdateWagonDto } from './dto/update-wagon.dto';
import { AssignWagonToTrainDto } from './dto/assign-wagon-to-train.dto';
import { ReorderWagonsDto } from './dto/reorder-wagons.dto';
import { BulkTransferWagonsDto } from './dto/bulk-transfer-wagons.dto';
import { BulkSetWagonStatusDto } from './dto/bulk-set-wagon-status.dto';
import { WagonsService } from './wagons.service';
@ApiTags('wagons')
@@ -78,6 +82,20 @@ export class WagonsController {
unassignFromTrain(@Param('id', ParseUUIDPipe) id: string) {
return this.wagonsService.unassignFromTrain(id);
}
@Post('bulk-transfer')
@FleetManage()
@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()
@ApiOperation({ summary: 'Set the status of multiple wagons' })
bulkSetStatus(@Body() dto: BulkSetWagonStatusDto) {
return this.wagonsService.bulkSetStatus(dto);
}
}
// Separate controller for trainspecific reorder (registered in module)

View File

@@ -1,15 +1,18 @@
import { WagonMovementKind, WagonStatus } from '@edr/types';
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, FindOptionsOrder, FindOptionsWhere, ILike } from 'typeorm';
import { Repository, DataSource, FindOptionsOrder, FindOptionsWhere, ILike, In } from 'typeorm';
import { CreateWagonDto } from './dto/create-wagon.dto';
import { ListWagonsQueryDto } from './dto/list-wagons-query.dto';
import { UpdateWagonDto } from './dto/update-wagon.dto';
import { AssignWagonToTrainDto } from './dto/assign-wagon-to-train.dto';
import { ReorderWagonsDto } from './dto/reorder-wagons.dto';
import { BulkTransferWagonsDto } from './dto/bulk-transfer-wagons.dto';
import { BulkSetWagonStatusDto } from './dto/bulk-set-wagon-status.dto';
import { Wagon } from './entities/wagon.entity';
import { WagonMovement } from './entities/wagon-movement.entity';
import { Train } from '../trains/entities/train.entity';
import { Yard } from '../rule-engine/entities/yard.entity';
@Injectable()
export class WagonsService {
@@ -168,6 +171,101 @@ export class WagonsService {
return this.wagonRepo.save(wagon);
}
/**
* Relocate many wagons to one destination yard in a single transaction. Each
* wagon whose yard actually changes gets a `wagon_movements` ledger row (kind
* `Manual`) so the yard history stays auditable — mirrors the single-wagon
* `update` path. Wagons already in the destination yard are skipped.
*/
async bulkTransfer(
dto: BulkTransferWagonsDto,
userId?: string | null,
): Promise<{ moved: number }> {
const { wagonIds, toYardId } = dto;
if (!wagonIds.length) return { moved: 0 };
const yard = await this.dataSource
.getRepository(Yard)
.findOne({ where: { id: toYardId } });
if (!yard) throw new NotFoundException('Destination yard not found');
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const wagons = await queryRunner.manager.find(Wagon, {
where: { id: In(wagonIds) },
});
if (wagons.length !== wagonIds.length) {
throw new NotFoundException('One or more wagons not found');
}
let moved = 0;
for (const wagon of wagons) {
const previousYardId = wagon.currentYardId ?? null;
if (previousYardId === toYardId) continue;
wagon.currentYardId = toYardId;
// Drop the eager relation so the scalar FK wins on save (see `update`).
wagon.currentYard = null;
await queryRunner.manager.save(Wagon, wagon);
await queryRunner.manager.save(
queryRunner.manager.create(WagonMovement, {
wagonId: wagon.id,
fromYardId: previousYardId,
toYardId,
kind: WagonMovementKind.Manual,
movedByUserId: userId ?? null,
occurredAt: new Date(),
}),
);
moved++;
}
await queryRunner.commitTransaction();
return { moved };
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
/**
* Set the same status on many wagons in one transaction (e.g. flip a batch
* from Available to Assigned in the yard workspace). Only the `status` column
* is touched — train assignment is managed through the assign/unassign flow.
*/
async bulkSetStatus(dto: BulkSetWagonStatusDto): Promise<{ updated: number }> {
const { wagonIds, status } = dto;
if (!wagonIds.length) return { updated: 0 };
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
const wagons = await queryRunner.manager.find(Wagon, {
where: { id: In(wagonIds) },
});
if (wagons.length !== wagonIds.length) {
throw new NotFoundException('One or more wagons not found');
}
for (const wagon of wagons) {
wagon.status = status;
}
await queryRunner.manager.save(Wagon, wagons);
await queryRunner.commitTransaction();
return { updated: wagons.length };
} catch (err) {
await queryRunner.rollbackTransaction();
throw err;
} finally {
await queryRunner.release();
}
}
async reorderWagons(_trainId: string, dto: ReorderWagonsDto): Promise<void> {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();