mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 13:10:56 +00:00
feat: reorder cargo and route steps in booking form; update related logic and UI components
This commit is contained in:
@@ -821,7 +821,6 @@ export default function EditBookingPage() {
|
||||
<Box>
|
||||
<Step5CargoDetails
|
||||
form={form}
|
||||
direction={direction!}
|
||||
referenceData={referenceData}
|
||||
isLoading={!referenceData}
|
||||
/>
|
||||
|
||||
@@ -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() {
|
||||
<Step2ServiceType referenceData={referenceData} form={form} />
|
||||
)}
|
||||
{step === 3 && (
|
||||
<Step4Route
|
||||
<Step5CargoDetails
|
||||
form={form}
|
||||
referenceData={referenceData}
|
||||
isLoading={refDataLoading}
|
||||
/>
|
||||
)}
|
||||
{step === 4 && (
|
||||
<Step5CargoDetails
|
||||
<Step4Route
|
||||
form={form}
|
||||
direction={direction!}
|
||||
referenceData={referenceData}
|
||||
isLoading={refDataLoading}
|
||||
/>
|
||||
|
||||
@@ -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<BookingFormValues> = {
|
||||
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<number, Array<Path<BookingFormValues>>> = {
|
||||
"equipmentReturn",
|
||||
"customsClearingEnabled",
|
||||
],
|
||||
3: [
|
||||
3: ["cargoType", "cargoWeight", "cargoTypePath", "containers"],
|
||||
4: [
|
||||
"originYard",
|
||||
"destinationYard",
|
||||
"extraRoutes",
|
||||
@@ -305,7 +294,6 @@ export const stepFields: Record<number, Array<Path<BookingFormValues>>> = {
|
||||
"isRefrigerated",
|
||||
"shippingLine",
|
||||
],
|
||||
4: ["cargoType", "cargoWeight", "cargoTypePath", "containers"],
|
||||
5: ["scheduledDate"],
|
||||
6: ["documents"],
|
||||
7: ["notes"],
|
||||
|
||||
@@ -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 (
|
||||
<StepCard>
|
||||
<StepHeader
|
||||
icon={<Package size={22} />}
|
||||
title="Cargo Details"
|
||||
description="Define your cargo type, weight, and container configuration."
|
||||
description="Choose your cargo type and configuration."
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<Skeleton height={14} w={96} radius="sm" />
|
||||
@@ -161,7 +122,7 @@ export function Step5CargoDetails({
|
||||
<StepHeader
|
||||
icon={<Package size={22} />}
|
||||
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({
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 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" && (
|
||||
<div className="space-y-3">
|
||||
<Controller
|
||||
name="cargoWeight"
|
||||
control={form.control}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextInput
|
||||
{...field}
|
||||
id="cargoWeight"
|
||||
type="number"
|
||||
label="Total Cargo Weight (Tons) *"
|
||||
placeholder="0.00"
|
||||
leftSection={<Weight className="h-4 w-4" />}
|
||||
error={fieldState.error?.message}
|
||||
readOnly
|
||||
description="Auto-calculated from the containers below."
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
min={0}
|
||||
step={0.01}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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" && (
|
||||
<div className="space-y-3">
|
||||
{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 && (
|
||||
<Controller
|
||||
name="cargoWeight"
|
||||
@@ -300,11 +233,7 @@ export function Step5CargoDetails({
|
||||
{...field}
|
||||
id="cargoWeight"
|
||||
type="number"
|
||||
label={
|
||||
isPerItem
|
||||
? "Total Number of Items *"
|
||||
: "Estimated Total Tons *"
|
||||
}
|
||||
label={isPerItem ? "Quantity (Items) *" : "Quantity (Tons) *"}
|
||||
placeholder={isPerItem ? "e.g. 500" : "e.g. 1200"}
|
||||
leftSection={
|
||||
isPerItem ? (
|
||||
@@ -317,7 +246,7 @@ export function Step5CargoDetails({
|
||||
description={
|
||||
isPerItem
|
||||
? "Total count of items you plan to import or export."
|
||||
: "Your best estimate of the total weight to ship, in tons."
|
||||
: "Estimated total tonnage to ship."
|
||||
}
|
||||
radius={10}
|
||||
styles={fieldStyles}
|
||||
@@ -330,7 +259,7 @@ export function Step5CargoDetails({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Container list */}
|
||||
{/* Container list — type + quantity only; no weight is collected here. */}
|
||||
{cargoType === "container" && (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
@@ -343,190 +272,148 @@ export function Step5CargoDetails({
|
||||
radius="md"
|
||||
leftSection={<Plus size={14} />}
|
||||
onClick={() =>
|
||||
append({ type: "20ft", containerType: "", qty: "1", vgm: "" })
|
||||
append({
|
||||
type: "20ft",
|
||||
containerType: "",
|
||||
qty: "1",
|
||||
vgm: "0",
|
||||
})
|
||||
}
|
||||
>
|
||||
Add Container
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{fields.map((field, index) => {
|
||||
const containerType = containers[index]?.type;
|
||||
const vgm = containers[index]?.vgm ?? 0;
|
||||
const alert = getOverweightAlert(containerType, +vgm);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={field.id}
|
||||
className="flex flex-col gap-3 rounded-xl border border-gray-200 bg-gray-50/50 p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text
|
||||
size="xs"
|
||||
fw={600}
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
className="tracking-wide"
|
||||
{fields.map((field, index) => (
|
||||
<div
|
||||
key={field.id}
|
||||
className="flex flex-col gap-3 rounded-xl border border-gray-200 bg-gray-50/50 p-4"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<Text
|
||||
size="xs"
|
||||
fw={600}
|
||||
c="dimmed"
|
||||
tt="uppercase"
|
||||
className="tracking-wide"
|
||||
>
|
||||
Container {index + 1}
|
||||
</Text>
|
||||
{fields.length > 1 && (
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={() => remove(index)}
|
||||
aria-label="Remove container"
|
||||
>
|
||||
Container {index + 1}
|
||||
</Text>
|
||||
{fields.length > 1 && (
|
||||
<ActionIcon
|
||||
color="red"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
onClick={() => remove(index)}
|
||||
aria-label="Remove container"
|
||||
>
|
||||
<Trash2 size={15} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
<Trash2 size={15} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Container size */}
|
||||
{/* Container size */}
|
||||
<Controller
|
||||
name={`containers.${index}.type`}
|
||||
control={form.control}
|
||||
render={({ field: typeField, fieldState }) => (
|
||||
<div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{[
|
||||
{ val: "20ft" as const, label: "20ft Container (TEU)" },
|
||||
{ val: "40ft" as const, label: "40ft Container (FEU)" },
|
||||
].map((ct) => (
|
||||
<OptionCard
|
||||
key={ct.val}
|
||||
selected={typeField.value === ct.val}
|
||||
onClick={() => typeField.onChange(ct.val)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<Package className="h-4 w-4 text-emerald-600" />
|
||||
<p className="font-semibold">{ct.label}</p>
|
||||
</div>
|
||||
</OptionCard>
|
||||
))}
|
||||
</div>
|
||||
<OptionFieldError error={fieldState.error} />
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Quantity + Container Type */}
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Controller
|
||||
name={`containers.${index}.type`}
|
||||
name={`containers.${index}.qty`}
|
||||
control={form.control}
|
||||
render={({ field: typeField, fieldState }) => (
|
||||
render={({ field: qtyField, fieldState }) => (
|
||||
<div>
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{[
|
||||
{
|
||||
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) => (
|
||||
<OptionCard
|
||||
key={ct.val}
|
||||
selected={typeField.value === ct.val}
|
||||
onClick={() => typeField.onChange(ct.val)}
|
||||
>
|
||||
<div className="mb-1 flex items-center gap-2">
|
||||
<Package className="h-4 w-4 text-emerald-600" />
|
||||
<p className="font-semibold">{ct.label}</p>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">
|
||||
{ct.limit}
|
||||
</p>
|
||||
</OptionCard>
|
||||
))}
|
||||
<Text size="sm" fw={500} mb={4}>
|
||||
Quantity *
|
||||
</Text>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
qtyField.onChange(
|
||||
Math.max(
|
||||
1,
|
||||
Number(qtyField.value ?? 1) - 1,
|
||||
).toString(),
|
||||
)
|
||||
}
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-white text-sm font-medium transition hover:bg-gray-50"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
value={qtyField.value ?? 1}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
qtyField.onChange(
|
||||
(Number(qtyField.value ?? 1) + 1).toString(),
|
||||
)
|
||||
}
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-white text-sm font-medium transition hover:bg-gray-50"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<OptionFieldError error={fieldState.error} />
|
||||
{fieldState.error?.message && (
|
||||
<Text size="xs" c="red" mt={4}>
|
||||
{fieldState.error.message}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Qty + VGM + Type */}
|
||||
<div className="grid gap-3 sm:grid-cols-3">
|
||||
<Controller
|
||||
name={`containers.${index}.qty`}
|
||||
control={form.control}
|
||||
render={({ field: qtyField, fieldState }) => (
|
||||
<div>
|
||||
<Text size="sm" fw={500} mb={4}>
|
||||
Quantity *
|
||||
</Text>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
qtyField.onChange(
|
||||
Math.max(
|
||||
1,
|
||||
Number(qtyField.value ?? 1) - 1,
|
||||
).toString(),
|
||||
)
|
||||
}
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-white text-sm font-medium transition hover:bg-gray-50"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<input
|
||||
value={qtyField.value ?? 1}
|
||||
onChange={(e) =>
|
||||
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"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
qtyField.onChange(
|
||||
(Number(qtyField.value ?? 1) + 1).toString(),
|
||||
)
|
||||
}
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-gray-200 bg-white text-sm font-medium transition hover:bg-gray-50"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
{fieldState.error?.message && (
|
||||
<Text size="xs" c="red" mt={4}>
|
||||
{fieldState.error.message}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name={`containers.${index}.vgm`}
|
||||
control={form.control}
|
||||
render={({ field: vgmField, fieldState }) => (
|
||||
<TextInput
|
||||
value={vgmField.value ?? 0}
|
||||
onChange={(e) => 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}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name={`containers.${index}.containerType`}
|
||||
control={form.control}
|
||||
render={({ field: ctField, fieldState }) => (
|
||||
<SelectField
|
||||
field={ctField}
|
||||
error={fieldState.error}
|
||||
label="Container Type *"
|
||||
placeholder="Select type..."
|
||||
data={
|
||||
containerTypeOptionsBySize.get(
|
||||
containers[index]?.type ?? "20ft",
|
||||
) ?? []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{alert && (
|
||||
<AlertBox tone="warning">
|
||||
<strong>Overweight Alert:</strong> {alert}
|
||||
</AlertBox>
|
||||
)}
|
||||
<Controller
|
||||
name={`containers.${index}.containerType`}
|
||||
control={form.control}
|
||||
render={({ field: ctField, fieldState }) => (
|
||||
<SelectField
|
||||
field={ctField}
|
||||
error={fieldState.error}
|
||||
label="Container Type *"
|
||||
placeholder="Select type..."
|
||||
data={
|
||||
containerTypeOptionsBySize.get(
|
||||
containers[index]?.type ?? "20ft",
|
||||
) ?? []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{(() => {
|
||||
@@ -536,10 +423,10 @@ export function Step5CargoDetails({
|
||||
<AlertBox tone="warning">
|
||||
<p className="font-semibold">Unpaired 20ft Container</p>
|
||||
<p className="mt-1 text-xs">
|
||||
One 20ft container occupies only half a wagon. The wagon
|
||||
will depart once a co-loader is found to fill the remaining
|
||||
slot, which <strong>may delay departure</strong> 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 <strong>may delay departure</strong> beyond the standard
|
||||
lead time.
|
||||
</p>
|
||||
</AlertBox>
|
||||
);
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
<DetailRow label="Freight type" value={cargoValue} />
|
||||
<DetailRow
|
||||
label={totalQuantityRow.label}
|
||||
value={totalQuantityRow.value}
|
||||
/>
|
||||
{/* Containers carry no weight at the wizard — only bulk shows a quantity row. */}
|
||||
{values.cargoType === "bulk" && (
|
||||
<DetailRow
|
||||
label={totalQuantityRow.label}
|
||||
value={totalQuantityRow.value}
|
||||
/>
|
||||
)}
|
||||
{values.cargoType === "container" && values.containers.length > 0 && (
|
||||
<Table mt="sm" withTableBorder withColumnBorders fz="sm">
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<Table.Th>Type</Table.Th>
|
||||
<Table.Th>Qty</Table.Th>
|
||||
<Table.Th>VGM (t)</Table.Th>
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
@@ -371,7 +373,6 @@ export function Step8Review({
|
||||
<Table.Tr key={i}>
|
||||
<Table.Td>{c.containerType || c.type}</Table.Td>
|
||||
<Table.Td>{c.qty}</Table.Td>
|
||||
<Table.Td>{c.vgm}</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
|
||||
Reference in New Issue
Block a user