From d1347892db7063bc5d4ecfe9059712b2ed7d797d Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Sat, 13 Jun 2026 12:37:11 +0300 Subject: [PATCH] feat(bookings): Implement unified hierarchical bulk cargo type selection via `cargoTypePath` --- .../src/pages/bookings/NewBookingPage.tsx | 7 +- .../pages/bookings/new-booking-form/schema.ts | 36 +++--- .../new-booking-form/step5-cargo-details.tsx | 112 +++++++++--------- 3 files changed, 81 insertions(+), 74 deletions(-) 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 34f87db00..210d97bd8 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -121,12 +121,15 @@ export default function NewBookingPage() { return ""; }; + const cargoTypePath = data.cargoTypePath ?? []; + const childId = cargoTypePath[1]; + const bulkChild = cargoTree .flatMap((g) => g.children ?? []) - .find((c) => c.id === data.bulkCommoditytype); + .find((c) => c.id === childId); const cargoTypeId = - data.cargoType === "bulk" ? data.bulkCommoditytype : undefined; + data.cargoType === "bulk" ? childId : undefined; const cargoFreeText = bulkChild?.show_free_text_box ? data.cargoFreeText 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 54e0da747..bec35de04 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 @@ -104,8 +104,7 @@ export const bookingFormSchema = z trainScheduleId: z.string().min(1, "Select a shipment date."), cargoType: z.enum(["container", "bulk"], "Select a cargo type."), cargoWeight: z.string(), - freightType: z.string(), // parent group - bulkCommoditytype: z.string(), + cargoTypePath: z.array(z.string()).default([]), cargoFreeText: z.string(), isHazardous: z.boolean(), isRefrigerated: z.boolean(), @@ -158,19 +157,6 @@ export const bookingFormSchema = z path: ["destinationYard"], }, ) - .refine((data) => !(data.cargoType === "bulk" && !data.freightType), { - message: "Select a freight type.", - path: ["freightType"], - }) - .refine( - (data) => - !( - data.cargoType === "bulk" && - data.freightType && - !data.bulkCommoditytype - ), - { message: "Select a commodity.", path: ["bulkCommoditytype"] }, - ) .refine( (data) => { if (data.cargoType !== "bulk") return true; @@ -190,6 +176,21 @@ export const bookingFormSchema = z path: ["termsAccepted"], }) .superRefine((data, ctx) => { + if (data.cargoType === "bulk") { + if (!data.cargoTypePath[0]) { + ctx.addIssue({ + code: "custom", + path: ["cargoTypePath"], + message: "Select a freight type.", + }); + } else if (!data.cargoTypePath[1]) { + ctx.addIssue({ + code: "custom", + path: ["cargoTypePath"], + message: "Select a commodity.", + }); + } + } if (data.cargoType === "container") { data.containers.forEach((c, i) => { if (!c.qty || +c.qty < 1) { @@ -233,7 +234,7 @@ export const initialBookingFormValues: DeepPartial = { scheduledDate: "", trainScheduleId: "", cargoWeight: "", - bulkCommoditytype: "", + cargoTypePath: [], cargoFreeText: "", isHazardous: false, isRefrigerated: false, @@ -263,8 +264,7 @@ export const stepFields: Record>> = { 4: [ "cargoType", "cargoWeight", - "freightType", - "bulkCommoditytype", + "cargoTypePath", "containers", "consolidationEnabled", ], 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 574a51a01..47e2fa2ff 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,11 +1,11 @@ -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form"; -import { MapPin, Package, Plus, Trash2, Weight } from "lucide-react"; +import { Package, Plus, Trash2, Weight } from "lucide-react"; import { ActionIcon, Button, Skeleton, - Stack, + InputLabel, Text, TextInput, } from "@mantine/core"; @@ -42,8 +42,9 @@ export function Step5CargoDetails({ isLoading?: boolean; }) { const cargoType = form.watch("cargoType"); - const freightType = form.watch("freightType"); - const bulkCommoditytype = form.watch("bulkCommoditytype"); + const cargoTypePath = form.watch("cargoTypePath") ?? []; + const parentId = cargoTypePath[0]; + const childId = cargoTypePath[1]; const containers = form.watch("containers"); const { fields, append, remove } = useFieldArray({ @@ -58,15 +59,17 @@ export function Step5CargoDetails({ ); }, [referenceData]); + useEffect(() => { + if (parentId) { + form.setValue("cargoTypePath", [parentId, ""], { shouldDirty: true }); + } + }, [parentId]); + const selectedCommodity = useMemo(() => { - if (!referenceData?.cargo_type || !freightType || !bulkCommoditytype) return null; - const group = referenceData.cargo_type.find( - (g) => g.code.toLowerCase() === freightType, - ); - return group?.children?.find( - (c) => c.name === bulkCommoditytype, - ) ?? null; - }, [referenceData, freightType, bulkCommoditytype]); + if (!referenceData?.cargo_type || !parentId || !childId) return null; + const group = referenceData.cargo_type.find((g) => g.id === parentId); + return group?.children?.find((c) => c.id === childId) ?? null; + }, [referenceData, parentId, childId]); const freightTypeGroups = useMemo(() => { if (!referenceData?.cargo_type) return []; @@ -75,13 +78,25 @@ export function Step5CargoDetails({ ); }, [referenceData]); + const freightTypeOptions = useMemo( + () => + freightTypeGroups.map((g) => ({ + value: g.id, + label: g.name, + })), + [freightTypeGroups], + ); + const commodityOptions = useMemo(() => { - if (!referenceData?.cargo_type || !freightType) return []; - const group = referenceData.cargo_type.find( - (g) => g.code.toLowerCase() === freightType, + if (!referenceData?.cargo_type || !parentId) return []; + const group = referenceData.cargo_type.find((g) => g.id === parentId); + return ( + group?.children?.map((c) => ({ + value: c.id, + label: c.name, + })) ?? [] ); - return group?.children?.map((c) => c.name) ?? []; - }, [referenceData, freightType]); + }, [referenceData, parentId]); function getOverweightAlert( type: "20ft" | "40ft", @@ -128,7 +143,7 @@ export function Step5CargoDetails({ {/* Cargo Type */}
- Cargo Type * + Cargo Type * { field.onChange("container"); - form.setValue("freightType", "", { shouldDirty: true }); + form.setValue("cargoTypePath", [], { shouldDirty: true }); }} >
@@ -174,7 +189,6 @@ export function Step5CargoDetails({ {/* Weight */}
- Weight - Freight Type * - ( -
-
- {freightTypeGroups.map((group) => { - const val = group.code.toLowerCase(); - return ( - { - field.onChange(val); - form.setValue("bulkCommoditytype", "", { - shouldDirty: true, - }); - }} - > -

{group.name}

-
- ); - })} -
- -
- )} - /> - - {freightType && commodityOptions.length > 0 && ( + {freightTypeOptions.length > 0 ? ( ( + )} + /> + ) : ( + + No freight types available. + + )} + + {parentId && commodityOptions.length > 0 && ( + ( +