mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
enhance booking windows section with pagination and improved UI
This commit is contained in:
@@ -431,7 +431,7 @@ export class BookingPricingService {
|
||||
|
||||
const lines: PriceLineItemDto[] = [];
|
||||
const usedRatesMap = new Map<string, Rate>();
|
||||
const wagonCount = await this.bookingsRepository.calculateWagonCount(booking.id);
|
||||
const wagonCount = await this.resolveWagonCount(booking);
|
||||
|
||||
for (const container of evalInput.containers) {
|
||||
const rate = this.pickRate(liveRates, rateType, container.containerTypeId, 'USD');
|
||||
@@ -571,6 +571,23 @@ export class BookingPricingService {
|
||||
return { lineItems: lines, usedRates: [...usedRatesMap.values()] };
|
||||
}
|
||||
|
||||
/**
|
||||
* Wagon count for PER_WAGON rates. A persisted booking uses the SQL aggregate;
|
||||
* an unsaved preview booking (no id) sums the wagonsRequired already computed
|
||||
* on its in-memory container lines — same math, no DB row needed.
|
||||
*/
|
||||
private async resolveWagonCount(booking: Booking): Promise<number> {
|
||||
if (!booking.id) {
|
||||
return Math.ceil(
|
||||
(booking.bookingContainers ?? []).reduce(
|
||||
(sum, bc) => sum + Number(bc.wagonsRequired ?? 0),
|
||||
0,
|
||||
),
|
||||
);
|
||||
}
|
||||
return this.bookingsRepository.calculateWagonCount(booking.id);
|
||||
}
|
||||
|
||||
/** Friendly container-type label for the per-unit card; degrades to "Container". */
|
||||
private async containerTypeLabel(containerTypeId: string): Promise<string> {
|
||||
try {
|
||||
|
||||
@@ -1051,7 +1051,22 @@ export class BookingTransitionService {
|
||||
// paid → auto-allocated by the settle/paid pipeline. Consolidated bookings
|
||||
// only reserve once both partners are FULLY_EXECUTED (handled inside).
|
||||
const fresh = await this.bookingsService.findById(booking.id);
|
||||
await this.bookingBatchService.acceptExportBooking(fresh);
|
||||
try {
|
||||
await this.bookingBatchService.acceptExportBooking(fresh);
|
||||
} catch (err) {
|
||||
// The status update above already committed. Without compensation the
|
||||
// client gets an error for a booking that reads as accepted after a
|
||||
// refresh — half-applied state. Put the request back so staff can retry.
|
||||
await this.bookingsRepository.update(booking.id, {
|
||||
status: "OPERATION_REQUEST_PENDING",
|
||||
fullyExecutedAt: null,
|
||||
lockedAt: booking.lockedAt ?? null,
|
||||
} as never);
|
||||
this.logger.warn(
|
||||
`Export accept failed post-commit for ${booking.reference}:${booking.id}; reverted to OPERATION_REQUEST_PENDING: ${(err as Error).message}`,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
// IMPORT and DOMESTIC bookings wait for their booking-day window cycle — the
|
||||
// batch runs after the window closes + staff document review, never at accept
|
||||
@@ -1073,23 +1088,59 @@ export class BookingTransitionService {
|
||||
} | null;
|
||||
}
|
||||
> {
|
||||
const note = await this.bookingsRepository.findLatestReviewNote(
|
||||
booking.id,
|
||||
"CHANGES_REQUESTED",
|
||||
);
|
||||
const summary =
|
||||
booking.contractSummary ??
|
||||
this.contractService.buildContractSummary(booking);
|
||||
const nextPending =
|
||||
booking.status === "PENDING_APPROVAL" ||
|
||||
booking.status === "APPROVED_PENDING_SIGNATURE"
|
||||
? await this.bookingsRepository.findNextPendingApprovalStep(booking.id)
|
||||
: null;
|
||||
const nextStep = computeNextStep(booking, nextPending);
|
||||
const activeBatchOffer =
|
||||
booking.status === "SELECTED_FOR_BATCH"
|
||||
? await this.bookingBatchService.getOpenOfferSummary(booking.id)
|
||||
: null;
|
||||
// This enrichment runs AFTER the transition has committed. A failure here
|
||||
// must never 500 the response — the client would report "failed" for a
|
||||
// transition that actually succeeded (visible only after a refresh).
|
||||
// Degrade each fragile field to null instead.
|
||||
let note: Awaited<
|
||||
ReturnType<typeof this.bookingsRepository.findLatestReviewNote>
|
||||
> = null;
|
||||
try {
|
||||
note = await this.bookingsRepository.findLatestReviewNote(
|
||||
booking.id,
|
||||
"CHANGES_REQUESTED",
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`enrichBookingResponse: review-note lookup failed for ${booking.id}: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
let summary: string | null = booking.contractSummary ?? null;
|
||||
try {
|
||||
summary =
|
||||
booking.contractSummary ??
|
||||
this.contractService.buildContractSummary(booking);
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`enrichBookingResponse: contract summary failed for ${booking.id}: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
let nextStep: BookingNextStep | null = null;
|
||||
try {
|
||||
const nextPending =
|
||||
booking.status === "PENDING_APPROVAL" ||
|
||||
booking.status === "APPROVED_PENDING_SIGNATURE"
|
||||
? await this.bookingsRepository.findNextPendingApprovalStep(booking.id)
|
||||
: null;
|
||||
nextStep = computeNextStep(booking, nextPending);
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`enrichBookingResponse: next-step lookup failed for ${booking.id}: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
let activeBatchOffer: Awaited<
|
||||
ReturnType<typeof this.bookingBatchService.getOpenOfferSummary>
|
||||
> = null;
|
||||
try {
|
||||
activeBatchOffer =
|
||||
booking.status === "SELECTED_FOR_BATCH"
|
||||
? await this.bookingBatchService.getOpenOfferSummary(booking.id)
|
||||
: null;
|
||||
} catch (err) {
|
||||
this.logger.warn(
|
||||
`enrichBookingResponse: batch-offer lookup failed for ${booking.id}: ${(err as Error).message}`,
|
||||
);
|
||||
}
|
||||
return {
|
||||
...booking,
|
||||
latestChangeRequestNote: note?.note ?? null,
|
||||
|
||||
@@ -587,6 +587,10 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
||||
.leftJoinAndSelect('booking.serviceType', 'serviceType')
|
||||
.leftJoinAndSelect('booking.approvalSteps', 'approvalSteps')
|
||||
.leftJoinAndSelect('booking.consolidationPartner', 'consolidationPartner')
|
||||
// Contract reference for the list column + search (no entity relation on
|
||||
// Booking → contract, so join by id and select just the reference).
|
||||
.leftJoin('freight.contracts', 'contract', 'contract.id = booking.contract_id')
|
||||
.addSelect('contract.reference', 'contract_reference')
|
||||
.where('booking.deleted_at IS NULL');
|
||||
|
||||
this.applyListFilters(qb, options);
|
||||
@@ -605,10 +609,24 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
||||
qb.orderBy(sortField, options.sortOrder ?? 'DESC');
|
||||
}
|
||||
|
||||
const [items, total] = await qb
|
||||
const total = await qb.getCount();
|
||||
const { entities: items, raw } = await qb
|
||||
.skip((page - 1) * pageSize)
|
||||
.take(pageSize)
|
||||
.getManyAndCount();
|
||||
.getRawAndEntities();
|
||||
|
||||
// The joined contract.reference comes back on the raw rows only (entity has no
|
||||
// contract relation) — map it onto each booking by position.
|
||||
const contractRefByBooking = new Map<string, string | null>();
|
||||
for (const row of raw as Array<{ booking_id: string; contract_reference: string | null }>) {
|
||||
if (row.booking_id && !contractRefByBooking.has(row.booking_id)) {
|
||||
contractRefByBooking.set(row.booking_id, row.contract_reference ?? null);
|
||||
}
|
||||
}
|
||||
for (const item of items) {
|
||||
(item as Booking & { contractReference?: string | null }).contractReference =
|
||||
contractRefByBooking.get(item.id) ?? null;
|
||||
}
|
||||
|
||||
if (items.length) {
|
||||
const links = await this.dataSource.getRepository(TrainScheduleBooking).find({
|
||||
|
||||
Reference in New Issue
Block a user