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

@@ -14,6 +14,19 @@ function isValidValidityDays(value: string): boolean {
return Number.isInteger(days) && days >= 1 && days <= 365;
}
/**
* Decisions that must be applied to BOTH halves of a consolidated pair. The two
* bookings share one wagon: accepting one alone would put half a wagon into the
* approval chain, and cancelling one alone would strand the other on a wagon it
* can no longer fill.
*/
const PAIRED_DECISIONS = {
accept: "accept",
cancel: "cancel",
operationAccept: "operationAccept",
requestChanges: "requestChanges",
} as const;
export function useBookingActionDialog(
bookingId: string,
context: BookingActionContext,
@@ -52,6 +65,30 @@ export function useBookingActionDialog(
const onSuccess = () => closeDialog();
// A booking on a shared wagon routes the four pairable decisions through the
// paired endpoint, which applies them to both halves all-or-nothing. Every
// other action stays per booking.
const pairedDecision =
PAIRED_DECISIONS[pendingAction.id as keyof typeof PAIRED_DECISIONS];
if (context.consolidationPartnerId && pairedDecision) {
if (pairedDecision === "accept") {
const days = Number(inputValue.trim());
if (!Number.isInteger(days) || days < 1 || days > 365) return;
mutations.pairedDecision.mutate(
{ decision: "accept", validityDays: days },
{ onSuccess },
);
return;
}
mutations.pairedDecision.mutate(
pairedDecision === "cancel"
? { decision: "cancel", reason: inputValue.trim() }
: { decision: pairedDecision, note: inputValue.trim() },
{ onSuccess },
);
return;
}
switch (pendingAction.id) {
case "accept": {
const days = Number(inputValue.trim());