mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 16:28:12 +00:00
add booking request functionality for GENERAL customs contracts
- Create migration for booking_requests table with necessary fields and indexes. - Implement BookingRequestRepository for database operations related to booking requests. - Develop BookingRequestService to handle business logic for submitting, accepting, rejecting, and canceling booking requests. - Create DTOs for creating booking requests and reviewing them. - Define BookingRequest entity to map to the booking_requests table. - Add UI components for managing shipment requests, including detail and list pages. - Implement OperationDatePicker component for selecting available shipment days.
This commit is contained in:
@@ -2074,7 +2074,18 @@ export class TrainSchedulingService {
|
||||
* Supports sub-route matching: if originYardId and/or destinationYardId are provided,
|
||||
* returns schedules whose route passes through both yards in the correct order.
|
||||
*/
|
||||
async getBookableSchedules(originYardId?: string, destinationYardId?: string) {
|
||||
/**
|
||||
* Raw OPEN same-route schedule entities a new booking may target (with the
|
||||
* relations needed for capacity/fleet checks). Shared by getBookableSchedules
|
||||
* (which maps to list items) and getAvailableDaysForCargo (which needs the raw
|
||||
* originStationId / scheduledDepartureDate / trainSet).
|
||||
*/
|
||||
private async getBookableScheduleEntities(
|
||||
originYardId?: string,
|
||||
destinationYardId?: string,
|
||||
): Promise<
|
||||
import('../train-schedules/entities/train-schedule.entity').TrainSchedule[]
|
||||
> {
|
||||
const schedules = await this.trainSchedulesRepository.findAll({
|
||||
where: {
|
||||
bookingWindowStatus: 'OPEN',
|
||||
@@ -2089,7 +2100,7 @@ export class TrainSchedulingService {
|
||||
order: { scheduledDepartureDate: 'ASC' },
|
||||
});
|
||||
|
||||
const filteredSchedules = schedules
|
||||
return schedules
|
||||
.filter((s) => ['DRAFT', 'SCHEDULED'].includes(s.status))
|
||||
.filter((s) => {
|
||||
// Build the full stop list: origin -> milestones (ordered) -> destination
|
||||
@@ -2128,10 +2139,15 @@ export class TrainSchedulingService {
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.map((s) => this.mapScheduleListItem(s));
|
||||
});
|
||||
}
|
||||
|
||||
return filteredSchedules;
|
||||
async getBookableSchedules(originYardId?: string, destinationYardId?: string) {
|
||||
const schedules = await this.getBookableScheduleEntities(
|
||||
originYardId,
|
||||
destinationYardId,
|
||||
);
|
||||
return schedules.map((s) => this.mapScheduleListItem(s));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2151,6 +2167,93 @@ export class TrainSchedulingService {
|
||||
return { days: [...days].sort() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Cargo-aware day pool: the EAT days that are actually FEASIBLE for the given
|
||||
* cargo. A day is selectable only when ≥1 OPEN schedule on the route that day
|
||||
* has BOTH (a) enough AVAILABLE wagons of the cargo's matching type at that
|
||||
* schedule's origin yard, and (b) remaining train capacity (not fully
|
||||
* allocated). Days with trains but not enough matching wagons are excluded.
|
||||
* Same `{ days: string[] }` shape as getAvailableDays — the customer still
|
||||
* picks a DAY, not a train.
|
||||
*/
|
||||
async getAvailableDaysForCargo(input: {
|
||||
originYardId?: string;
|
||||
destinationYardId?: string;
|
||||
freightType: 'CONTAINER' | 'BULK';
|
||||
cargoTypeCode?: string | null;
|
||||
totalWeightTons?: number;
|
||||
containers?: Array<{ containerSize: string; quantity: number }>;
|
||||
}): Promise<{ days: string[] }> {
|
||||
const schedules = await this.getBookableScheduleEntities(
|
||||
input.originYardId,
|
||||
input.destinationYardId,
|
||||
);
|
||||
if (schedules.length === 0) return { days: [] };
|
||||
|
||||
const wagonTypes = await this.dataSource.getRepository(WagonType).find();
|
||||
|
||||
// Resolve the wagon type this cargo needs.
|
||||
const requiredType =
|
||||
input.freightType === 'BULK'
|
||||
? pickBulkWagonType(wagonTypes, input.cargoTypeCode)
|
||||
: wagonTypes.find(
|
||||
(wt) => wt.code === getDefaultContainerWagonTypeCode() && wt.isActive,
|
||||
);
|
||||
if (!requiredType) return { days: [] };
|
||||
|
||||
// How many wagons of that type the cargo needs.
|
||||
const slotsNeeded = this.wagonsNeededForCargo(input, requiredType);
|
||||
|
||||
// AVAILABLE wagons of the required type, counted once per origin yard.
|
||||
const availableByYard = new Map<string, number>();
|
||||
const availableAt = async (yardId: string): Promise<number> => {
|
||||
const cached = availableByYard.get(yardId);
|
||||
if (cached !== undefined) return cached;
|
||||
const counts = await this.countFleetAvailability(yardId);
|
||||
const n =
|
||||
counts.find((c) => c.wagonTypeId === requiredType.id)?.available ?? 0;
|
||||
availableByYard.set(yardId, n);
|
||||
return n;
|
||||
};
|
||||
|
||||
const days = new Set<string>();
|
||||
for (const s of schedules) {
|
||||
const hasCapacity =
|
||||
Math.max(0, (s.maxWagons ?? 0) - (s.trainSet?.wagonCount ?? 0)) > 0;
|
||||
if (!hasCapacity) continue;
|
||||
const enoughWagons = (await availableAt(s.originStationId)) >= slotsNeeded;
|
||||
if (!enoughWagons) continue;
|
||||
if (s.scheduledDepartureDate)
|
||||
days.add(eatDay(new Date(s.scheduledDepartureDate)));
|
||||
}
|
||||
return { days: [...days].sort() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wagons needed for a cargo (pre-booking estimate). BULK: ceil(weight /
|
||||
* capacity). CONTAINER: TEU packing — 40ft = 2 TEU, 20ft = 1 TEU, 2 TEU per
|
||||
* wagon. Mirrors wagon-plan.util without fabricating Booking entities.
|
||||
*/
|
||||
private wagonsNeededForCargo(
|
||||
input: {
|
||||
freightType: 'CONTAINER' | 'BULK';
|
||||
totalWeightTons?: number;
|
||||
containers?: Array<{ containerSize: string; quantity: number }>;
|
||||
},
|
||||
wagonType: WagonType,
|
||||
): number {
|
||||
if (input.freightType === 'BULK') {
|
||||
const capacity = Number(wagonType.capacityTons) || 1;
|
||||
const weight = Number(input.totalWeightTons ?? 0);
|
||||
return Math.max(1, Math.ceil(weight / capacity));
|
||||
}
|
||||
const teu = (input.containers ?? []).reduce((sum, c) => {
|
||||
const per = c.containerSize === '40ft' ? 2 : 1;
|
||||
return sum + per * Math.max(0, Number(c.quantity ?? 0));
|
||||
}, 0);
|
||||
return Math.max(1, Math.ceil(teu / 2));
|
||||
}
|
||||
|
||||
/** Whether a route has ≥1 OPEN bookable departure on a given EAT day. */
|
||||
async existsOpenScheduleOnRouteDay(
|
||||
originYardId: string,
|
||||
|
||||
Reference in New Issue
Block a user