From 3591abd27fa17a35d4c70a8f315ee2b7dc1a8f65 Mon Sep 17 00:00:00 2001 From: Marshal Date: Fri, 19 Jun 2026 10:45:11 +0000 Subject: [PATCH] feat(bookings): enhance booking details with consolidation notices and streamline form handling --- .../BookingDetailPage/ReadonlyBookingView.tsx | 19 +++- .../BookingDetailPage/components/Notices.tsx | 94 ++++++++++++++++++- .../components/ScheduleCard.tsx | 3 - .../components/ShipmentDetailsCard.tsx | 5 +- .../src/pages/bookings/EditBookingPage.tsx | 14 ++- .../pages/bookings/new-booking-form/schema.ts | 15 ++- .../new-booking-form/step1-contract-type.tsx | 4 +- .../bookings/new-booking-form/step4-route.tsx | 15 ++- .../new-booking-form/step8-review.tsx | 4 - 9 files changed, 141 insertions(+), 32 deletions(-) diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx index c340dc845..6fb25516a 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/ReadonlyBookingView.tsx @@ -12,7 +12,11 @@ import { ActivityCard } from "./components/ActivityCard"; import { ContractCard } from "./components/ContractCard"; import { DocRow, IconSquare } from "./components/Documents"; import { BodyGrid, CardTitle, PageShell, SectionCard } from "./components/layout"; -import { CancelledBanner } from "./components/Notices"; +import { + CancelledBanner, + ConsolidationPairedNotice, + ConsolidationWaitingBanner, +} from "./components/Notices"; import { HeaderButton, PageHeader } from "./components/PageHeader"; import { PaymentDeadlineCard } from "./components/PaymentDeadlineCard"; import { PaymentMethodModal } from "./components/PaymentMethodModal"; @@ -48,6 +52,13 @@ export function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) status === "SELECTED_FOR_BATCH" && booking.paymentStatus !== "PAID"; const showCountdown = canPay && !!booking.paymentDeadline; const isExpired = status === "EXPIRED"; + const isPendingConsolidation = status === "PENDING_CONSOLIDATION"; + // Paired: a consolidation partner was found and the booking resumed the normal + // flow. Surface the "partner found" reassurance only in the early stages, + // before approval, so it doesn't linger for the rest of the booking's life. + const showPairedNotice = + !!booking.consolidationPartnerId && + ["SUBMITTED", "PENDING_APPROVAL", "CHANGES_REQUESTED"].includes(status); return ( @@ -92,10 +103,16 @@ export function ReadonlyBookingView({ booking }: { booking: Freight.IBooking }) subtitle="Payment wasn't completed in time, so this booking lost its slot. Rebook to try another schedule." onRebook={() => navigate("/bookings/new")} /> + ) : isPendingConsolidation ? ( + ) : ( )} + {showPairedNotice && } + + + +
+ +
+ + + Waiting for a partner + + + Your shipment is waiting to share a wagon + + + Your cargo only fills part of a wagon, so we’re pairing it with + another shipment on the same route to share the space. As soon as a + matching shipment is found, your booking continues automatically — + acceptance, approval and contract stay independent and yours alone. + + +
+ {priceLabel && ( +
+ + Your price (held) + + + {priceLabel} + +
+ )} +
+ + ); +} + +/** + * A brief positive notice shown once a consolidation partner has been found and + * the booking has resumed the normal flow (SUBMITTED with a partner linked). + * Reassures the customer the wait ended; the booking proceeds independently. + */ +export function ConsolidationPairedNotice() { + return ( +
+ +
+ + Consolidation partner found + + + A matching shipment was found to share the wagon, so your booking is + back on track and now moving through review and approval as usual. + Nothing more is needed from you for now. + +
+
+ ); +} + export function MutationErrors({ mutations, }: { diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ScheduleCard.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ScheduleCard.tsx index 5f64d10a1..52451028d 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ScheduleCard.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ScheduleCard.tsx @@ -61,7 +61,6 @@ export function ScheduleCard({ : "Rail only"; const equipmentReturn = booking.equipmentReturn === "WITH_RETURN" ? "With return" : "Without return"; - const consolidation = booking.allowConsolidation ? "Allowed" : "Not allowed"; const assignedTrain: Row = { label: "Assigned train", value: booking.trainId ?? "Not yet assigned", @@ -80,7 +79,6 @@ export function ScheduleCard({ { label: "Equipment return", value: equipmentReturn }, assignedTrain, { label: "Scheduled", value: fmtDate(booking.scheduledDate) }, - { label: "Consolidation", value: consolidation }, ] : [ statusRow, @@ -88,7 +86,6 @@ export function ScheduleCard({ { label: "Equipment return", value: equipmentReturn }, { label: "Proposed date", value: fmtDate(booking.scheduledDate) }, assignedTrain, - { label: "Consolidation", value: consolidation }, ]; return ( diff --git a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ShipmentDetailsCard.tsx b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ShipmentDetailsCard.tsx index 259a5c113..d15730541 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ShipmentDetailsCard.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/BookingDetailPage/components/ShipmentDetailsCard.tsx @@ -44,10 +44,7 @@ export function ShipmentDetailsCard({ booking }: { booking: Freight.IBooking }) ], ["Scheduled date", fmtDate(booking.scheduledDate)], ], - [ - ["Consolidation", booking.allowConsolidation ? "Allowed" : "Not allowed"], - ["Assigned train", booking.trainId ?? "Not yet assigned"], - ], + [["Assigned train", booking.trainId ?? "Not yet assigned"]], ]; return ( diff --git a/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx index f0f765cd6..bcaecad8f 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx @@ -359,10 +359,16 @@ export default function EditBookingPage() { const shippingLineOptions = useMemo(() => { if (!referenceData?.shipping_line) return []; - return referenceData.shipping_line.map((sl) => ({ - value: sl.name, - label: sl.name, - })); + // Dedupe by name (the value the form keys on) so two lines sharing a name + // can't produce a duplicate Select option and crash Mantine. + const seen = new Set(); + const options: { value: string; label: string }[] = []; + for (const sl of referenceData.shipping_line) { + if (!sl.name || seen.has(sl.name)) continue; + seen.add(sl.name); + options.push({ value: sl.name, label: sl.name }); + } + return options; }, [referenceData]); const setDocument = (key: string, file: File | null) => { diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts index e2b040fbd..a55ef7962 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts @@ -138,7 +138,10 @@ export const bookingFormSchema = z .refine((vgm) => Number(vgm) >= 0, "Must be greater than 0"), }), ), - consolidationEnabled: z.boolean(), + // Consolidation is system-managed, not a customer choice. The backend only + // consolidates partial-wagon bookings, so this is always allowed; the + // customer neither sees nor toggles it. + consolidationEnabled: z.boolean().default(true), documents: z.record(z.string(), z.any()).default({}), notes: z.string(), }) @@ -243,7 +246,7 @@ export const initialBookingFormValues: DeepPartial = { isHazardous: false, isRefrigerated: false, containers: [{ type: "20ft", containerType: "", qty: "1", vgm: "" }], - consolidationEnabled: false, + consolidationEnabled: true, documents: {}, notes: "", }; @@ -265,13 +268,7 @@ export const stepFields: Record>> = { "isRefrigerated", "shippingLine", ], - 4: [ - "cargoType", - "cargoWeight", - "cargoTypePath", - "containers", - "consolidationEnabled", - ], + 4: ["cargoType", "cargoWeight", "cargoTypePath", "containers"], 5: ["scheduledDate"], 6: ["documents"], 7: ["notes"], diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx index f7545f042..d6fd968d5 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx @@ -174,8 +174,8 @@ export function Step1ContractType({ form.setValue("containers", mappedContainers); } - // ── Consolidation ─────────────────────────────────────────────────── - form.setValue("consolidationEnabled", booking.allowConsolidation); + // Consolidation is system-managed (always allowed) — not copied from the + // previous booking and not customer-controllable. // ── Scheduled date ────────────────────────────────────────────────── if (booking.scheduledDate) { diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step4-route.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step4-route.tsx index dd5df7d9c..cd05f4e9a 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step4-route.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step4-route.tsx @@ -35,10 +35,17 @@ export function Step4Route({ const shippingLineOptions = useMemo(() => { if (!referenceData?.shipping_line) return []; - return referenceData.shipping_line.map((sl) => ({ - value: sl.name, - label: sl.name, - })); + // The form keys shipping line by name, so options are keyed by name too. + // Dedupe by name: if the reference data has two lines sharing a name, a + // duplicate option would crash Mantine's Select ("Duplicate options..."). + const seen = new Set(); + const options: { value: string; label: string }[] = []; + for (const sl of referenceData.shipping_line) { + if (!sl.name || seen.has(sl.name)) continue; + seen.add(sl.name); + options.push({ value: sl.name, label: sl.name }); + } + return options; }, [referenceData]); const originData = useMemo(() => { diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx index 5e2e7a7fb..c9c8265fe 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx @@ -338,10 +338,6 @@ export function Step8Review({ label="Total VGM" value={totalVgm > 0 ? `${totalVgm.toFixed(1)} tons` : "—"} /> - {values.cargoType === "container" && values.containers.length > 0 && (