Merge branch 'dev' of github.com:Tria-plc/edr-platform into freight_feature/usermanagement

This commit is contained in:
Marshal
2026-07-03 18:48:18 +00:00
106 changed files with 4924 additions and 876 deletions

View File

@@ -0,0 +1,15 @@
import { LoadingStatus } from '@edr/types';
import { ApiProperty } from '@nestjs/swagger';
import { ArrayMinSize, IsArray, IsEnum, IsUUID } from 'class-validator';
export class UpdateImportLoadingStatusDto {
@ApiProperty({ type: [String] })
@IsArray()
@ArrayMinSize(1)
@IsUUID('4', { each: true })
bookingIds!: string[];
@ApiProperty({ enum: LoadingStatus })
@IsEnum(LoadingStatus)
loadingStatus!: LoadingStatus;
}

View File

@@ -28,6 +28,7 @@ import { GetEligibleBulkBookingsDto } from "./dto/get-eligible-bulk-bookings.dto
import { GetEligibleContainerBookingsDto } from "./dto/get-eligible-container-bookings.dto";
import { PinWagonsDto } from "./dto/pin-wagons.dto";
import { UpdateContainerItemDto } from "./dto/update-container-item.dto";
import { UpdateImportLoadingStatusDto } from "./dto/update-import-loading-status.dto";
import { PreviewBulkTrainScheduleDto } from "./dto/preview-bulk-train-schedule.dto";
import { PreviewContainerTrainScheduleDto } from "./dto/preview-container-train-schedule.dto";
import { PreviewTrainScheduleDto } from "./dto/preview-train-schedule.dto";
@@ -336,6 +337,28 @@ export class TrainSchedulingController {
return this.trainSchedulingService.getCompositionRemovals(id);
}
@Get("schedules/:id/import-loading-bookings")
@TrainSchedulingView()
@ApiOperation({
summary: "List import bookings eligible for loading confirmation on this schedule",
})
getImportLoadingBookings(@Param("id", ParseUUIDPipe) id: string) {
return this.trainSchedulingService.getImportLoadingBookings(id);
}
@Patch("schedules/:id/import-loading-status")
@TrainSchedulingManage()
@ApiOperation({
summary:
"Mark import bookings loaded/unloaded on this schedule (tracking only, does not affect dispatch)",
})
updateImportLoadingStatus(
@Param("id", ParseUUIDPipe) id: string,
@Body() dto: UpdateImportLoadingStatusDto,
) {
return this.trainSchedulingService.updateImportLoadingStatus(id, dto);
}
@Post("schedules/:id/pin-wagons")
@TrainSchedulingManage()
@ApiOperation({ summary: "Pin physical wagons to train set slots" })

View File

@@ -1,5 +1,6 @@
import {
AllocationLoadType,
LoadingStatus,
SchedulingStatus,
TrainCheckpointKind,
TrainScheduleStatus as TrainScheduleStatusEnum,
@@ -46,6 +47,7 @@ import { GetEligibleBulkBookingsDto } from './dto/get-eligible-bulk-bookings.dto
import { GetEligibleContainerBookingsDto } from './dto/get-eligible-container-bookings.dto';
import { PinWagonsDto } from './dto/pin-wagons.dto';
import { UpdateContainerItemDto } from './dto/update-container-item.dto';
import { UpdateImportLoadingStatusDto } from './dto/update-import-loading-status.dto';
import { PreviewBulkTrainScheduleDto } from './dto/preview-bulk-train-schedule.dto';
import { PreviewContainerTrainScheduleDto } from './dto/preview-container-train-schedule.dto';
import { PreviewTrainScheduleDto } from './dto/preview-train-schedule.dto';
@@ -866,6 +868,87 @@ export class TrainSchedulingService {
);
}
async getImportLoadingBookings(scheduleId: string) {
const schedule = await this.trainSchedulesRepository.findById(scheduleId);
if (!schedule) {
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
}
const [scheduleBookings, allocations] = await Promise.all([
this.trainScheduleBookingsRepository.findByScheduleId(scheduleId),
this.wagonBookingAllocationsRepository.findByScheduleId(scheduleId),
]);
if (!scheduleBookings.length) {
return { count: 0, items: [] };
}
const allocatedBookingIds = new Set(allocations.map((a) => a.bookingId));
const statusByBookingId = new Map(
scheduleBookings.map((sb) => [sb.bookingId, sb.loadingStatus]),
);
const candidateIds = scheduleBookings
.map((sb) => sb.bookingId)
.filter((id) => allocatedBookingIds.has(id));
if (!candidateIds.length) {
return { count: 0, items: [] };
}
const bookings = await this.bookingsRepository.findByIdsForScheduling(candidateIds);
const items = bookings
.filter((b) => b.tradeDirection === 'IMPORT' && b.paymentStatus === 'PAID')
.map((b) => ({
id: b.id,
reference: b.reference ?? null,
customer: b.company?.name ?? null,
weightTons: b.cargoTotalWeightVgm,
loadingStatus: statusByBookingId.get(b.id) ?? LoadingStatus.Unloaded,
}));
return { count: items.length, items };
}
async updateImportLoadingStatus(scheduleId: string, dto: UpdateImportLoadingStatusDto) {
const schedule = await this.trainSchedulesRepository.findById(scheduleId);
if (!schedule) {
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
}
const [scheduleBookings, allocations, bookings] = await Promise.all([
this.trainScheduleBookingsRepository.findByScheduleId(scheduleId),
this.wagonBookingAllocationsRepository.findByScheduleId(scheduleId),
this.bookingsRepository.findByIdsForScheduling(dto.bookingIds),
]);
const scheduledIds = new Set(scheduleBookings.map((sb) => sb.bookingId));
const allocatedIds = new Set(allocations.map((a) => a.bookingId));
const bookingById = new Map(bookings.map((b) => [b.id, b]));
const invalid: string[] = [];
for (const id of dto.bookingIds) {
const booking = bookingById.get(id);
if (
!scheduledIds.has(id) ||
!allocatedIds.has(id) ||
!booking ||
booking.tradeDirection !== 'IMPORT' ||
booking.paymentStatus !== 'PAID'
) {
invalid.push(id);
}
}
if (invalid.length) {
throw new BadRequestException(
`Not eligible for import loading confirmation on this schedule: ${invalid.join(', ')}`,
);
}
await this.trainScheduleBookingsRepository.updateLoadingStatusMany(
scheduleId,
dto.bookingIds,
dto.loadingStatus,
);
return this.getImportLoadingBookings(scheduleId);
}
async pinWagons(scheduleId: string, dto: PinWagonsDto) {
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
if (!schedule) {