add per-container handling options for hazardous, reefer, and return services

- Introduced new boolean fields (isHazardous, isReefer, isReturn) in UnitDraft and related interfaces to allow individual container handling options.
- Updated emptyUnit function to initialize these new fields.
- Modified GlCreateBookingForm to handle and display these options for each container.
- Adjusted calculations for hazardous, reefer, and return quantities based on the new handling options.
- Updated the schema for container units and booking container lines to include handling options.
- Added migration to support the new return flag in the database.
- Enhanced various components to reflect gross weight calculations, ensuring consistency across the application.
This commit is contained in:
Marshal
2026-07-18 19:20:45 +00:00
parent a7041ee70f
commit 0dead281ce
28 changed files with 585 additions and 250 deletions

View File

@@ -3607,11 +3607,19 @@ export class BookingBatchService implements OnModuleInit {
/** See {@link isScheduleFull} — same check for callers that already hold the full graph. */
private async isTrainFull(schedule: TrainSchedule): Promise<boolean> {
// Built train: the physical consist is the only capacity axis, and a wagon
// is committed to its booking for the WHOLE trip — wagon allocation has no
// leg concept, so a wagon hauling Negad→Mojo cargo can never be re-sold for
// the Doraleh→Negad edge it merely passes through. Count commitments
// train-wide, not per corridor edge: the per-edge budget read "free slots"
// on pass-through legs of a sold-out consist, so the window of a full train
// cycled OPEN forever instead of concluding DONE (and the day pool's
// leftover bookings were never expired).
const physicalWagons = await this.builtTrainWagonCount(schedule);
if (physicalWagons != null) {
return (await this.committedWagons(schedule)) >= physicalWagons;
}
if ((await this.remainingWagons(schedule)) <= 0) return true;
// Built train: the physical consist is the only capacity axis. Weight and
// length were enforced when the consist was assembled (builder /
// adjust-consist), so a free wagon slot means the train genuinely has room.
if ((await this.builtTrainWagonCount(schedule)) != null) return false;
const locomotive = schedule.trainSet?.locomotive;
if (!locomotive) return false; // no weight/length limits to bind against
const wagonDims = await this.loadWagonDims();
@@ -3620,6 +3628,29 @@ export class BookingBatchService implements OnModuleInit {
return budget.isExhausted(this.minPerWagonNeed(wagonDims));
}
/**
* Wagons the schedule's allocated + reserved bookings occupy train-wide,
* regardless of which corridor leg each rides. Deduped by booking id — a
* booking mid-settle can momentarily be both linked and reserved.
*/
private async committedWagons(schedule: TrainSchedule): Promise<number> {
const wagonDims = await this.loadWagonDims();
const allocated = (schedule.scheduleBookings ?? [])
.map((sb) => sb.booking)
.filter((b): b is Booking => Boolean(b));
const reserved = await this.bookingsRepository.findReservedForSchedule(
schedule.id,
);
const byId = new Map(
[...allocated, ...reserved].map((b) => [b.id, b] as const),
);
let total = 0;
for (const booking of byId.values()) {
total += this.wagonsFor(booking, wagonDims);
}
return total;
}
/**
* Smallest gross weight / shortest length one more wagon could add: the
* lightest wagon type at its rated payload. Feeds CorridorBudget.isExhausted,