From 733bfd1e5e116bc478a5bedb7540f07c98617064 Mon Sep 17 00:00:00 2001 From: Marshal Date: Tue, 23 Jun 2026 22:15:29 +0000 Subject: [PATCH] feat: reorder cargo and route steps in booking form; update related logic and UI components --- .../src/pages/bookings/EditBookingPage.tsx | 1 - .../src/pages/bookings/NewBookingPage.tsx | 18 +- .../pages/bookings/new-booking-form/schema.ts | 34 +- .../new-booking-form/step5-cargo-details.tsx | 397 +++++++----------- .../new-booking-form/step8-review.tsx | 17 +- 5 files changed, 170 insertions(+), 297 deletions(-) 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 da39659d2..07a778d6d 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/EditBookingPage.tsx @@ -821,7 +821,6 @@ export default function EditBookingPage() { diff --git a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx index 5a4e67d0d..17d3eef81 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -357,16 +357,14 @@ export default function NewBookingPage() { // Bulk amount lives in cargoTotalWeightVgm — tons (estimated) or a whole // item count, depending on the commodity's unit_of_measure. Item counts are - // rounded since fractional items are meaningless. Container totals are the - // summed VGM of all container lines. + // rounded since fractional items are meaningless. Containers carry NO weight + // at the wizard — VGM is captured later in operations — so container bookings + // send 0. const isPerItem = bulkChild?.unit_of_measure === Freight.CargoUnitOfMeasure.PerItem; const totalWeight = data.cargoType === "container" - ? data.containers.reduce( - (acc, c) => acc + Number(c.vgm || 0) * Number(c.qty || 0), - 0, - ) + ? 0 : isPerItem ? Math.round(Number(data.cargoWeight || 0)) : Number(data.cargoWeight || 0); @@ -420,7 +418,8 @@ export default function NewBookingPage() { ? data.containers.map((c) => ({ containerTypeId: findContainerTypeId(c.containerType), quantity: Number(c.qty || 1), - vgmPerUnitTons: Number(c.vgm || 0), + // Weight (VGM) is not collected at the wizard — captured in operations. + vgmPerUnitTons: 0, })) : [], ...(data.previousContractRef @@ -599,16 +598,15 @@ export default function NewBookingPage() { )} {step === 3 && ( - )} {step === 4 && ( - 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 11291b3aa..c3085fb57 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 @@ -6,8 +6,8 @@ export const STEPS = [ { id: 0, label: "Operation Type", short: "Operation" }, { id: 1, label: "Contract Type", short: "Contract" }, { id: 2, label: "Service Type & Mile", short: "Service" }, - { id: 3, label: "Route", short: "Route" }, - { id: 4, label: "Cargo Details", short: "Cargo" }, + { id: 3, label: "Cargo Details", short: "Cargo" }, + { id: 4, label: "Route", short: "Route" }, { id: 5, label: "Shipment Date", short: "Schedule" }, { id: 6, label: "Documents", short: "Documents" }, { id: 7, label: "Review & Submit", short: "Submit" }, @@ -158,11 +158,9 @@ export const bookingFormSchema = z .refine((q) => q.length !== 0, "Quantity is required.") .refine((q) => !isNaN(+q), "Enter a valid Number") .refine((qty) => Number(qty) >= 1, "Must be greater than 0"), - vgm: z - .string() - .refine((vgm) => vgm.length !== 0, "VGM is required.") - .refine((vgm) => !isNaN(+vgm), "Enter a valid Number") - .refine((vgm) => Number(vgm) >= 0, "Must be greater than 0"), + // Weight (VGM) is NOT collected at the wizard — it is captured later in + // operations. Kept optional so existing payload code stays valid. + vgm: z.string().default("0"), }), ), // Consolidation is system-managed, not a customer choice: the backend @@ -202,12 +200,10 @@ export const bookingFormSchema = z .refine( (data) => { if (data.cargoType !== "bulk") return true; - const cargoWeight = Number(data.cargoWeight); - return ( - !!data.cargoWeight && !Number.isNaN(cargoWeight) && cargoWeight > 0 - ); + const quantity = Number(data.cargoWeight); + return !!data.cargoWeight && !Number.isNaN(quantity) && quantity > 0; }, - { message: "Enter a cargo weight greater than 0.", path: ["cargoWeight"] }, + { message: "Enter a quantity greater than 0.", path: ["cargoWeight"] }, ) .refine( (data) => !(data.cargoType === "container" && data.containers.length === 0), @@ -240,14 +236,6 @@ export const bookingFormSchema = z message: "Enter at least 1 container.", }); } - - if (!c.vgm || +c.vgm <= 0) { - ctx.addIssue({ - code: "custom", - path: ["containers", i, "vgm"], - message: "Enter VGM greater than 0.", - }); - } }); } }); @@ -281,7 +269,7 @@ export const initialBookingFormValues: DeepPartial = { cargoFreeText: "", isHazardous: false, isRefrigerated: false, - containers: [{ type: "20ft", containerType: "", qty: "1", vgm: "" }], + containers: [{ type: "20ft", containerType: "", qty: "1", vgm: "0" }], documents: {}, notes: "", }; @@ -297,7 +285,8 @@ export const stepFields: Record>> = { "equipmentReturn", "customsClearingEnabled", ], - 3: [ + 3: ["cargoType", "cargoWeight", "cargoTypePath", "containers"], + 4: [ "originYard", "destinationYard", "extraRoutes", @@ -305,7 +294,6 @@ export const stepFields: Record>> = { "isRefrigerated", "shippingLine", ], - 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/step5-cargo-details.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step5-cargo-details.tsx index b31a83174..9765d1950 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step5-cargo-details.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step5-cargo-details.tsx @@ -1,17 +1,11 @@ import { useEffect, useMemo } from "react"; import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form"; import { Package, Plus, Trash2, Weight } from "lucide-react"; -import { - ActionIcon, - Button, - Skeleton, - Text, - TextInput, -} from "@mantine/core"; +import { ActionIcon, Button, Skeleton, Text, TextInput } from "@mantine/core"; import type { Freight } from "@edr/types"; import { BookingFormInputValues, - calcWagons, + calcWagons, type BookingFormValues, } from "./schema"; import { @@ -33,12 +27,10 @@ type BookingForm = UseFormReturn< export function Step5CargoDetails({ form, - direction, referenceData, isLoading, }: { form: BookingForm; - direction: Freight.ScheduleTradeDirection; referenceData?: Freight.BookingReferenceData; isLoading?: boolean; }) { @@ -66,21 +58,6 @@ export function Step5CargoDetails({ } }, [parentId]); - // For containerised cargo, the total weight is derived from the containers - // (Σ qty × vgm) rather than typed by hand — keep cargoWeight in sync. - useEffect(() => { - if (cargoType !== "container") return; - const total = (containers ?? []).reduce( - (sum, c) => sum + (Number(c?.qty) || 0) * (Number(c?.vgm) || 0), - 0, - ); - form.setValue("cargoWeight", total ? String(total) : "", { - shouldValidate: true, - }); - // form is stable; re-run when the containers or cargo type change. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [containers, cargoType]); - const selectedCommodity = useMemo(() => { if (!referenceData?.cargo_type || !parentId || !childId) return null; const group = referenceData.cargo_type.find((g) => g.id === parentId); @@ -88,8 +65,8 @@ export function Step5CargoDetails({ }, [referenceData, parentId, childId]); // Unit of measure for bulk/break-bulk cargo: PER_ITEM → ask for a total item - // count; otherwise ask for estimated tons. Drives the amount field's label, - // icon, and step so customers enter the right unit. + // count; otherwise ask for an estimated tonnage. Drives the quantity field's + // label, icon, and step so customers enter the right unit. const isPerItem = selectedCommodity?.unit_of_measure === "PER_ITEM"; const freightTypeGroups = useMemo(() => { @@ -119,29 +96,13 @@ export function Step5CargoDetails({ ); }, [referenceData, parentId]); - function getOverweightAlert( - type: "20ft" | "40ft", - vgm: number, - ): string | null { - if (type === "20ft" && vgm > 0) { - const limit = direction === "EXPORT" ? 25 : 20; - if (vgm > limit) { - return `VGM ${vgm}t exceeds the ${limit}t ${direction ?? "standard"} limit for a 20ft container. An Overweight Surcharge will apply.`; - } - } - if (type === "40ft" && vgm > 32.5) { - return `VGM ${vgm}t exceeds the 32.5t global limit for a 40ft container. An Overweight Surcharge will apply.`; - } - return null; - } - if (isLoading) { return ( } title="Cargo Details" - description="Define your cargo type, weight, and container configuration." + description="Choose your cargo type and configuration." />
@@ -161,7 +122,7 @@ export function Step5CargoDetails({ } title="Cargo Details" - description="Define your cargo type, weight, and container configuration." + description="Choose your cargo type and configuration. Container weight is captured later in operations." /> {/* Cargo Type */} @@ -204,36 +165,8 @@ export function Step5CargoDetails({ />
- {/* Containerised cargo: total weight is auto-summed from the containers - below, so we show it here up-front as a read-only running total. */} - {cargoType === "container" && ( -
- ( - } - error={fieldState.error?.message} - readOnly - description="Auto-calculated from the containers below." - radius={10} - styles={fieldStyles} - min={0} - step={0.01} - /> - )} - /> -
- )} - {/* Bulk freight type — pick the commodity FIRST so we know whether the - cargo is measured in tons or items before asking for the amount. */} + cargo is measured in tons or items before asking for the quantity. */} {cargoType === "bulk" && (
{freightTypeOptions.length > 0 ? ( @@ -288,9 +221,9 @@ export function Step5CargoDetails({ /> )} - {/* Amount — only once a commodity is chosen, so the unit (tons vs - items) is known. PER_TON asks for estimated tons to ship; - PER_ITEM asks for the total item count to import/export. */} + {/* Quantity — only once a commodity is chosen, so the unit (tons vs + items) is known. PER_TON asks for estimated tons; PER_ITEM asks + for the total item count. */} {selectedCommodity && ( )} - {/* Container list */} + {/* Container list — type + quantity only; no weight is collected here. */} {cargoType === "container" && ( <>
@@ -343,190 +272,148 @@ export function Step5CargoDetails({ radius="md" leftSection={} onClick={() => - append({ type: "20ft", containerType: "", qty: "1", vgm: "" }) + append({ + type: "20ft", + containerType: "", + qty: "1", + vgm: "0", + }) } > Add Container
- {fields.map((field, index) => { - const containerType = containers[index]?.type; - const vgm = containers[index]?.vgm ?? 0; - const alert = getOverweightAlert(containerType, +vgm); - - return ( -
-
- ( +
+
+ + Container {index + 1} + + {fields.length > 1 && ( + remove(index)} + aria-label="Remove container" > - Container {index + 1} - - {fields.length > 1 && ( - remove(index)} - aria-label="Remove container" - > - - - )} -
+ + + )} +
- {/* Container size */} + {/* Container size */} + ( +
+
+ {[ + { val: "20ft" as const, label: "20ft Container (TEU)" }, + { val: "40ft" as const, label: "40ft Container (FEU)" }, + ].map((ct) => ( + typeField.onChange(ct.val)} + > +
+ +

{ct.label}

+
+
+ ))} +
+ +
+ )} + /> + + {/* Quantity + Container Type */} +
( + render={({ field: qtyField, fieldState }) => (
-
- {[ - { - val: "20ft" as const, - label: "20ft Container (TEU)", - limit: - direction === "EXPORT" - ? "Max 25t per container" - : "Max 20t per container", - }, - { - val: "40ft" as const, - label: "40ft Container (FEU)", - limit: "Max 32.5t per container", - }, - ].map((ct) => ( - typeField.onChange(ct.val)} - > -
- -

{ct.label}

-
-

- {ct.limit} -

-
- ))} + + Quantity * + +
+ + qtyField.onChange(e.target.value)} + onBlur={qtyField.onBlur} + type="number" + min={1} + className="h-9 w-full rounded-lg border border-gray-200 bg-white px-3 text-center text-sm outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500" + /> +
- + {fieldState.error?.message && ( + + {fieldState.error.message} + + )}
)} /> - {/* Qty + VGM + Type */} -
- ( -
- - Quantity * - -
- - - qtyField.onChange(e.target.value) - } - onBlur={qtyField.onBlur} - type="number" - min={1} - className="h-9 w-full rounded-lg border border-gray-200 bg-white px-3 text-center text-sm outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500" - /> - -
- {fieldState.error?.message && ( - - {fieldState.error.message} - - )} -
- )} - /> - - ( - vgmField.onChange(e.target.value)} - onBlur={vgmField.onBlur} - type="number" - label="Tons *" - placeholder="e.g. 18.5" - error={fieldState.error?.message} - radius="md" - min={0} - step={0.1} - /> - )} - /> - - ( - - )} - /> -
- - {alert && ( - - Overweight Alert: {alert} - - )} + ( + + )} + />
- ); - })} +
+ ))}
{(() => { @@ -536,10 +423,10 @@ export function Step5CargoDetails({

Unpaired 20ft Container

- One 20ft container occupies only half a wagon. The wagon - will depart once a co-loader is found to fill the remaining - slot, which may delay departure beyond the - standard lead time. + One 20ft container occupies only half a wagon. The wagon will + depart once a co-loader is found to fill the remaining slot, + which may delay departure beyond the standard + lead time.

); 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 5acbc0213..d7a57d7d4 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 @@ -33,8 +33,8 @@ import { StepHeader } from "./shared"; export const REVIEW_STEP_TARGETS = { contract: 1, service: 2, - route: 3, - cargo: 4, + cargo: 3, + route: 4, schedule: 5, documents: 6, } as const; @@ -351,17 +351,19 @@ export function Step8Review({ onEdit={() => setStep(REVIEW_STEP_TARGETS.cargo)} > - + {/* Containers carry no weight at the wizard — only bulk shows a quantity row. */} + {values.cargoType === "bulk" && ( + + )} {values.cargoType === "container" && values.containers.length > 0 && ( Type Qty - VGM (t) @@ -371,7 +373,6 @@ export function Step8Review({ {c.containerType || c.type} {c.qty} - {c.vgm} ))}