mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 08:20:58 +00:00
train
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { CargoUnitOfMeasure } from '@edr/types';
|
||||
import { IsBoolean, IsEnum, IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator';
|
||||
import { IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, IsUUID, MaxLength, Min } from 'class-validator';
|
||||
|
||||
export class CreateCargoTypeDto {
|
||||
@ApiProperty({ description: 'Cargo type display name', maxLength: 255 })
|
||||
@@ -22,12 +22,15 @@ export class CreateCargoTypeDto {
|
||||
parentGroupId?: string;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: [String],
|
||||
format: 'uuid',
|
||||
description:
|
||||
'Wagon type used to carry this (bulk) cargo. Drives train scheduling wagon-type resolution; required for bulk commodities that are scheduled.',
|
||||
'Wagon types that can carry this (bulk) cargo. Drives train scheduling wagon-type resolution; at least one is required for bulk commodities that are scheduled.',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsUUID('4')
|
||||
wagonTypeId?: string | null;
|
||||
@IsArray()
|
||||
@IsUUID('4', { each: true })
|
||||
wagonTypeIds?: string[];
|
||||
|
||||
@ApiPropertyOptional({ default: false })
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsInt, IsNumber, IsOptional, IsString, IsUUID, Max, MaxLength, Min } from 'class-validator';
|
||||
import { IsArray, IsBoolean, IsInt, IsNumber, IsOptional, IsString, IsUUID, Max, MaxLength, Min } from 'class-validator';
|
||||
|
||||
export class CreateContainerTypeDto {
|
||||
@ApiProperty({ description: 'Customer-facing label, e.g. "20ft Dry Container"', maxLength: 100 })
|
||||
@@ -31,12 +31,15 @@ export class CreateContainerTypeDto {
|
||||
isOpenTop?: boolean;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
type: [String],
|
||||
format: 'uuid',
|
||||
description:
|
||||
'Wagon type used to carry this container. Drives train scheduling wagon-type resolution; required when this container type is scheduled.',
|
||||
'Wagon types that can carry this container. Drives train scheduling wagon-type resolution; at least one is required when this container type is scheduled.',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsUUID('4')
|
||||
wagonTypeId?: string | null;
|
||||
@IsArray()
|
||||
@IsUUID('4', { each: true })
|
||||
wagonTypeIds?: string[];
|
||||
|
||||
@ApiPropertyOptional({ default: true })
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { CargoUnitOfMeasure } from '@edr/types';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
Index,
|
||||
JoinColumn,
|
||||
JoinTable,
|
||||
ManyToMany,
|
||||
ManyToOne,
|
||||
OneToMany,
|
||||
} from 'typeorm';
|
||||
import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
|
||||
|
||||
@Entity({ schema: 'freight', name: 'cargo_types' })
|
||||
@Index(['isActive'])
|
||||
@Index(['displayOrder'])
|
||||
@Index(['parentGroupId'])
|
||||
@Index(['wagonTypeId'])
|
||||
@Index(['code'])
|
||||
export class CargoType extends BaseEntity {
|
||||
@Column({ name: 'code', type: 'varchar', length: 50, unique: true, default: '' })
|
||||
@@ -28,17 +36,19 @@ export class CargoType extends BaseEntity {
|
||||
unitOfMeasure?: CargoUnitOfMeasure | null;
|
||||
|
||||
/**
|
||||
* Wagon type that carries this (bulk) cargo. Replaces the former hardcoded
|
||||
* cargo-code → wagon-code map: train scheduling resolves the bulk wagon type
|
||||
* through this FK. Nullable — grouping rows and container/legacy cargo never
|
||||
* carry it; scheduling throws if a scheduled bulk cargo type leaves it unset.
|
||||
* Wagon types that can carry this (bulk) cargo. Train scheduling resolves the
|
||||
* bulk wagon type through this list, picking whichever type the schedule's
|
||||
* train (or yard) actually has. Grouping rows and container/legacy cargo
|
||||
* leave it empty; scheduling throws if a scheduled bulk cargo type has none.
|
||||
*/
|
||||
@Column({ name: 'wagon_type_id', type: 'uuid', nullable: true })
|
||||
wagonTypeId?: string | null;
|
||||
|
||||
@ManyToOne(() => WagonType, { nullable: true, onDelete: 'RESTRICT' })
|
||||
@JoinColumn({ name: 'wagon_type_id' })
|
||||
wagonType?: WagonType | null;
|
||||
@ManyToMany(() => WagonType)
|
||||
@JoinTable({
|
||||
name: 'cargo_type_wagon_types',
|
||||
schema: 'freight',
|
||||
joinColumn: { name: 'cargo_type_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'wagon_type_id', referencedColumnName: 'id' },
|
||||
})
|
||||
wagonTypes?: WagonType[];
|
||||
|
||||
@Column({ name: 'requires_director_approval', type: 'boolean', default: false })
|
||||
requiresDirectorApproval!: boolean;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
import { Column, Entity, Index, JoinTable, ManyToMany, OneToMany } from 'typeorm';
|
||||
import { WeightLimitRule } from './weight-limit-rule.entity';
|
||||
import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
|
||||
|
||||
@Entity({ schema: 'freight', name: 'container_types' })
|
||||
@Index(['code'])
|
||||
@Index(['isActive'])
|
||||
@Index(['wagonTypeId'])
|
||||
export class ContainerType extends BaseEntity {
|
||||
@Column({ name: 'code', type: 'varchar', length: 20, unique: true })
|
||||
code!: string;
|
||||
@@ -27,17 +26,19 @@ export class ContainerType extends BaseEntity {
|
||||
isOpenTop!: boolean;
|
||||
|
||||
/**
|
||||
* Wagon type that carries this container. Replaces the former hardcoded
|
||||
* container wagon-code default (NW5): train scheduling resolves the container
|
||||
* wagon type through this FK. Nullable; scheduling throws if a scheduled
|
||||
* container type leaves it unset.
|
||||
* Wagon types that can carry this container (e.g. a 20ft rides NX70 or NW5).
|
||||
* Train scheduling resolves the container wagon type through this list,
|
||||
* picking whichever type the schedule's train (or yard) actually has.
|
||||
* Scheduling throws if a scheduled container type has none configured.
|
||||
*/
|
||||
@Column({ name: 'wagon_type_id', type: 'uuid', nullable: true })
|
||||
wagonTypeId?: string | null;
|
||||
|
||||
@ManyToOne(() => WagonType, { nullable: true, onDelete: 'RESTRICT' })
|
||||
@JoinColumn({ name: 'wagon_type_id' })
|
||||
wagonType?: WagonType | null;
|
||||
@ManyToMany(() => WagonType)
|
||||
@JoinTable({
|
||||
name: 'container_type_wagon_types',
|
||||
schema: 'freight',
|
||||
joinColumn: { name: 'container_type_id', referencedColumnName: 'id' },
|
||||
inverseJoinColumn: { name: 'wagon_type_id', referencedColumnName: 'id' },
|
||||
})
|
||||
wagonTypes?: WagonType[];
|
||||
|
||||
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||
isActive!: boolean;
|
||||
|
||||
@@ -15,7 +15,7 @@ export class CargoTypesRepository implements ICargoTypesRepository {
|
||||
}
|
||||
|
||||
findById(id: string): Promise<CargoType | null> {
|
||||
return this.repo.findOne({ where: { id }, relations: { parent: true } });
|
||||
return this.repo.findOne({ where: { id }, relations: { parent: true, wagonTypes: true } });
|
||||
}
|
||||
|
||||
findByCode(code: string): Promise<CargoType | null> {
|
||||
@@ -35,6 +35,7 @@ export class CargoTypesRepository implements ICargoTypesRepository {
|
||||
const qb = this.repo
|
||||
.createQueryBuilder('cargoType')
|
||||
.leftJoinAndSelect('cargoType.parent', 'parent')
|
||||
.leftJoinAndSelect('cargoType.wagonTypes', 'wagonType')
|
||||
.orderBy(`cargoType.${query.sortBy ?? 'displayOrder'}`, query.sortOrder ?? 'ASC');
|
||||
|
||||
if (query.isActive !== undefined) {
|
||||
@@ -63,7 +64,18 @@ export class CargoTypesRepository implements ICargoTypesRepository {
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<CargoType>): Promise<CargoType | null> {
|
||||
await this.repo.update(id, data as never);
|
||||
// Relation lists can't ride a column UPDATE — sync them via entity save.
|
||||
const { wagonTypes, ...columns } = data;
|
||||
if (Object.keys(columns).length) {
|
||||
await this.repo.update(id, columns as never);
|
||||
}
|
||||
if (wagonTypes) {
|
||||
const entity = await this.repo.findOne({ where: { id } });
|
||||
if (entity) {
|
||||
entity.wagonTypes = wagonTypes;
|
||||
await this.repo.save(entity);
|
||||
}
|
||||
}
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export class ContainerTypesRepository implements IContainerTypesRepository {
|
||||
}
|
||||
|
||||
findById(id: string): Promise<ContainerType | null> {
|
||||
return this.repo.findOne({ where: { id } });
|
||||
return this.repo.findOne({ where: { id }, relations: { wagonTypes: true } });
|
||||
}
|
||||
|
||||
findByCode(code: string): Promise<ContainerType | null> {
|
||||
@@ -34,6 +34,7 @@ export class ContainerTypesRepository implements IContainerTypesRepository {
|
||||
findPaged(query: ListContainerTypesQueryDto): Promise<PaginatedResponse<ContainerType>> {
|
||||
const qb = this.repo
|
||||
.createQueryBuilder('containerType')
|
||||
.leftJoinAndSelect('containerType.wagonTypes', 'wagonType')
|
||||
.orderBy(`containerType.${query.sortBy ?? 'displayOrder'}`, query.sortOrder ?? 'ASC');
|
||||
|
||||
if (query.isActive !== undefined) {
|
||||
@@ -54,7 +55,18 @@ export class ContainerTypesRepository implements IContainerTypesRepository {
|
||||
}
|
||||
|
||||
async update(id: string, data: Partial<ContainerType>): Promise<ContainerType | null> {
|
||||
await this.repo.update(id, data as never);
|
||||
// Relation lists can't ride a column UPDATE — sync them via entity save.
|
||||
const { wagonTypes, ...columns } = data;
|
||||
if (Object.keys(columns).length) {
|
||||
await this.repo.update(id, columns as never);
|
||||
}
|
||||
if (wagonTypes) {
|
||||
const entity = await this.repo.findOne({ where: { id } });
|
||||
if (entity) {
|
||||
entity.wagonTypes = wagonTypes;
|
||||
await this.repo.save(entity);
|
||||
}
|
||||
}
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ListCargoTypesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
import { ReorderItemsDto } from '../dto/reorder-items.dto';
|
||||
import { UpdateCargoTypeDto } from '../dto/update-cargo-type.dto';
|
||||
import { CargoType } from '../entities/cargo-type.entity';
|
||||
import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
|
||||
import {
|
||||
CARGO_TYPES_REPOSITORY,
|
||||
ICargoTypesRepository,
|
||||
@@ -59,7 +60,8 @@ export class CargoTypesService {
|
||||
requiresDirectorApproval: dto.requiresDirectorApproval ?? false,
|
||||
isActive: dto.isActive ?? true,
|
||||
unitOfMeasure: dto.unitOfMeasure ?? null,
|
||||
wagonTypeId: dto.wagonTypeId ?? null,
|
||||
// Join rows are written by the save (RESTRICT FK rejects unknown ids).
|
||||
wagonTypes: (dto.wagonTypeIds ?? []).map((id) => ({ id }) as WagonType),
|
||||
displayOrder,
|
||||
});
|
||||
}
|
||||
@@ -72,7 +74,13 @@ export class CargoTypesService {
|
||||
const parent = await this.repository.findById(dto.parentGroupId);
|
||||
if (!parent) throw new NotFoundException(`Parent cargo type ${dto.parentGroupId} not found`);
|
||||
}
|
||||
const updated = await this.repository.update(id, dto);
|
||||
const { wagonTypeIds, insertAfterId: _insertAfterId, ...columns } = dto;
|
||||
const updated = await this.repository.update(id, {
|
||||
...columns,
|
||||
...(wagonTypeIds
|
||||
? { wagonTypes: wagonTypeIds.map((wagonTypeId) => ({ id: wagonTypeId }) as WagonType) }
|
||||
: {}),
|
||||
});
|
||||
if (!updated) throw new NotFoundException(`Cargo type ${id} not found`);
|
||||
return updated;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ListContainerTypesQueryDto } from '../dto/list-rule-engine-query.dto';
|
||||
import { ReorderItemsDto } from '../dto/reorder-items.dto';
|
||||
import { UpdateContainerTypeDto } from '../dto/update-container-type.dto';
|
||||
import { ContainerType } from '../entities/container-type.entity';
|
||||
import { WagonType } from '../../wagon-types/entities/wagon-type.entity';
|
||||
import {
|
||||
CONTAINER_TYPES_REPOSITORY,
|
||||
IContainerTypesRepository,
|
||||
@@ -51,7 +52,8 @@ export class ContainerTypesService {
|
||||
isReefer: dto.isReefer ?? false,
|
||||
isOpenTop: dto.isOpenTop ?? false,
|
||||
isActive: dto.isActive ?? true,
|
||||
wagonTypeId: dto.wagonTypeId ?? null,
|
||||
// Join rows are written by the save (RESTRICT FK rejects unknown ids).
|
||||
wagonTypes: (dto.wagonTypeIds ?? []).map((id) => ({ id }) as WagonType),
|
||||
displayOrder,
|
||||
});
|
||||
}
|
||||
@@ -59,7 +61,13 @@ export class ContainerTypesService {
|
||||
/** Update an existing container type. */
|
||||
async update(id: string, dto: UpdateContainerTypeDto): Promise<ContainerType> {
|
||||
await this.findById(id);
|
||||
const updated = await this.repository.update(id, dto);
|
||||
const { wagonTypeIds, insertAfterId: _insertAfterId, ...columns } = dto;
|
||||
const updated = await this.repository.update(id, {
|
||||
...columns,
|
||||
...(wagonTypeIds
|
||||
? { wagonTypes: wagonTypeIds.map((wagonTypeId) => ({ id: wagonTypeId }) as WagonType) }
|
||||
: {}),
|
||||
});
|
||||
if (!updated) throw new NotFoundException(`Container type ${id} not found`);
|
||||
return updated;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user