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

@@ -163,11 +163,17 @@ export function Step8Review({
.join(", ")
: "";
// 20ft containers must pair up (two per wagon) — an odd total blocks submit.
// 20ft containers must pair up (two per wagon). Without customs an odd total
// still blocks submit — nothing downstream can place the unpaired container.
// With customs it is allowed: Global Logistics completes the booking and links
// it to another customer's odd booking so the two share the wagon, so an odd
// count here is only a notice.
const { hasOddUnit: hasOdd20ft, ft20Wagons: twentyFtCount } =
values.cargoType === "container"
? calcWagons(values.containers ?? [])
: { hasOddUnit: false, ft20Wagons: 0 };
const oddPairsViaCustoms = hasOdd20ft && Boolean(values.customsClearingEnabled);
const oddBlocksSubmit = hasOdd20ft && !oddPairsViaCustoms;
const isGeneralContract = values.bookingType === "general_contract";
// Both one-time and general contracts take the bulk amount from the cargo step
@@ -543,7 +549,7 @@ export function Step8Review({
</Paper>
<Paper radius={20} p="lg" withBorder bg="white">
{hasOdd20ft ? (
{oddBlocksSubmit ? (
<Box mb="md">
<AlertBox tone="error">
<p className="font-semibold">
@@ -558,6 +564,20 @@ export function Step8Review({
</p>
</AlertBox>
</Box>
) : oddPairsViaCustoms ? (
<Box mb="md">
<AlertBox tone="info">
<p className="font-semibold">
Odd number of 20ft containers ({twentyFtCount})
</p>
<p className="mt-1 text-xs">
20ft containers travel two per wagon, so one of yours will
share a wagon with another shipment. Global Logistics
arranges the pairing when completing your booking you are
billed only for your own containers.
</p>
</AlertBox>
</Box>
) : noWagonForSelectedDay ? (
<Box mb="md">
<AlertBox tone="error">
@@ -587,7 +607,7 @@ export function Step8Review({
leftSection={<Send size={16} />}
onClick={onSubmit}
loading={submitPending}
disabled={submitPending || hasOdd20ft || noWagonForSelectedDay}
disabled={submitPending || oddBlocksSubmit || noWagonForSelectedDay}
>
Submit
</Button>

View File

@@ -106,14 +106,14 @@ export default function NewShipmentRequestPage() {
contract.cargoScope?.[0];
const isPerItem = bulkScope?.cargoType?.unitOfMeasure === "PER_ITEM";
// 20ft containers ride two per wagon, so an odd total can never be planned —
// and GL's create-booking form blocks it too, so an odd request would only
// dead-end there. Same even-number rule the booking forms apply.
// 20ft containers ride two per wagon. An odd total leaves one unpaired, which
// is allowed here: on a customs contract GL completes the booking and links it
// to another customer's odd booking so the two share the wagon. The request is
// therefore informational only, not a block.
const ft20Requested = isContainer ? Number(qtyBySize["20ft"]) || 0 : 0;
const hasOdd20ft = ft20Requested % 2 === 1;
const handleSubmit = () => {
if (hasOdd20ft) return;
const dto: Freight.CreateBookingRequestDto = {
contractRouteId: route?.id,
scheduledDate: hasCustoms ? undefined : scheduledDate || undefined,
@@ -220,17 +220,17 @@ export default function NewShipmentRequestPage() {
{hasOdd20ft ? (
<Alert
color="red"
color="blue"
variant="light"
radius="md"
icon={<AlertCircle size={16} />}
title={`Odd number of 20ft containers (${ft20Requested})`}
>
<Text fz={13}>
20ft containers travel two per wagon, so they must be requested
in even numbers. Please add one more 20ft container or remove
one (e.g. request {ft20Requested + 1} or {ft20Requested - 1}{" "}
instead of {ft20Requested}).
20ft containers travel two per wagon, so one of yours will
share a wagon with another shipment. Global Logistics arranges
the pairing when completing your booking you are billed only
for your own containers.
</Text>
</Alert>
) : null}
@@ -281,7 +281,6 @@ export default function NewShipmentRequestPage() {
leftSection={<Send size={16} />}
loading={submit.isPending}
onClick={handleSubmit}
disabled={hasOdd20ft}
>
Submit shipment request
</Button>