feat: add BookingWagonsPanel component for displaying allocated wagons in booking details

- Implemented BookingWagonsPanel to show allocated wagons, their containers, and export functionality.
- Integrated the new panel into BookingRequestDetailPage and BookingRequestsPage.
- Enhanced wagon cancellation modal to support rebooking of wagon cancellations with partner units.
- Updated API service to include a method for downloading wagons workbook.
- Modified types and constants to accommodate new features related to wagons.
- Adjusted various components and pages to ensure compatibility with the new wagon-related functionality.
This commit is contained in:
marshalyordanos
2026-09-03 15:39:27 +03:00
parent 165146c09d
commit 9c57aa1c0c
23 changed files with 1244 additions and 61 deletions

View File

@@ -43,6 +43,9 @@ function makeService(overrides?: {
findBookingsWithUnreviewedDocuments: jest
.fn()
.mockResolvedValue(new Set<string>()),
findBookingsWithRedeemableCredit: jest
.fn()
.mockResolvedValue(new Map<string, string>()),
};
const bookingsService = {
findById: jest.fn().mockResolvedValue(booking),

View File

@@ -1347,6 +1347,17 @@ export class BookingClearanceService {
.hasDocumentsAwaitingReview = pending.has(b.id);
}
// A cancelled booking may still hold a paid-for wagon-cancellation credit.
// GL redeems it from this queue, so the row carries the cancellation id the
// rebook action needs.
const credits = await this.bookingsRepository.findBookingsWithRedeemableCredit(
filtered.map((b) => b.id),
);
for (const b of filtered) {
(b as Booking & { rebookableCancellationId?: string | null })
.rebookableCancellationId = credits.get(b.id) ?? null;
}
const rows = await this.attachContractSummary(filtered);
return this.narrowToYardScope(rows, user);
}

View File

@@ -183,8 +183,8 @@ describe('ContractBookingService — manual odd-20ft consolidation', () => {
{ quantity: 4, containerType: { sizeFt: 20 } },
],
},
// A bare instance has no cargo yet — GL enters it on the split form, so it
// stays a candidate.
// Cargo not entered yet — its 20ft count is unknown, so it cannot be
// shown to fill the wagon and is not offered.
{ id: 'bare', reference: 'BK-BARE', bookingContainers: [] },
];
@@ -198,7 +198,7 @@ describe('ContractBookingService — manual odd-20ft consolidation', () => {
rows.filter((row) => {
void booking;
const lines = row.bookingContainers ?? [];
if (lines.length === 0) return true;
if (lines.length === 0) return false;
const ft20 = lines
.filter((l) => Number(l.containerType?.sizeFt) === 20)
.reduce((sum, l) => sum + Number(l.quantity || 0), 0);
@@ -209,8 +209,8 @@ describe('ContractBookingService — manual odd-20ft consolidation', () => {
});
const candidates = await service.listConsolidationCandidates('c-1', 'b-1');
expect(candidates.map((c) => c.reference)).toEqual(['BK-ODD', 'BK-BARE']);
expect(candidates.map((c) => c.reference)).toEqual(['BK-ODD']);
expect(candidates[0].ft20Quantity).toBe(3);
expect(candidates[1].hasCargo).toBe(false);
expect(candidates[0].hasCargo).toBe(true);
});
});