feat: Implement consolidated booking functionality

- Added support for viewing and managing consolidated bookings in BookingRequestDetailPage.
- Enhanced BookingRequestsPage to display paired bookings in a single row.
- Introduced pairedDecision method in bookings service to handle decisions for both halves of a consolidated pair.
- Updated contracts service to include methods for manual consolidation of odd-20ft bookings.
- Created new components for selecting and editing consolidation partners.
- Added tests for paired decision logic and manual consolidation scenarios.
- Updated UI to reflect changes in booking handling and provide user feedback for odd container counts.
This commit is contained in:
Marshal
2026-08-18 12:50:35 +00:00
parent c723b660e2
commit 22a3fb98ee
25 changed files with 2121 additions and 34 deletions

View File

@@ -323,6 +323,26 @@ export const bookingsService = {
cancel: (id: string, reason: string) =>
postBooking<BookingDetail>(B.CANCEL(id), { reason }),
/**
* Apply one staff decision to BOTH halves of a consolidated pair. The two
* bookings share a wagon, so they advance or cancel together — all-or-nothing
* on the server. Each half keeps its own invoice and payment.
*/
pairedDecision: async (
id: string,
decision: "accept" | "cancel" | "operationAccept" | "requestChanges",
options: { reason?: string; note?: string; validityDays?: number } = {},
): Promise<{ booking: BookingDetail; partner: BookingDetail }> => {
const response = await client.post(B.PAIRED_DECISION(id), {
decision,
...options,
});
return unwrap(response.data) as {
booking: BookingDetail;
partner: BookingDetail;
};
},
create: async (payload: Record<string, unknown>): Promise<BookingDetail> => {
const response = await client.post<{ booking: BookingDetail } | BookingDetail>(
B.BASE,

View File

@@ -71,6 +71,32 @@ export interface ShipmentValidation {
totalAmount?: number;
}
/**
* A booking GL may pick as the shared-wagon partner of an odd-20ft customs
* booking. `hasCargo` is false for a bare instance whose containers GL still
* enters on the split completion form.
*/
export interface ConsolidationCandidate {
id: string;
reference: string;
contractId: string | null;
companyName: string | null;
status: string;
tradeDirection: string | null;
originYardId: string | null;
destinationYardId: string | null;
scheduledDate: string | null;
ft20Quantity: number;
hasCargo: boolean;
}
/** Both halves of a shared-wagon completion, each with its own full payload. */
export interface CompleteConsolidatedPairPayload {
partnerBookingId: string;
booking: Freight.CreateBookingUnderContractDto;
partner: Freight.CreateBookingUnderContractDto;
}
export interface ContractListSummaryMetrics {
inQueue: number;
needsAction: number;
@@ -674,6 +700,46 @@ export const contractsService = {
};
},
/**
* Bookings GL may link to an odd-20ft customs booking as its shared-wagon
* partner (same route and direction, customs, odd 20ft, not already paired).
*/
listConsolidationCandidates: async (
id: string,
bookingId: string,
): Promise<ConsolidationCandidate[]> => {
const response = await client.get(
C.CONSOLIDATION_CANDIDATES(id, bookingId),
);
return (unwrap(response.data) ?? []) as ConsolidationCandidate[];
},
/**
* Complete an odd-20ft booking together with the partner booking sharing its
* wagon. All-or-nothing on the server: either both bookings complete and are
* linked, or neither does. Each booking keeps its own price and its own
* invoice — only the wagon is shared.
*/
completeConsolidatedPair: async (
id: string,
bookingId: string,
payload: CompleteConsolidatedPairPayload,
): Promise<{
booking: { id: string; reference: string };
partner: { id: string; reference: string };
warnings?: string[];
}> => {
const response = await client.post(
C.BOOKINGS_COMPLETE_CONSOLIDATED(id, bookingId),
payload,
);
return unwrap(response.data) as {
booking: { id: string; reference: string };
partner: { id: string; reference: string };
warnings?: string[];
};
},
/**
* Pre-create validation + authoritative price preview: the same
* BookingPricingService pass that prices the booking on create (rail +