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 f1921738d..04f3f6621 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 @@ -175,8 +175,14 @@ export const bookingFormSchema = z containers: z.array( z.object({ type: z.enum(["20ft", "40ft"]), - qty: z.number(), - vgm: z.number(), + qty: z + .string() + .refine((q) => !isNaN(+q), "Enter a valid Number") + .refine((qty) => Number(qty) >= 1, "Must be greater than 0"), + vgm: z + .string() + .refine((vgm) => !isNaN(+vgm), "Enter a valid Number") + .refine((vgm) => Number(vgm) >= 0, "Must be greater than 0"), }), ), consolidationEnabled: z.boolean(), @@ -338,7 +344,7 @@ export const bookingFormSchema = z } data.containers.forEach((c, i) => { - if (!c.qty || c.qty < 1) { + if (!c.qty || +c.qty < 1) { ctx.addIssue({ code: "custom", path: ["containers", i, "qty"], @@ -346,7 +352,7 @@ export const bookingFormSchema = z }); } - if (!c.vgm || c.vgm <= 0) { + if (!c.vgm || +c.vgm <= 0) { ctx.addIssue({ code: "custom", path: ["containers", i, "vgm"], @@ -400,7 +406,7 @@ export const initialBookingFormValues: BookingFormValues = { breakBulkTypeOther: "", isHazardous: false, isRefrigerated: false, - containers: [{ type: "20ft", qty: 1, vgm: 0 }], + containers: [{ type: "20ft", qty: "1", vgm: "" }], consolidationEnabled: false, documents: {}, notes: "", @@ -437,14 +443,21 @@ export type RouteDirection = "import" | "export" | "domestic" | null; export interface ContainerConfig { type: "20ft" | "40ft"; - qty: number; - vgm: number; + qty: string; + vgm: string; +} + +export interface WagonConfig { + type: "20ft" | "40ft"; } export interface WagonCalcResult { totalWagons: number; hasOddUnit: boolean; sharedWagons: number; + wagonLayout: WagonConfig[]; + ft40Wagons: number; + ft20Wagons: number; } export function genContractId(): string { @@ -467,22 +480,22 @@ export function getRouteDirection( } export function calcWagons(containers: ContainerConfig[]): WagonCalcResult { - let totalWagons = 0; - let hasOddUnit = false; - let sharedWagons = 0; + const Ft40Wagons = containers + .filter((c) => c.type === "40ft") + .reduce((sum, c) => sum + Number(c.qty), 0); + const Ft20Wagons = containers + .filter((c) => c.type === "20ft") + .reduce((sum, c) => sum + Number(c.qty), 0); + const wagonLayout: WagonConfig[] = []; + let hasOddUnit = Ft20Wagons % 2 === 1; + let sharedWagons = Math.floor(Ft20Wagons / 2); - for (const c of containers) { - if (c.type === "40ft") { - totalWagons += c.qty; - sharedWagons += c.qty; - } else { - const pairs = Math.floor(c.qty / 2); - const odd = c.qty % 2; - totalWagons += pairs + odd; - sharedWagons += pairs; - if (odd > 0) hasOddUnit = true; - } - } - - return { totalWagons, hasOddUnit, sharedWagons }; + return { + totalWagons: sharedWagons + Ft40Wagons, + hasOddUnit, + sharedWagons, + ft40Wagons: Ft40Wagons, + ft20Wagons: Ft20Wagons, + wagonLayout, + }; } 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 7fc5b7b02..d0c8b7b9a 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 @@ -1,11 +1,7 @@ import { Controller, type UseFormReturn } from "react-hook-form"; import { Flame, MapPin, Snowflake } from "lucide-react"; import { Field, Separator, Switch } from "@edr/ui-common"; -import { - type BookingFormValues, - getRouteDirection, - STATIONS, -} from "./schema"; +import { type BookingFormValues, getRouteDirection, STATIONS } from "./schema"; import { SelectField, SelectOptions, StepHeader, StepLabel } from "./shared"; type BookingForm = UseFormReturn; @@ -33,7 +29,6 @@ export function Step4Route({ form }: { form: BookingForm }) { />
- Route
{ field.onChange("bulk"); - form.setValue("containers", [{ type: "20ft", qty: 1, vgm: 0 }], { shouldDirty: true }); + form.setValue( + "containers", + [{ type: "20ft", qty: "1", vgm: "0" }], + { shouldDirty: true }, + ); }} >
@@ -254,7 +265,7 @@ export function Step5CargoDetails({ type="button" variant="outline" size="sm" - onClick={() => append({ type: "20ft", qty: 1, vgm: 0 })} + onClick={() => append({ type: "20ft", qty: "1", vgm: "0" })} > Add Container @@ -275,7 +286,7 @@ export function Step5CargoDetails({ {fields.map((field, index) => { const containerType = containers[index]?.type; const vgm = containers[index]?.vgm ?? 0; - const alert = getOverweightAlert(containerType, vgm); + const alert = getOverweightAlert(containerType, +vgm); return (
qtyField.onChange( - Math.max(1, (qtyField.value ?? 1) - 1), + 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-input transition hover:bg-accent" @@ -361,9 +375,7 @@ export function Step5CargoDetails({ - qtyField.onChange( - Math.max(1, parseInt(e.target.value) || 1), - ) + qtyField.onChange(e.target.value) } onBlur={qtyField.onBlur} type="number" @@ -375,7 +387,7 @@ export function Step5CargoDetails({ type="button" onClick={() => qtyField.onChange( - (qtyField.value ?? 1) + 1, + (Number(qtyField.value ?? 1) + 1).toString(), ) } className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg border border-input transition hover:bg-accent" @@ -396,11 +408,7 @@ export function Step5CargoDetails({ VGM (Tons) * - vgmField.onChange( - parseFloat(e.target.value) || 0, - ) - } + onChange={(e) => vgmField.onChange(e.target.value)} onBlur={vgmField.onBlur} type="number" aria-invalid={fieldState.invalid} diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step6-wagon-allocation.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step6-wagon-allocation.tsx index 0bae2917e..c58b34241 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step6-wagon-allocation.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step6-wagon-allocation.tsx @@ -1,11 +1,6 @@ -import { Controller, type UseFormReturn } from "react-hook-form"; -import { AlertTriangle } from "lucide-react"; -import { Separator } from "@edr/ui-common"; -import { - type BookingFormValues, - type WagonCalcResult, -} from "./schema"; -import { AlertBox, OptionCard, StepHeader, StepLabel } from "./shared"; +import { type UseFormReturn } from "react-hook-form"; +import { type BookingFormValues, type WagonCalcResult } from "./schema"; +import { AlertBox, StepHeader, StepLabel } from "./shared"; type BookingForm = UseFormReturn; @@ -17,13 +12,14 @@ export function Step6WagonAllocation({ wagons: WagonCalcResult | null; }) { const containers = form.watch("containers") ?? []; - const totalContainers = containers.reduce((sum, c) => sum + (c.qty || 0), 0); + const totalContainers = containers.reduce( + (sum, c) => sum + Number(c.qty || 0), + 0, + ); const containerSummary = containers - .filter((c) => c.qty > 0) + .filter((c) => +c.qty > 0) .map((c) => `${c.qty} × ${c.type}`) .join(", "); - const consolidationEnabled = form.watch("consolidationEnabled"); - return (
Wagon Layout
- {Array.from({ length: wagons.totalWagons }, (_, index) => { - const isOdd = - wagons.hasOddUnit && index === wagons.totalWagons - 1; - return ( -
- {isOdd ? "1 × 20ft (1/2)" : "2 × 20ft"} -
- ); - })} + {new Array(wagons.ft40Wagons).fill(0).map((_, index) => ( +
+ 1 × 40ft +
+ ))} + {new Array(wagons.sharedWagons).fill(0).map((_, index) => ( +
+ 2 × 20ft +
+ ))} + {wagons.hasOddUnit && ( +
+ 1 × 20ft +
+ )}
-

- {containers.some((c) => c.type === "40ft") - ? "1 × 40ft = 1 Rail Wagon" - : `CEILING(${totalContainers} / 2) = ${wagons.totalWagons} Rail Wagon${wagons.totalWagons > 1 ? "s" : ""} — 2 × 20ft = 1 Wagon`} -

- {wagons.hasOddUnit && ( <> - -
-
- -

- Consolidation Option (US-07) -

+ +
+
+

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. +

+
-

- You have 1 unpaired 20ft container. Opt in to share a wagon - slot with another shipper to optimise costs, or request a - dedicated wagon. -

- ( -
- field.onChange(true)} - > -

- Allow Consolidation -

-

- Share a wagon slot. Billing split with co-loader. -

-
- field.onChange(false)} - > -

Dedicated Wagon

-

- Exclusive slot. Standard single-party billing. -

-
-
- )} - /> -
+ )}