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"}
+
+ {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. */}
-
- }
- onClick={handleReview}
- disabled={hasOdd20ft}
- >
- Review price & book
-
-
-
-
+
+ }
+ onClick={handleReview}
+ disabled={hasOdd20ft}
+ >
+ Review price & book
+
+
+
+
+
@@ -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)}
+ >
+
+
+
+ ),
+ )}
);