mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
feat(wagons): audited maintenance/availability toggle
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
import { WagonStatus } from '@edr/types';
|
||||
import { ArrayNotEmpty, IsArray, IsEnum, IsUUID } from 'class-validator';
|
||||
import {
|
||||
ArrayNotEmpty,
|
||||
IsArray,
|
||||
IsEnum,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
export class BulkSetWagonStatusDto {
|
||||
@IsArray()
|
||||
@@ -9,4 +17,10 @@ export class BulkSetWagonStatusDto {
|
||||
|
||||
@IsEnum(WagonStatus)
|
||||
status!: WagonStatus;
|
||||
|
||||
/** Reason shown in the wagon's status history (maintenance/availability flips). */
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(500)
|
||||
note?: string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
|
||||
import { Wagon } from './wagon.entity';
|
||||
|
||||
/**
|
||||
* One wagon status flip (Available ⇄ Maintenance, Detained, …) — the audit
|
||||
* trail behind the maintenance/availability buttons on the wagons desk.
|
||||
* Written in the same transaction as the status change.
|
||||
*/
|
||||
@Entity({ schema: 'freight', name: 'wagon_status_logs' })
|
||||
@Index(['wagonId', 'createdAt'])
|
||||
export class WagonStatusLog extends BaseEntity {
|
||||
@Column({ name: 'wagon_id', type: 'uuid' })
|
||||
wagonId!: string;
|
||||
|
||||
@ManyToOne(() => Wagon)
|
||||
@JoinColumn({ name: 'wagon_id' })
|
||||
wagon?: Wagon;
|
||||
|
||||
@Column({ name: 'from_status', type: 'varchar', length: 30 })
|
||||
fromStatus!: string;
|
||||
|
||||
@Column({ name: 'to_status', type: 'varchar', length: 30 })
|
||||
toStatus!: string;
|
||||
|
||||
@Column({ name: 'changed_by_user_id', type: 'uuid', nullable: true })
|
||||
changedByUserId?: string | null;
|
||||
|
||||
@Column({ name: 'note', type: 'text', nullable: true })
|
||||
note?: string | null;
|
||||
}
|
||||
@@ -119,9 +119,22 @@ export class WagonsController {
|
||||
}
|
||||
|
||||
@Post('bulk-status')
|
||||
@FleetManage(FREIGHT_PERMS.wagons.update)
|
||||
@ApiOperation({ summary: 'Set the status of multiple wagons' })
|
||||
bulkSetStatus(@Body() dto: BulkSetWagonStatusDto) {
|
||||
return this.wagonsService.bulkSetStatus(dto);
|
||||
// One-of: the dedicated maintenance⇄availability key, full wagon edit, or
|
||||
// the legacy coarse fleet:manage — operations/OCC hold statusToggle only.
|
||||
@BookingStaff([
|
||||
FREIGHT_PERMS.wagons.statusToggle,
|
||||
FREIGHT_PERMS.wagons.update,
|
||||
FREIGHT_PERMS.fleet.manage,
|
||||
])
|
||||
@ApiOperation({ summary: 'Set the status of multiple wagons (audited in wagon_status_logs)' })
|
||||
bulkSetStatus(@Body() dto: BulkSetWagonStatusDto, @CurrentUser() user: TCurrentUser) {
|
||||
return this.wagonsService.bulkSetStatus(dto, user?.id);
|
||||
}
|
||||
|
||||
@Get(':id/status-history')
|
||||
@FleetView(FREIGHT_PERMS.wagons.view)
|
||||
@ApiOperation({ summary: 'Status-flip history of a wagon (maintenance ⇄ availability audit)' })
|
||||
statusHistory(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.wagonsService.statusHistory(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { Wagon } from './entities/wagon.entity';
|
||||
import { WagonMovement } from './entities/wagon-movement.entity';
|
||||
import { WagonStatusLog } from './entities/wagon-status-log.entity';
|
||||
import { WagonTransferRequest } from './entities/wagon-transfer-request.entity';
|
||||
import { Train } from '../trains/entities/train.entity';
|
||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||
@@ -16,6 +17,7 @@ import { WagonTransferRequestsService } from './wagon-transfer-requests.service'
|
||||
TypeOrmModule.forFeature([
|
||||
Wagon,
|
||||
WagonMovement,
|
||||
WagonStatusLog,
|
||||
WagonTransferRequest,
|
||||
Train,
|
||||
Yard,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { AssignWagonToTrainDto } from './dto/assign-wagon-to-train.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 { WagonStatusLog } from './entities/wagon-status-log.entity';
|
||||
import { WagonMovement } from './entities/wagon-movement.entity';
|
||||
import { Train } from '../trains/entities/train.entity';
|
||||
import { Yard } from '../rule-engine/entities/yard.entity';
|
||||
@@ -440,7 +441,10 @@ export class WagonsService {
|
||||
* 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 }> {
|
||||
async bulkSetStatus(
|
||||
dto: BulkSetWagonStatusDto,
|
||||
changedByUserId?: string,
|
||||
): Promise<{ updated: number }> {
|
||||
const { wagonIds, status } = dto;
|
||||
if (!wagonIds.length) return { updated: 0 };
|
||||
|
||||
@@ -466,10 +470,24 @@ export class WagonsService {
|
||||
);
|
||||
}
|
||||
|
||||
// Audit trail rides the same transaction — a status flip without its
|
||||
// history row can't happen. No-change wagons write no log row.
|
||||
const logs = wagons
|
||||
.filter((w) => w.status !== status)
|
||||
.map((w) =>
|
||||
queryRunner.manager.create(WagonStatusLog, {
|
||||
wagonId: w.id,
|
||||
fromStatus: w.status,
|
||||
toStatus: status,
|
||||
changedByUserId: changedByUserId ?? null,
|
||||
note: dto.note ?? null,
|
||||
}),
|
||||
);
|
||||
for (const wagon of wagons) {
|
||||
wagon.status = status;
|
||||
}
|
||||
await queryRunner.manager.save(Wagon, wagons);
|
||||
if (logs.length) await queryRunner.manager.save(WagonStatusLog, logs);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
return { updated: wagons.length };
|
||||
@@ -481,4 +499,13 @@ export class WagonsService {
|
||||
}
|
||||
}
|
||||
|
||||
/** Status-flip history of one wagon, newest first (maintenance/availability audit). */
|
||||
async statusHistory(wagonId: string): Promise<WagonStatusLog[]> {
|
||||
return this.dataSource.getRepository(WagonStatusLog).find({
|
||||
where: { wagonId },
|
||||
order: { createdAt: 'DESC' },
|
||||
take: 100,
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user