update locaomotive and fix wagon transfer issue

This commit is contained in:
Marshal
2026-07-27 20:34:14 +00:00
parent deadc59277
commit 496c66017f
23 changed files with 811 additions and 230 deletions

View File

@@ -1,7 +1,17 @@
import { WagonStatus } from '@edr/types';
import { ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsOptional, IsString, IsUUID, Max, Min } from 'class-validator';
import { Transform, Type } from 'class-transformer';
import {
IsBoolean,
IsDateString,
IsEnum,
IsInt,
IsOptional,
IsString,
IsUUID,
Max,
Min,
} from 'class-validator';
export class ListWagonsQueryDto {
@ApiPropertyOptional({ description: 'Search wagon number (partial match)' })
@@ -29,6 +39,15 @@ export class ListWagonsQueryDto {
@IsUUID()
trainId?: string;
@ApiPropertyOptional({
description:
'Only loose wagons (not coupled to a built train) — what a picker can actually take.',
})
@IsOptional()
@Transform(({ value }: { value: unknown }) => value === true || value === 'true')
@IsBoolean()
unassigned?: boolean;
@ApiPropertyOptional({
description: 'Filter by run number — matches export OR import run (e.g. 8001).',
})
@@ -60,4 +79,23 @@ export class ListWagonsQueryDto {
@Min(1)
@Max(500)
limit?: number;
/** Page size for `GET /wagons/paged`; the legacy `limit` still drives `GET /wagons`. */
@ApiPropertyOptional({ default: 20, minimum: 1, maximum: 100 })
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(100)
pageSize?: number;
@ApiPropertyOptional({ description: 'Registered on or after this day (YYYY-MM-DD)' })
@IsOptional()
@IsDateString()
createdFrom?: string;
@ApiPropertyOptional({ description: 'Registered on or before this day (YYYY-MM-DD)' })
@IsOptional()
@IsDateString()
createdTo?: string;
}

View File

@@ -114,6 +114,8 @@ export class WagonTransferRequestsService {
currentYardId: yardId,
wagonTypeId,
status: WagonStatus.Available,
// Coupled to a built train = not movable; bulkTransfer rejects it too.
trainId: IsNull(),
},
});
}
@@ -399,6 +401,7 @@ export class WagonTransferRequestsService {
currentYardId: request.fromYardId,
wagonTypeId: request.wagonTypeId,
status: WagonStatus.Available,
trainId: IsNull(),
},
order: { wagonNumber: 'ASC' },
take: remaining,

View File

@@ -44,6 +44,14 @@ export class WagonsController {
return this.wagonsService.findAll(query);
}
// Must be declared before @Get(':id') so the path isn't captured as an id.
@Get('paged')
@StaffReference()
@ApiOperation({ summary: 'List wagons, paginated ({items, meta})' })
findAllPaged(@Query() query: ListWagonsQueryDto) {
return this.wagonsService.findAllPaged(query);
}
@Get(':id')
@StaffReference()
@ApiOperation({ summary: 'Get a wagon by ID' })

View File

@@ -1,4 +1,4 @@
import { Freight, WagonMovementKind, WagonStatus } from '@edr/types';
import { Freight, PaginatedResponse, WagonMovementKind, WagonStatus } from '@edr/types';
import {
BadRequestException,
Injectable,
@@ -6,7 +6,8 @@ import {
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, DataSource, In } from 'typeorm';
import { Repository, DataSource, In, SelectQueryBuilder } from 'typeorm';
import { paginateQuery } from '../../common/utils/pagination.util';
import { CreateWagonDto } from './dto/create-wagon.dto';
import { ListWagonsQueryDto } from './dto/list-wagons-query.dto';
import { UpdateWagonDto } from './dto/update-wagon.dto';
@@ -42,7 +43,8 @@ export class WagonsService {
return this.wagonRepo.save(wagon);
}
async findAll(query: ListWagonsQueryDto = {}): Promise<Wagon[]> {
/** Shared filter/sort builder behind `findAll` (array) and `findAllPaged` (envelope). */
private buildListQuery(query: ListWagonsQueryDto): SelectQueryBuilder<Wagon> {
const search = query.search?.trim();
const trainId = query.trainId?.trim();
const wagonTypeId = query.wagonTypeId?.trim();
@@ -61,6 +63,9 @@ export class WagonsService {
if (query.currentYardId)
qb.andWhere('w.currentYardId = :currentYardId', { currentYardId: query.currentYardId });
if (trainId) qb.andWhere('w.trainId = :trainId', { trainId });
// Pickers (train-builder, transfer fulfilment) can only take a wagon that is
// not already coupled to a built train — never offer one the API will reject.
if (query.unassigned) qb.andWhere('w.trainId IS NULL');
if (wagonTypeId) qb.andWhere('w.wagonTypeId = :wagonTypeId', { wagonTypeId });
// Filter by run: the odd export run identifies the pair, so match either
@@ -72,6 +77,18 @@ export class WagonsService {
);
}
// Registration-day range, both ends inclusive (the UI picks whole days).
if (query.createdFrom) {
qb.andWhere('w.createdAt >= CAST(:createdFrom AS date)', {
createdFrom: query.createdFrom,
});
}
if (query.createdTo) {
qb.andWhere("w.createdAt < CAST(:createdTo AS date) + INTERVAL '1 day'", {
createdTo: query.createdTo,
});
}
// Search matches the wagon number or either run number.
if (search) {
qb.andWhere(
@@ -95,6 +112,12 @@ export class WagonsService {
const sortOrder = query.sortOrder?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC';
qb.orderBy(`w.${sortBy}`, sortOrder);
return qb;
}
async findAll(query: ListWagonsQueryDto = {}): Promise<Wagon[]> {
const qb = this.buildListQuery(query);
if (query.page && query.limit) {
qb.skip((Number(query.page) - 1) * Number(query.limit));
}
@@ -103,6 +126,11 @@ export class WagonsService {
return qb.getMany();
}
/** Same filters as `findAll`, on the shared `{items, meta}` list envelope. */
findAllPaged(query: ListWagonsQueryDto = {}): Promise<PaginatedResponse<Wagon>> {
return paginateQuery(this.buildListQuery(query), query);
}
async findById(id: string): Promise<Wagon> {
const wagon = await this.wagonRepo.findOne({
where: { id },