This commit is contained in:
natib21
2026-07-02 14:27:09 +00:00
parent 7d85f5dc2e
commit e5fed081fc
6 changed files with 36 additions and 2 deletions

View File

@@ -39,6 +39,7 @@ import { ContractRendererService } from "../../contracts/contract-renderer.servi
import { ContractTemplateResolver } from "../../contracts/contract-template.resolver"; import { ContractTemplateResolver } from "../../contracts/contract-template.resolver";
import { ContractViewModelBuilder } from "../../contracts/contract-view-model.builder"; import { ContractViewModelBuilder } from "../../contracts/contract-view-model.builder";
import { TrainSchedulingModule } from "../train-scheduling/train-scheduling.module"; import { TrainSchedulingModule } from "../train-scheduling/train-scheduling.module";
import { VehiclesModule } from "../vehicles/vehicles.module";
@Module({ @Module({
imports: [ imports: [
@@ -58,6 +59,7 @@ import { TrainSchedulingModule } from "../train-scheduling/train-scheduling.modu
forwardRef(() => TrainSchedulingModule), forwardRef(() => TrainSchedulingModule),
FilesModule, FilesModule,
MinioModule, MinioModule,
VehiclesModule,
CompaniesModule, CompaniesModule,
// CustomersModule, // CustomersModule,
RuleEngineModule, RuleEngineModule,

View File

@@ -31,6 +31,8 @@ import { ServiceType } from '../rule-engine/entities/service-type.entity';
import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity'; import { TrainSchedule } from '../train-schedules/entities/train-schedule.entity';
import { BookingsRepository } from './bookings.repository'; import { BookingsRepository } from './bookings.repository';
import { ConsolidationService } from './consolidation.service'; import { ConsolidationService } from './consolidation.service';
import { VehiclesService } from '../vehicles/vehicles.service';
import { VehicleAvailability } from '../vehicles/entities/vehicle.entity';
import { assertFreightShape } from './booking-freight.util'; import { assertFreightShape } from './booking-freight.util';
import { CreateBookingContainerDto, CreateBookingDto } from './dto/create-booking.dto'; import { CreateBookingContainerDto, CreateBookingDto } from './dto/create-booking.dto';
import { mapStatusCountsToTabs } from './booking-list-tabs.config'; import { mapStatusCountsToTabs } from './booking-list-tabs.config';
@@ -81,6 +83,7 @@ export class BookingsService {
private readonly ruleEngineService: RuleEngineService, private readonly ruleEngineService: RuleEngineService,
private readonly containerTypesService: ContainerTypesService, private readonly containerTypesService: ContainerTypesService,
private readonly consolidationService: ConsolidationService, private readonly consolidationService: ConsolidationService,
private readonly vehiclesService: VehiclesService,
) {} ) {}
/** Resolve trade direction from yard countries; reject client mismatch. */ /** Resolve trade direction from yard countries; reject client mismatch. */
@@ -1348,6 +1351,16 @@ export class BookingsService {
throw new NotFoundException(`Booking ${bookingId} not found`); throw new NotFoundException(`Booking ${bookingId} not found`);
} }
const previousAllocations = await this.dataSource.manager.find(BookingContainerAllocation, {
where: {
bookingId,
containerId: In(allocations.map((a) => a.containerId)),
},
});
const previousVehicleIds = previousAllocations
.map((a) => a.vehicleId)
.filter((id): id is string => Boolean(id));
await this.dataSource.transaction(async (manager) => { await this.dataSource.transaction(async (manager) => {
for (const allocation of allocations) { for (const allocation of allocations) {
await manager.delete(BookingContainerAllocation, { await manager.delete(BookingContainerAllocation, {
@@ -1364,6 +1377,16 @@ export class BookingsService {
} }
}); });
const vehicleIds = new Set(allocations.map((a) => a.vehicleId));
await Promise.all(
[...vehicleIds].map((vehicleId) =>
this.vehiclesService.setAvailability(vehicleId, VehicleAvailability.BUSY),
),
);
await this.vehiclesService.releaseIfUnused(
previousVehicleIds.filter((id) => !vehicleIds.has(id)),
);
return { return {
success: true, success: true,
allocated: allocations.length, allocated: allocations.length,

View File

@@ -8,6 +8,7 @@ import { FirstMile, FirstMileStatus } from '../first-mile/entities/first-mile.en
import { FirstMileContainerAllocation } from '../first-mile/entities/first-mile-container-allocation.entity'; import { FirstMileContainerAllocation } from '../first-mile/entities/first-mile-container-allocation.entity';
import { LastMile, LastMileStatus } from '../last-mile/entities/last-mile.entity'; import { LastMile, LastMileStatus } from '../last-mile/entities/last-mile.entity';
import { LastMileContainerAllocation } from '../last-mile/entities/last-mile-container-allocation.entity'; import { LastMileContainerAllocation } from '../last-mile/entities/last-mile-container-allocation.entity';
import { BookingContainerAllocation } from '../bookings/entities/booking-container-allocation.entity';
@Injectable() @Injectable()
export class VehiclesService { export class VehiclesService {
@@ -113,7 +114,7 @@ export class VehiclesService {
async releaseIfUnused(vehicleIds: string[]): Promise<void> { async releaseIfUnused(vehicleIds: string[]): Promise<void> {
const manager = this.vehicleRepo.manager; const manager = this.vehicleRepo.manager;
for (const vehicleId of [...new Set(vehicleIds)]) { for (const vehicleId of [...new Set(vehicleIds)]) {
const [fmRecords, lmRecords, fmAllocations, lmAllocations] = await Promise.all([ const [fmRecords, lmRecords, fmAllocations, lmAllocations, bookingAllocations] = await Promise.all([
manager.count(FirstMile, { manager.count(FirstMile, {
where: { vehicleId, status: Not<FirstMileStatus>('RECEIVED_TO_PORT') }, where: { vehicleId, status: Not<FirstMileStatus>('RECEIVED_TO_PORT') },
}), }),
@@ -134,8 +135,9 @@ export class VehiclesService {
.andWhere('lm.status != :done', { done: 'DELIVERED' }) .andWhere('lm.status != :done', { done: 'DELIVERED' })
.andWhere('lm.deletedAt IS NULL') .andWhere('lm.deletedAt IS NULL')
.getCount(), .getCount(),
manager.count(BookingContainerAllocation, { where: { vehicleId } }),
]); ]);
if (fmRecords + lmRecords + fmAllocations + lmAllocations === 0) { if (fmRecords + lmRecords + fmAllocations + lmAllocations + bookingAllocations === 0) {
await this.setAvailability(vehicleId, VehicleAvailability.FREE); await this.setAvailability(vehicleId, VehicleAvailability.FREE);
} }
} }

View File

@@ -33,6 +33,7 @@ const BookingDetailPage = () => {
onSuccess: () => { onSuccess: () => {
toast.success("Containers allocated"); toast.success("Containers allocated");
qc.invalidateQueries({ queryKey: QUERY_KEYS.BOOKINGS.byId(id ?? "") }); qc.invalidateQueries({ queryKey: QUERY_KEYS.BOOKINGS.byId(id ?? "") });
qc.invalidateQueries({ queryKey: ["vehicles"] });
}, },
onError: () => { onError: () => {
toast.error("Failed to allocate containers"); toast.error("Failed to allocate containers");

View File

@@ -400,6 +400,7 @@ const FirstMilePage = () => {
firstMileService.update(id, data), firstMileService.update(id, data),
onSuccess: () => { onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.list() }); void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.list() });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
}, },
onError: () => { onError: () => {
toast({ title: "Update failed", variant: "destructive" }); toast({ title: "Update failed", variant: "destructive" });
@@ -444,6 +445,7 @@ const FirstMilePage = () => {
}, },
onSuccess: () => { onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.ROOT }); void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.ROOT });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
toast({ title: "Booking accepted", description: "First-mile leg created successfully." }); toast({ title: "Booking accepted", description: "First-mile leg created successfully." });
closeAccept(); closeAccept();
}, },
@@ -460,6 +462,7 @@ const FirstMilePage = () => {
onSuccess: () => { onSuccess: () => {
toast({ title: "Containers allocated" }); toast({ title: "Containers allocated" });
void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.byId(containerAllocationFirstMileId ?? "") }); void qc.invalidateQueries({ queryKey: QUERY_KEYS.FIRST_MILE.byId(containerAllocationFirstMileId ?? "") });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
setContainerAllocationOpen(false); setContainerAllocationOpen(false);
setContainerAllocationFirstMileId(null); setContainerAllocationFirstMileId(null);
}, },

View File

@@ -377,6 +377,7 @@ const LastMilePage = () => {
lastMileService.update(id, data), lastMileService.update(id, data),
onSuccess: () => { onSuccess: () => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT }); void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
}, },
onError: () => { onError: () => {
toast({ title: "Update failed", variant: "destructive" }); toast({ title: "Update failed", variant: "destructive" });
@@ -415,6 +416,7 @@ const LastMilePage = () => {
onSuccess: () => { onSuccess: () => {
toast({ title: "Containers allocated", variant: "default" }); toast({ title: "Containers allocated", variant: "default" });
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.byId(activeId ?? "") }); void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.byId(activeId ?? "") });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
closeAllocation(); closeAllocation();
}, },
onError: () => { onError: () => {
@@ -453,6 +455,7 @@ const LastMilePage = () => {
}, },
onSuccess: (created) => { onSuccess: (created) => {
void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT }); void qc.invalidateQueries({ queryKey: QUERY_KEYS.LAST_MILE.ROOT });
void qc.invalidateQueries({ queryKey: ["vehicles"] });
toast({ toast({
title: "Last-mile leg created", title: "Last-mile leg created",
description: `${created.length} ${created.length === 1 ? "delivery" : "deliveries"} accepted successfully.`, description: `${created.length} ${created.length === 1 ? "delivery" : "deliveries"} accepted successfully.`,