mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
add detail batch and allocation monitoring page
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
|
||||
@Entity({ schema: 'freight', name: 'train_composition_removal_logs' })
|
||||
@Index(['scheduleId'])
|
||||
export class TrainCompositionRemovalLog extends BaseEntity {
|
||||
@Column({ name: 'schedule_id', type: 'uuid' }) scheduleId!: string;
|
||||
|
||||
@Column({ name: 'booking_id', type: 'uuid' }) bookingId!: string;
|
||||
|
||||
@Column({ name: 'booking_reference', type: 'varchar', length: 64, nullable: true })
|
||||
bookingReference?: string | null;
|
||||
|
||||
@Column({ name: 'removed_by_user_id', type: 'uuid', nullable: true })
|
||||
removedByUserId?: string | null;
|
||||
|
||||
@Column({ name: 'removed_at', type: 'timestamptz', default: () => 'NOW()' })
|
||||
removedAt!: Date;
|
||||
|
||||
@Column({ name: 'notes', type: 'text', nullable: true })
|
||||
notes?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DataSource } from 'typeorm';
|
||||
import { BaseRepository } from '@edr/api-common';
|
||||
import { TrainCompositionRemovalLog } from './entities/train-composition-removal-log.entity';
|
||||
|
||||
@Injectable()
|
||||
export class TrainCompositionRemovalLogRepository extends BaseRepository<TrainCompositionRemovalLog> {
|
||||
constructor(dataSource: DataSource) {
|
||||
super(dataSource.getRepository(TrainCompositionRemovalLog));
|
||||
}
|
||||
|
||||
async findByScheduleId(scheduleId: string): Promise<TrainCompositionRemovalLog[]> {
|
||||
return this.findAll({
|
||||
where: { scheduleId },
|
||||
order: { removedAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { TrainScheduleBooking } from './entities/train-schedule-booking.entity';
|
||||
import { TrainSchedule } from './entities/train-schedule.entity';
|
||||
import { TrainCompositionRemovalLog } from './entities/train-composition-removal-log.entity';
|
||||
import { WagonAllocationBulkLoad } from './entities/wagon-allocation-bulk-load.entity';
|
||||
import { WagonAllocationContainerItem } from './entities/wagon-allocation-container-item.entity';
|
||||
import { WagonBookingAllocation } from './entities/wagon-booking-allocation.entity';
|
||||
import { TrainScheduleBookingsRepository } from './train-schedule-bookings.repository';
|
||||
import { TrainSchedulesRepository } from './train-schedules.repository';
|
||||
import { TrainCompositionRemovalLogRepository } from './train-composition-removal-log.repository';
|
||||
import { WagonAllocationBulkLoadsRepository } from './wagon-allocation-bulk-loads.repository';
|
||||
import { WagonAllocationContainerItemsRepository } from './wagon-allocation-container-items.repository';
|
||||
import { WagonBookingAllocationsRepository } from './wagon-booking-allocations.repository';
|
||||
@@ -17,6 +19,7 @@ import { WagonBookingAllocationsRepository } from './wagon-booking-allocations.r
|
||||
TypeOrmModule.forFeature([
|
||||
TrainSchedule,
|
||||
TrainScheduleBooking,
|
||||
TrainCompositionRemovalLog,
|
||||
WagonBookingAllocation,
|
||||
WagonAllocationContainerItem,
|
||||
WagonAllocationBulkLoad,
|
||||
@@ -25,6 +28,7 @@ import { WagonBookingAllocationsRepository } from './wagon-booking-allocations.r
|
||||
providers: [
|
||||
TrainSchedulesRepository,
|
||||
TrainScheduleBookingsRepository,
|
||||
TrainCompositionRemovalLogRepository,
|
||||
WagonBookingAllocationsRepository,
|
||||
WagonAllocationContainerItemsRepository,
|
||||
WagonAllocationBulkLoadsRepository,
|
||||
@@ -32,6 +36,7 @@ import { WagonBookingAllocationsRepository } from './wagon-booking-allocations.r
|
||||
exports: [
|
||||
TrainSchedulesRepository,
|
||||
TrainScheduleBookingsRepository,
|
||||
TrainCompositionRemovalLogRepository,
|
||||
WagonBookingAllocationsRepository,
|
||||
WagonAllocationContainerItemsRepository,
|
||||
WagonAllocationBulkLoadsRepository,
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import { IsOptional, IsString } from 'class-validator';
|
||||
|
||||
export class UpdateContainerItemDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
containerNumber?: string | null;
|
||||
}
|
||||
@@ -9,7 +9,10 @@ import {
|
||||
Post,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { CurrentUser } from '@edr/api-common';
|
||||
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import type { AuthUserPayload } from '../../common/resolve-auth-user-id';
|
||||
import { resolveAuthUserId } from '../../common/resolve-auth-user-id';
|
||||
|
||||
import { TrainSchedulingManage, TrainSchedulingView } from '../../common/booking-guards';
|
||||
import { AssignBookingsDto } from './dto/assign-bookings.dto';
|
||||
@@ -18,6 +21,7 @@ import { GetEligibleBookingsDto } from './dto/get-eligible-bookings.dto';
|
||||
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 { 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';
|
||||
@@ -176,8 +180,44 @@ export class TrainSchedulingController {
|
||||
unassignBooking(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||||
@CurrentUser() user: AuthUserPayload,
|
||||
) {
|
||||
return this.trainSchedulingService.unassignBooking(id, bookingId);
|
||||
return this.trainSchedulingService.unassignBooking(id, bookingId, resolveAuthUserId(user));
|
||||
}
|
||||
|
||||
@Delete('schedules/:id/wagons/:trainSetWagonId')
|
||||
@TrainSchedulingManage()
|
||||
@ApiOperation({ summary: 'Remove an empty wagon slot from a train' })
|
||||
removeWagonSlot(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Param('trainSetWagonId', ParseUUIDPipe) trainSetWagonId: string,
|
||||
) {
|
||||
return this.trainSchedulingService.removeTrainSetWagonSlot(id, trainSetWagonId);
|
||||
}
|
||||
|
||||
@Patch('schedules/:id/container-items/:itemId')
|
||||
@TrainSchedulingManage()
|
||||
@ApiOperation({ summary: 'Update a container number on a wagon slot' })
|
||||
updateContainerItem(
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Param('itemId', ParseUUIDPipe) itemId: string,
|
||||
@Body() dto: UpdateContainerItemDto,
|
||||
) {
|
||||
return this.trainSchedulingService.updateContainerItem(id, itemId, dto);
|
||||
}
|
||||
|
||||
@Get('schedules/:id/unassigned-bookings')
|
||||
@TrainSchedulingView()
|
||||
@ApiOperation({ summary: 'Get unassigned bookings for a schedule' })
|
||||
getUnassignedBookings(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.trainSchedulingService.getUnassignedBookings(id);
|
||||
}
|
||||
|
||||
@Get('schedules/:id/composition-removals')
|
||||
@TrainSchedulingView()
|
||||
@ApiOperation({ summary: 'Get removal log for a schedule' })
|
||||
getCompositionRemovals(@Param('id', ParseUUIDPipe) id: string) {
|
||||
return this.trainSchedulingService.getCompositionRemovals(id);
|
||||
}
|
||||
|
||||
@Post('schedules/:id/pin-wagons')
|
||||
|
||||
@@ -144,6 +144,7 @@ describe('TrainSchedulingService', () => {
|
||||
wagonAllocationContainerItemsRepository as never,
|
||||
wagonAllocationBulkLoadsRepository as never,
|
||||
trainCheckpointEventsRepository as never,
|
||||
{} as never, // trainCompositionRemovalLogRepository
|
||||
);
|
||||
|
||||
const defaultFleetWagons = [
|
||||
|
||||
@@ -26,9 +26,11 @@ import { TrainSetWagon } from '../train-sets/entities/train-set-wagon.entity';
|
||||
import { TrainSet } from '../train-sets/entities/train-set.entity';
|
||||
import { TrainScheduleBooking } from '../train-schedules/entities/train-schedule-booking.entity';
|
||||
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
|
||||
import { WagonAllocationContainerItem } from '../train-schedules/entities/wagon-allocation-container-item.entity';
|
||||
import { WagonBookingAllocation } from '../train-schedules/entities/wagon-booking-allocation.entity';
|
||||
import { TrainScheduleBookingsRepository } from '../train-schedules/train-schedule-bookings.repository';
|
||||
import { TrainSchedulesRepository } from '../train-schedules/train-schedules.repository';
|
||||
import { TrainCompositionRemovalLogRepository } from '../train-schedules/train-composition-removal-log.repository';
|
||||
import { WagonAllocationBulkLoadsRepository } from '../train-schedules/wagon-allocation-bulk-loads.repository';
|
||||
import { WagonAllocationContainerItemsRepository } from '../train-schedules/wagon-allocation-container-items.repository';
|
||||
import { WagonBookingAllocationsRepository } from '../train-schedules/wagon-booking-allocations.repository';
|
||||
@@ -41,6 +43,7 @@ import { GetEligibleBookingsDto } from './dto/get-eligible-bookings.dto';
|
||||
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 { 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';
|
||||
@@ -143,6 +146,7 @@ export class TrainSchedulingService {
|
||||
private readonly wagonAllocationContainerItemsRepository: WagonAllocationContainerItemsRepository,
|
||||
private readonly wagonAllocationBulkLoadsRepository: WagonAllocationBulkLoadsRepository,
|
||||
private readonly trainCheckpointEventsRepository: TrainCheckpointEventsRepository,
|
||||
private readonly trainCompositionRemovalLogRepository: TrainCompositionRemovalLogRepository,
|
||||
private readonly configService?: ConfigService,
|
||||
) {}
|
||||
|
||||
@@ -467,7 +471,7 @@ export class TrainSchedulingService {
|
||||
return { ...detail, warnings, deferredBookings };
|
||||
}
|
||||
|
||||
async unassignBooking(scheduleId: string, bookingId: string) {
|
||||
async unassignBooking(scheduleId: string, bookingId: string, userId?: string) {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
@@ -481,6 +485,9 @@ export class TrainSchedulingService {
|
||||
throw new NotFoundException(`Booking ${bookingId} is not assigned to this schedule`);
|
||||
}
|
||||
|
||||
const booking = await this.bookingsRepository.findById(bookingId);
|
||||
const bookingReference = booking?.reference ?? null;
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
const allocationIds = (schedule.trainSet?.wagons ?? [])
|
||||
.flatMap((w) => w.allocations ?? [])
|
||||
@@ -529,6 +536,18 @@ export class TrainSchedulingService {
|
||||
}
|
||||
});
|
||||
|
||||
await this.trainCompositionRemovalLogRepository.create({
|
||||
scheduleId,
|
||||
bookingId,
|
||||
bookingReference,
|
||||
removedByUserId: userId ?? null,
|
||||
removedAt: new Date(),
|
||||
});
|
||||
|
||||
console.log(
|
||||
`[NOTIFY] Booking ${bookingReference} removed from schedule ${scheduleId} by user ${userId ?? 'unknown'} — customer should be notified to reschedule or cancel.`,
|
||||
);
|
||||
|
||||
return this.getTrainScheduleById(scheduleId);
|
||||
}
|
||||
|
||||
@@ -2236,6 +2255,103 @@ export class TrainSchedulingService {
|
||||
return result;
|
||||
}
|
||||
|
||||
async removeTrainSetWagonSlot(scheduleId: string, trainSetWagonId: string): Promise<any> {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
}
|
||||
if (!['DRAFT', 'SCHEDULED'].includes(schedule.status)) {
|
||||
throw new BadRequestException('Cannot remove wagon slots from a finalized or dispatched schedule');
|
||||
}
|
||||
|
||||
const wagon = (schedule.trainSet?.wagons ?? []).find((w) => w.id === trainSetWagonId);
|
||||
if (!wagon) {
|
||||
throw new NotFoundException(`Train set wagon ${trainSetWagonId} not found in this schedule`);
|
||||
}
|
||||
|
||||
if ((wagon.allocations ?? []).length > 0) {
|
||||
throw new BadRequestException(
|
||||
'Cannot remove a wagon slot that has active allocations; remove the booking first',
|
||||
);
|
||||
}
|
||||
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await manager.getRepository(TrainSetWagon).delete(trainSetWagonId);
|
||||
await manager.getRepository(TrainSet).update(schedule.trainSetId, {
|
||||
wagonCount: Math.max(0, (schedule.trainSet?.wagonCount ?? 0) - 1),
|
||||
totalLengthMeters: Math.max(0, (schedule.trainSet?.totalLengthMeters ?? 0) - (wagon.lengthMeters ?? 0)),
|
||||
});
|
||||
});
|
||||
|
||||
return this.getTrainScheduleById(scheduleId);
|
||||
}
|
||||
|
||||
async updateContainerItem(
|
||||
scheduleId: string,
|
||||
itemId: string,
|
||||
dto: UpdateContainerItemDto,
|
||||
): Promise<{ id: string; containerNumber: string | null }> {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
}
|
||||
if (schedule.status === 'DISPATCHED') {
|
||||
throw new BadRequestException('Cannot edit a dispatched schedule');
|
||||
}
|
||||
|
||||
const item = await this.dataSource.getRepository(WagonAllocationContainerItem).findOne({
|
||||
where: { id: itemId },
|
||||
relations: ['wagonBookingAllocation', 'wagonBookingAllocation.trainSetWagon'],
|
||||
});
|
||||
|
||||
if (!item) {
|
||||
throw new NotFoundException(`Container item ${itemId} not found`);
|
||||
}
|
||||
|
||||
const wagonId = item.wagonBookingAllocationId;
|
||||
const wagonAllocation = await this.dataSource.getRepository(WagonBookingAllocation).findOne({
|
||||
where: { id: wagonId },
|
||||
relations: ['trainSetWagon'],
|
||||
});
|
||||
|
||||
if (!wagonAllocation?.trainSetWagon) {
|
||||
throw new NotFoundException(`Container item ${itemId} does not belong to this schedule`);
|
||||
}
|
||||
|
||||
const trainSetWagonId = wagonAllocation.trainSetWagon.id;
|
||||
const wagonIds = (schedule.trainSet?.wagons ?? []).map((w) => w.id);
|
||||
if (!wagonIds.includes(trainSetWagonId)) {
|
||||
throw new NotFoundException(`Container item ${itemId} does not belong to this schedule`);
|
||||
}
|
||||
|
||||
await this.dataSource.getRepository(WagonAllocationContainerItem).update(itemId, {
|
||||
containerNumber: dto.containerNumber ?? null,
|
||||
});
|
||||
|
||||
return { id: itemId, containerNumber: dto.containerNumber ?? null };
|
||||
}
|
||||
|
||||
async getUnassignedBookings(scheduleId: string): Promise<any[]> {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
if (!schedule) {
|
||||
throw new NotFoundException(`Train schedule ${scheduleId} not found`);
|
||||
}
|
||||
|
||||
const allBookings = await this.bookingsRepository.findAll({
|
||||
where: { trainScheduleId: scheduleId },
|
||||
select: ['id', 'reference', 'freightType', 'priorityScore', 'cargoTotalWeightVgm', 'status', 'schedulingStatus'],
|
||||
});
|
||||
|
||||
const allocatedBookingIds = await this.getWagonAssignedBookingIds(scheduleId);
|
||||
|
||||
const unassigned = allBookings.filter((b: any) => !allocatedBookingIds.has(b.id));
|
||||
return unassigned.sort((a: any, b: any) => (b.priorityScore ?? 0) - (a.priorityScore ?? 0));
|
||||
}
|
||||
|
||||
async getCompositionRemovals(scheduleId: string): Promise<any[]> {
|
||||
return this.trainCompositionRemovalLogRepository.findByScheduleId(scheduleId);
|
||||
}
|
||||
|
||||
private async getWagonAssignedBookingIds(scheduleId: string): Promise<Set<string>> {
|
||||
const schedule = await this.trainSchedulesRepository.findByIdWithFullGraph(scheduleId);
|
||||
const wagonIds = (schedule?.trainSet?.wagons ?? []).map((w) => w.id);
|
||||
|
||||
Reference in New Issue
Block a user