diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx index 27e1597c5..4a1cb402f 100644 --- a/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx +++ b/apps/edr-freight-web/backoffice/src/components/contracts/GlCreateBookingForm.tsx @@ -172,11 +172,11 @@ function emptyUnit(): UnitDraft { function emptyLine(size: string): ContainerLineDraft { return { containerSize: size, - quantity: "1", + quantity: "0", hazardousQuantity: "0", reeferQuantity: "0", returnQuantity: "0", - units: [emptyUnit()], + units: [], }; } diff --git a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx index e0c17a57b..e89977eef 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trainScheduling/TrainScheduleV2DetailPage.tsx @@ -742,16 +742,7 @@ export default function TrainScheduleV2DetailPage() { {canEditBookings && (previewResult || displayWagonPlan.length) ? ( - {!hasContainerStep ? ( - - ) : ( + {hasContainerStep ? ( - )} + ) : null} diff --git a/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx b/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx index 50f2828f6..6b2ce4470 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/NewShipmentPage.tsx @@ -266,8 +266,7 @@ function NewShipmentBookingForm({ withReturn: contract.equipmentReturn === "WITH_RETURN", // The contract quotes USD; the customer bills this shipment in the // currency they pick here. Intercity is always ETB. - paymentCurrency: - contract.tradeDirection === "DOMESTIC" ? "ETB" : "USD", + paymentCurrency: contract.tradeDirection === "DOMESTIC" ? "ETB" : "USD", }, resolver: zodResolver( createShipmentFormSchema({ @@ -306,7 +305,10 @@ function NewShipmentBookingForm({ bookingId: completeBookingId, dto, }) - : api.contracts.createBookingUnderContract.call({ id: contractId, dto }), + : api.contracts.createBookingUnderContract.call({ + id: contractId, + dto, + }), onSuccess: (booking) => { queryClient.invalidateQueries({ queryKey: api.bookings.list.queryKey() }); queryClient.invalidateQueries({ @@ -366,7 +368,8 @@ function NewShipmentBookingForm({ .map((l) => ({ containerSize: l.containerSize, quantity: Number(l.quantity), - hazardousQuantity: Number(l.hazardousQuantity || 0) || undefined, + hazardousQuantity: + Number(l.hazardousQuantity || 0) || undefined, reeferQuantity: Number(l.reeferQuantity || 0) || undefined, ...(withReturnService ? { returnQuantity: Number(l.returnQuantity || 0) } @@ -379,7 +382,9 @@ function NewShipmentBookingForm({ // line counts and bills each surcharge on the ticked containers. isHazardous: Boolean(u.isHazardous), isReefer: Boolean(u.isReefer), - ...(withReturnService ? { isReturn: Boolean(u.isReturn) } : {}), + ...(withReturnService + ? { isReturn: Boolean(u.isReturn) } + : {}), })), })), } @@ -417,6 +422,12 @@ function NewShipmentBookingForm({ validateMutation.mutate(buildDto(values)); }); + // The per-field messages render inline, but on a long single-page form the + // failing field is often scrolled out of view — mirror the backoffice's + // summary alert next to the submit button so the click never looks inert. + const showValidationSummary = + form.formState.isSubmitted && !form.formState.isValid; + const handleConfirm = () => { if (!pendingValues) return; // Guard: never let a booking with unresolved 20ft pairing errors submit. @@ -457,8 +468,15 @@ function NewShipmentBookingForm({ mb="lg" > - - {completeBookingId ? "Complete Your Booking" : "New Shipment Booking"} + <Title + order={1} + fw={800} + fz={26} + style={{ letterSpacing: "-0.01em" }} + > + {completeBookingId + ? "Complete Your Booking" + : "New Shipment Booking"} {completeBookingId @@ -534,28 +552,41 @@ function NewShipmentBookingForm({ marginTop: "auto", }} > - - - {/* Mantine tooltips get no pointer events from a disabled button, + + {showValidationSummary ? ( + } + mb="sm" + > + Fix the highlighted fields before reviewing the price. + + ) : null} + + + {/* Mantine tooltips get no pointer events from a disabled button, so the wrapper carries the hover target. */} - - - - - + + + + + + @@ -621,8 +652,7 @@ function PriceConfirmModal({ quantity: li.quantity, amount: li.amount, })), - total: - validation?.totalAmount ?? items.reduce((s, l) => s + l.amount, 0), + total: validation?.totalAmount ?? items.reduce((s, l) => s + l.amount, 0), }; }, [validation, baseTotal]); @@ -715,8 +745,8 @@ function PriceConfirmModal({ ))} - Adjust the 20ft container weights or quantities so pairs differ - by no more than 10 tons. + Adjust the 20ft container weights or quantities so pairs + differ by no more than 10 tons. @@ -810,7 +840,12 @@ function PriceConfirmModal({ )} - + {total.lines.map((line, i) => ( @@ -1007,7 +1042,9 @@ function ScheduleStep({ const isIntercity = contract.tradeDirection === "DOMESTIC"; const { data: availableDays, isLoading } = useQuery({ ...api.bookings.getAvailableDaysForCargo.queryOptions({ - input: cargoQuery ?? ({ freightType: "BULK" } as Freight.AvailableDaysForCargoQuery), + input: + cargoQuery ?? + ({ freightType: "BULK" } as Freight.AvailableDaysForCargoQuery), }), enabled: cargoQuery !== null && !isIntercity, }); @@ -1064,7 +1101,12 @@ function ScheduleStep({ title="Schedule" description="Intercity shipments have no fixed day." /> - }> + } + > Your shipment rides the next import/export train passing through your corridor. Operations assign it to a train with free capacity — you will be notified when it is accepted and payment is due. @@ -1110,7 +1152,12 @@ function ScheduleStep({ )} /> {cargoQuery === null ? ( - }> + } + > Enter your cargo details first — available shipment days depend on the wagons your cargo needs. @@ -1245,11 +1292,11 @@ function CargoStep({ "containers", sizes.map((size) => ({ containerSize: size, - quantity: "1", + quantity: "0", hazardousQuantity: "0", reeferQuantity: "0", returnQuantity: "0", - units: [emptyUnit()], + units: [], })), { shouldValidate: false }, ); @@ -1289,11 +1336,11 @@ function CargoStep({ return ( current.find((l) => l.containerSize === size) ?? { containerSize: size, - quantity: "1", + quantity: "0", hazardousQuantity: "0", reeferQuantity: "0", returnQuantity: "0", - units: [emptyUnit()], + units: [], } ); } @@ -1315,7 +1362,10 @@ function CargoStep({ })), }; }); - form.setValue("containers", next, { shouldValidate: true, shouldDirty: true }); + form.setValue("containers", next, { + shouldValidate: true, + shouldDirty: true, + }); setImportErrors([]); setImportSummary(`Imported ${rows.length} container(s) from ${file.name}.`); }; @@ -1330,7 +1380,12 @@ function CargoStep({ /> {sizes.length > 0 && ( - + @@ -1458,10 +1513,11 @@ function CargoStep({ title={`Odd number of 20ft containers (${ft20})`} > - 20ft containers travel two per wagon, so they must be booked in - even numbers. Please add one more 20ft container or remove one - (e.g. book {ft20 + 1} or {ft20 - 1} instead of {ft20}) — the - booking cannot be submitted with an unpaired 20ft container. + 20ft containers travel two per wagon, so they must be booked + in even numbers. Please add one more 20ft container or remove + one (e.g. book {ft20 + 1} or {ft20 - 1} instead of {ft20}) — + the booking cannot be submitted with an unpaired 20ft + container. ); @@ -1660,16 +1716,6 @@ function NotesSection({ form }: { form: ShipmentForm }) { ); } -/** A blank container row — handling switches start off. */ -const emptyUnit = () => ({ - containerNumber: "", - sealNumber: "", - vgmTons: "", - isHazardous: false, - isReefer: false, - isReturn: false, -}); - function ContainerLineEditor({ form, index, @@ -1726,7 +1772,11 @@ function ContainerLineEditor({ * price estimate and the submitted payload stay in step with the switches. */ const syncHandlingCounts = ( - units: Array<{ isHazardous?: boolean; isReefer?: boolean; isReturn?: boolean }>, + units: Array<{ + isHazardous?: boolean; + isReefer?: boolean; + isReturn?: boolean; + }>, ) => { const set = ( key: "hazardousQuantity" | "reeferQuantity" | "returnQuantity", @@ -1856,94 +1906,100 @@ function ContainerLineEditor({ ))} )} - {Array.from({ length: Math.max(quantity, units.length) }).map((_, u) => ( - - ( - - field.onChange(e.currentTarget.value.toUpperCase()) - } - placeholder="e.g. MSCU1234567" - error={fieldState.error?.message} - radius={10} - styles={fieldStyles} - style={{ flex: 1 }} - /> - )} - /> - ( - - )} - /> - ( - - )} - /> - {handlingColumns.map((col) => ( + {Array.from({ length: Math.max(quantity, units.length) }).map( + (_, u) => ( + ( - - - toggleUnitHandling(u, col.key, e.currentTarget.checked) - } - size="sm" - /> - + render={({ field, fieldState }) => ( + + field.onChange(e.currentTarget.value.toUpperCase()) + } + placeholder="e.g. MSCU1234567" + error={fieldState.error?.message} + radius={10} + styles={fieldStyles} + style={{ flex: 1 }} + /> )} /> - ))} - removeUnit(u)} - > - - - - ))} + ( + + )} + /> + ( + + )} + /> + {handlingColumns.map((col) => ( + ( + + + toggleUnitHandling( + u, + col.key, + e.currentTarget.checked, + ) + } + size="sm" + /> + + )} + /> + ))} + removeUnit(u)} + > + + + + ), + )} );