mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
feat(clearance): preview charge documents before and after upload
This commit is contained in:
@@ -199,6 +199,7 @@ import {
|
||||
isPlaceholderContainerNumber,
|
||||
placementsForBookings,
|
||||
type ContainerUnitForPlacement,
|
||||
occupiedTeuBySlot,
|
||||
} from '../container-placement.util';
|
||||
|
||||
const SCHEDULABLE_BOOKING_STATUSES = ['PAID'] as const;
|
||||
@@ -1996,7 +1997,11 @@ export class TrainSchedulingService {
|
||||
);
|
||||
if (unplacedUnits.length) {
|
||||
const slots = getContainerSlotSequenceNos(preview.wagonPlan);
|
||||
const generated = autoFillPlacements(unplacedUnits, slots);
|
||||
const generated = autoFillPlacements(
|
||||
unplacedUnits,
|
||||
slots,
|
||||
occupiedTeuBySlot(containerPlacements ?? [], units),
|
||||
);
|
||||
const missing = findMissingContainerNumberIssues(unplacedUnits, generated);
|
||||
if (missing.length) {
|
||||
throw new BadRequestException({
|
||||
@@ -2053,16 +2058,23 @@ export class TrainSchedulingService {
|
||||
// Linked ride-alongs count as requested too: silently dropping one here is
|
||||
// exactly the delete-and-recreate orphan this method must never produce.
|
||||
const plannedIds = new Set(validation.bookings.map((b) => b.id));
|
||||
const droppedRequested = allBookingIds.filter((id) => !plannedIds.has(id));
|
||||
const dropped = allBookingIds.filter((id) => !plannedIds.has(id));
|
||||
const reasonById = new Map(
|
||||
validation.deferredBookings.map((d) => [d.id, `${d.reference}: ${d.reason}`]),
|
||||
);
|
||||
const detailOf = (id: string) =>
|
||||
reasonById.get(id) ?? `${id}: does not fit the train's wagon stock or capacity`;
|
||||
// Auto-allocation (keepDeferredLinked): an ALREADY-LINKED booking the plan
|
||||
// cannot seat — e.g. it boards at a yard whose planned wagons are all taken
|
||||
// — stays linked and is parked WAITING_FOR_WAGON for staff, instead of
|
||||
// aborting the whole rebuild and leaving every OTHER paid booking without a
|
||||
// wagon too. Explicitly requested ids still fail loudly.
|
||||
const keptDeferredIds = dto.keepDeferredLinked
|
||||
? dropped.filter((id) => !dto.bookingIds.includes(id))
|
||||
: [];
|
||||
const droppedRequested = dropped.filter((id) => !keptDeferredIds.includes(id));
|
||||
if (droppedRequested.length) {
|
||||
const reasonById = new Map(
|
||||
validation.deferredBookings.map((d) => [d.id, `${d.reference}: ${d.reason}`]),
|
||||
);
|
||||
const details = droppedRequested.map(
|
||||
(id) =>
|
||||
reasonById.get(id) ??
|
||||
`${id}: does not fit the train's wagon stock or capacity`,
|
||||
);
|
||||
const details = droppedRequested.map(detailOf);
|
||||
throw new BadRequestException({
|
||||
message: `Cannot allocate — ${details.join('; ')}`,
|
||||
violations: details,
|
||||
@@ -2072,6 +2084,9 @@ export class TrainSchedulingService {
|
||||
}
|
||||
|
||||
const { bookings, wagonPlan, warnings, deferredBookings } = validation;
|
||||
for (const id of keptDeferredIds) {
|
||||
warnings.push(`${detailOf(id)} — kept on the schedule, waiting for a wagon`);
|
||||
}
|
||||
const totalWeightTons = validation.summary.totalWeightTons;
|
||||
const totalLengthMeters = validation.summary.totalLengthMeters;
|
||||
|
||||
@@ -2179,10 +2194,10 @@ export class TrainSchedulingService {
|
||||
wagonPlan,
|
||||
);
|
||||
|
||||
const scheduleBookingRecords = bookings.map((booking) => ({
|
||||
trainScheduleId: scheduleId,
|
||||
bookingId: booking.id,
|
||||
}));
|
||||
const scheduleBookingRecords = [
|
||||
...bookings.map((booking) => booking.id),
|
||||
...keptDeferredIds,
|
||||
].map((bookingId) => ({ trainScheduleId: scheduleId, bookingId }));
|
||||
await this.trainScheduleBookingsRepository.createMany(scheduleBookingRecords, manager);
|
||||
|
||||
await this.persistAllocationsAndLoads(
|
||||
@@ -2208,6 +2223,13 @@ export class TrainSchedulingService {
|
||||
manager,
|
||||
);
|
||||
}
|
||||
for (const bookingId of keptDeferredIds) {
|
||||
await this.bookingsRepository.updateSchedulingFields(
|
||||
bookingId,
|
||||
{ schedulingStatus: SchedulingStatus.WaitingForWagon },
|
||||
manager,
|
||||
);
|
||||
}
|
||||
|
||||
if (schedule.status === TrainScheduleStatusEnum.Draft && bookings.length > 0) {
|
||||
await this.trainSchedulesRepository.updateStatus(
|
||||
@@ -4928,6 +4950,7 @@ export class TrainSchedulingService {
|
||||
stock,
|
||||
legs: legByBookingId,
|
||||
edgeCount: Math.max(1, stops.length - 1),
|
||||
stops,
|
||||
});
|
||||
violations.push(...planned.configIssues);
|
||||
const fittingBookings = planned.fitting;
|
||||
@@ -9362,8 +9385,7 @@ export class TrainSchedulingService {
|
||||
|
||||
if (!performAssign || !assignableIds.length) return result;
|
||||
|
||||
const needsPlacements = containerBookings.some((b) => assignableSet.has(b.id));
|
||||
if (needsPlacements && !assignPlacements.length) {
|
||||
if (containerBookings.some((b) => assignableSet.has(b.id)) && !assignPlacements.length) {
|
||||
return {
|
||||
...result,
|
||||
violations: [...result.violations, 'Container placements could not be generated'],
|
||||
@@ -9371,12 +9393,14 @@ export class TrainSchedulingService {
|
||||
}
|
||||
|
||||
try {
|
||||
// No placements handed over on purpose: the assign re-plans the WHOLE
|
||||
// linked set (incl. non-eligible linked bookings such as one already in
|
||||
// transit), so slot numbers from THIS preview would not line up with the
|
||||
// plan it builds — it auto-fills every unit against its own plan instead.
|
||||
// The preview placements above only serve the missing-number check.
|
||||
await this.assignBookingsToSchedule(
|
||||
schedule.id,
|
||||
{
|
||||
bookingIds: assignableIds,
|
||||
containerPlacements: needsPlacements ? assignPlacements : undefined,
|
||||
},
|
||||
{ bookingIds: assignableIds, keepDeferredLinked: true },
|
||||
undefined,
|
||||
);
|
||||
result.assignedBookingIds = assignableIds;
|
||||
@@ -10044,8 +10068,11 @@ export class TrainSchedulingService {
|
||||
if (!booking) return null;
|
||||
|
||||
const wagonAssignedIds = await this.getWagonAssignedBookingIds(scheduleId);
|
||||
// Count the fleet at the yard the BOOKING boards from, not the train's
|
||||
// origin: on a split consist a Mojo booking can only ride Mojo wagons, and
|
||||
// Gelan's 31 spare wagons told a paying customer there was no shortage.
|
||||
const fleetCounts = await this.countFleetAvailability(
|
||||
schedule.originStationId,
|
||||
booking.originYardId ?? schedule.originStationId,
|
||||
scheduleId,
|
||||
);
|
||||
const fleetByTypeId = new Map(
|
||||
|
||||
Reference in New Issue
Block a user