From a267526be50ba26ad3045bbc38daac7893943eae Mon Sep 17 00:00:00 2001 From: ghost2023 Date: Mon, 1 Jun 2026 16:37:28 +0300 Subject: [PATCH] refactor(bookings): Migrate new booking API payload to use reference data IDs --- .../src/pages/bookings/NewBookingPage.tsx | 197 +++++++++++++----- .../pages/bookings/new-booking-form/schema.ts | 1 + .../bookings/new-booking-form/shared.tsx | 4 +- .../new-booking-form/step1-contract-type.tsx | 12 +- .../new-booking-form/step2-service-type.tsx | 8 +- .../bookings/new-booking-form/step4-route.tsx | 27 +-- .../new-booking-form/step5-cargo-details.tsx | 20 +- .../new-booking-form/step8-review.tsx | 8 +- packages/types/src/freight/index.ts | 49 ++--- 9 files changed, 218 insertions(+), 108 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 a1cbc6376..5be286d76 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -3,13 +3,21 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import { useNavigate } from "react-router-dom"; -import { Check, CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react"; +import { + AlertCircle, + Check, + CheckCircle2, + ChevronLeft, + ChevronRight, + LoaderCircle, +} from "lucide-react"; import { Button } from "@edr/ui-common"; import type { Freight } from "@edr/types"; import Breadcrumbs from "@/components/Breadcrumbs"; import { api } from "@/services/api"; import type { CreateBookingPayload } from "@/services/bookings.service"; import { + BookingFormInputValues, STEPS, bookingFormSchema, calcWagons, @@ -46,7 +54,7 @@ export default function NewBookingPage() { }, }); - const form = useForm({ + const form = useForm({ defaultValues: initialBookingFormValues, resolver: zodResolver(bookingFormSchema), mode: "onChange", @@ -77,8 +85,6 @@ export default function NewBookingPage() { return; } - const reference = data.previousContractRef; - const totalWeight = data.cargoType === "container" ? data.containers.reduce( @@ -87,59 +93,115 @@ export default function NewBookingPage() { ) : Number(data.cargoWeight || 0); - const apiPayload = { - reference, - customerId: customer!.id, - scheduledDate: new Date().toISOString().slice(0, 10), - totalAmount: 0, - contractType: - data.contractType.toUpperCase() as CreateBookingPayload["contractType"], - previousContractId: data.previousContractRef || undefined, - serviceType: - data.service.serviceType === "rail" - ? "RAIL_ONLY" - : "RAIL_AND_FORWARDING", - ...(data.service.serviceType === "rail" - ? {} - : { - firstMileEnabled: data.firstMile.enabled, - firstMilePickupAddress: data.firstMile.pickUpAddress ?? undefined, - lastMileEnabled: data.lastMile.enabled, - lastMileDeliveryAddress: data.lastMile.deliveryAddress ?? undefined, - equipmentReturn: - data.equipmentReturn === "with_return" - ? ("WITH_RETURN" as const) - : ("WITHOUT_RETURN" as const), - customsClearingEnabled: data.customsClearingEnabled, - }), - originStation: data.originYard, - destinationStation: data.destinationYard, - cargoTotalWeightVgm: totalWeight, - freightType: data.cargoType === "container" ? "BREAK_BULK" : "BULK", - freightSubtype: - data.cargoType === "container" - ? undefined - : data.freightType === "bulk" + // ── Reference data lookups ────────────────────────────────────────── + const yards = referenceData?.yard ?? []; + const services = referenceData?.service ?? []; + const shippingLines = referenceData?.shipping_line ?? []; + const cargoTree = referenceData?.cargo_type ?? []; + const containerGroups = referenceData?.containers ?? []; + + const findYardId = (name: string): string => + yards.find((y) => y.name === name)?.id ?? ""; + + const findServiceTypeId = (): string => { + const code = data.serviceType === "rail" ? "RAIL" : "RAIL_AND_FORWARDING"; + return services.find((s) => s.code === code)?.id ?? services[0]?.id ?? ""; + }; + + const findShippingLineId = (name: string): string | undefined => + shippingLines.find((l) => l.name === name)?.id; + + const findCargoTypeId = (name: string): string | undefined => { + for (const group of cargoTree) { + const child = group.children?.find((c) => c.name === name); + if (child) return child.id; + } + return undefined; + }; + + const findContainerCargoTypeId = (): string => { + const group = cargoTree.find( + (g) => g.code === "CONTAINER" || /container/i.test(g.name), + ); + console.log(group, cargoTree); + return group?.id ?? ""; + }; + + const findContainerTypeId = (name: string): string => { + for (const group of containerGroups) { + const ct = group.types.find((t) => t.name === name); + if (ct) return ct.id; + } + return ""; + }; + + const cargoTypeId = + data.cargoType === "container" + ? findContainerCargoTypeId() + : (findCargoTypeId( + data.freightType === "bulk" ? data.bulkCommodity : data.breakBulkType, - isHazardous: data.isHazardous, - isRefrigerated: data.isRefrigerated, + ) ?? ""); + + const cargoFreeText = + data.cargoType === "container" + ? undefined + : data.freightType === "bulk" && data.bulkCommodity === "Others" + ? data.bulkCommodityOther + : data.freightType === "break_bulk" && data.breakBulkType === "Others" + ? data.breakBulkTypeOther + : undefined; + + // ── Build API payload ─────────────────────────────────────────────── + const apiPayload: CreateBookingPayload = { + scheduledDate: new Date().toISOString().slice(0, 10), + contractType: + data.contractType.toUpperCase() as CreateBookingPayload["contractType"], + serviceTypeId: findServiceTypeId(), + equipmentReturn: + data.equipmentReturn === "with_return" + ? "WITH_RETURN" + : "WITHOUT_RETURN", + originYardId: findYardId(data.originYard), + destinationYardId: findYardId(data.destinationYard), tradeDirection: - getRouteDirection(data.originYard, data.destinationYard) === "export" + direction === "export" ? "EXPORT" - : "IMPORT", + : direction === "domestic" + ? "DOMESTIC" + : "IMPORT", + cargoTypeId, + cargoTotalWeightVgm: totalWeight, + isHazardous: data.isHazardous, paymentCurrency: "USD", allowConsolidation: data.consolidationEnabled, - ...(data.cargoType === "container" && data.containers.length > 0 - ? { - containers: data.containers.map((c) => ({ - type: c.type === "40ft" ? ("40FT" as const) : ("20FT" as const), - qty: Number(c.qty || 1), - vgm: Number(c.vgm || 0), - })), - } + containers: + data.cargoType === "container" + ? data.containers.map((c) => ({ + containerTypeId: findContainerTypeId(c.containerType), + quantity: Number(c.qty || 1), + vgmPerUnitTons: Number(c.vgm || 0), + })) + : [], + ...(customer ? { customerId: customer.id } : {}), + ...(data.previousContractRef + ? { previousContractId: data.previousContractRef } : {}), - } satisfies CreateBookingPayload; + ...(data.contractType === "renewal" && data.previousContractRef + ? { pnrCode: data.previousContractRef } + : {}), + ...(data.serviceType === "rail_forwarding" && data.firstMile.enabled + ? { firstMilePickupAddress: data.firstMile.pickUpAddress } + : {}), + ...(data.serviceType === "rail_forwarding" && data.lastMile.enabled + ? { lastMileDeliveryAddress: data.lastMile.deliveryAddress } + : {}), + ...(data.shippingLine + ? { shippingLineId: findShippingLineId(data.shippingLine) } + : {}), + ...(cargoFreeText ? { cargoFreeText } : {}), + }; createMutation.mutate(apiPayload); }); @@ -178,10 +240,27 @@ export default function NewBookingPage() {
+ {createMutation.isError && ( +
+ +
+

Submission failed

+

+ {createMutation.error instanceof Error + ? createMutation.error.message + : "An unexpected error occurred. Please try again."} +

+
+
+ )} {step === 1 && } {step === 2 && } {step === 3 && ( - + )} {step === 4 && ( ) : ( - )}
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 4da8ed6ef..d994f93f7 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 @@ -249,6 +249,7 @@ export const bookingFormSchema = z }); export type BookingFormValues = z.infer; +export type BookingFormInputValues = z.input; export const initialBookingFormValues: DeepPartial = { previousContractRef: "", diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx index 120aa48a3..a731d081c 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx @@ -21,7 +21,7 @@ import { SelectTrigger, SelectValue, } from "@edr/ui-common"; -import type { BookingFormValues } from "./schema"; +import type { BookingFormInputValues, BookingFormValues } from "./schema"; import { cn } from "@/lib/utils"; export function OptionFieldError({ error }: { error?: { message?: string } }) { @@ -122,7 +122,7 @@ export function SelectField({ disabled, children, }: { - field: ControllerRenderProps; + field: ControllerRenderProps; error?: RhfFieldError; label: string; placeholder: string; diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx index 62a66d0e1..717de081e 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step1-contract-type.tsx @@ -1,7 +1,11 @@ import { Controller, type UseFormReturn } from "react-hook-form"; import { FileText, RefreshCw } from "lucide-react"; import { Field } from "@edr/ui-common"; -import { MOCK_VALID_CONTRACTS, type BookingFormValues } from "./schema"; +import { + BookingFormInputValues, + MOCK_VALID_CONTRACTS, + type BookingFormValues, +} from "./schema"; import { AlertBox, OptionCard, @@ -11,7 +15,11 @@ import { StepHeader, } from "./shared"; -type BookingForm = UseFormReturn; +type BookingForm = UseFormReturn< + BookingFormInputValues, + any, + BookingFormValues +>; export function Step1ContractType({ form }: { form: BookingForm }) { const contractType = form.watch("contractType"); diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step2-service-type.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step2-service-type.tsx index e9bd1d331..d202ce5c6 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step2-service-type.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step2-service-type.tsx @@ -2,10 +2,14 @@ import { useEffect, useRef } from "react"; import { Controller, type UseFormReturn } from "react-hook-form"; import { FileText, Package, Train, Truck } from "lucide-react"; import { Badge, Field, FieldError, Input, Switch } from "@edr/ui-common"; -import { type BookingFormValues } from "./schema"; +import { BookingFormInputValues, type BookingFormValues } from "./schema"; import { OptionCard, OptionFieldError, StepHeader } from "./shared"; -type BookingForm = UseFormReturn; +type BookingForm = UseFormReturn< + BookingFormInputValues, + any, + BookingFormValues +>; export function Step2ServiceType({ form }: { form: BookingForm }) { const serviceType = form.watch("serviceType"); 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 2713197a0..f42e19449 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 @@ -4,16 +4,17 @@ import { Flame, MapPin, Snowflake } from "lucide-react"; import { Field, SelectItem, Separator, Skeleton, Switch } from "@edr/ui-common"; import type { Freight } from "@edr/types"; import { + BookingFormInputValues, type BookingFormValues, getRouteDirection, } from "./schema"; -import { - SelectField, - StepHeader, - StepLabel, -} from "./shared"; +import { SelectField, StepHeader, StepLabel } from "./shared"; -type BookingForm = UseFormReturn; +type BookingForm = UseFormReturn< + BookingFormInputValues, + any, + BookingFormValues +>; export function Step4Route({ form, @@ -115,13 +116,13 @@ export function Step4Route({ />
{direction && ( -
- - {directionLabel[direction]} -
- )} +
+ + {directionLabel[direction]} +
+ )} )} 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 0105f54c3..758cdede0 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,9 +1,17 @@ import { useMemo } from "react"; import { Controller, useFieldArray, type UseFormReturn } from "react-hook-form"; import { MapPin, Package, Plus, Trash2, Weight } from "lucide-react"; -import { Button, Field, FieldError, FieldLabel, Input, Skeleton } from "@edr/ui-common"; +import { + Button, + Field, + FieldError, + FieldLabel, + Input, + Skeleton, +} from "@edr/ui-common"; import type { Freight } from "@edr/types"; import { + BookingFormInputValues, calcWagons, type BookingFormValues, type RouteDirection, @@ -17,7 +25,11 @@ import { StepLabel, } from "./shared"; -type BookingForm = UseFormReturn; +type BookingForm = UseFormReturn< + BookingFormInputValues, + any, + BookingFormValues +>; export function Step5CargoDetails({ form, @@ -50,8 +62,8 @@ export function Step5CargoDetails({ const bulkCommodityOptions = useMemo(() => { if (!referenceData?.cargo_type) return []; - return referenceData.cargo_type.flatMap((group) => - group.children?.map((c) => c.name) ?? [], + return referenceData.cargo_type.flatMap( + (group) => group.children?.map((c) => c.name) ?? [], ); }, [referenceData]); diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx index 6f72dc63f..2c1b2e4d0 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step8-review.tsx @@ -11,13 +11,17 @@ import { Textarea, } from "@edr/ui-common"; import { + BookingFormInputValues, type BookingFormValues, type RouteDirection, - type WagonCalcResult, } from "./schema"; import { StepHeader } from "./shared"; -type BookingForm = UseFormReturn; +type BookingForm = UseFormReturn< + BookingFormInputValues, + any, + BookingFormValues +>; export function Step8Review({ form, diff --git a/packages/types/src/freight/index.ts b/packages/types/src/freight/index.ts index f4a0874ac..2dce17419 100644 --- a/packages/types/src/freight/index.ts +++ b/packages/types/src/freight/index.ts @@ -274,45 +274,36 @@ export interface BookingReferenceData { // ── DTOs ─────────────────────────────────────────────────────────────────────── +export interface CreateBookingContainerDto { + containerTypeId: string; + quantity: number; + vgmPerUnitTons: number; +} + export interface CreateBookingDto { - reference: string; - customerId: string; + reference?: string; + customerId?: string; trainId?: string; scheduledDate: string; - totalAmount: number; - paymentStatus?: string; contractType: "NEW" | "RENEWAL"; previousContractId?: string; - serviceType: "RAIL_ONLY" | "RAIL_AND_FORWARDING"; - - firstMileEnabled?: boolean; + serviceTypeId: string; firstMilePickupAddress?: string; - lastMileEnabled?: boolean; lastMileDeliveryAddress?: string; - - equipmentReturn: "WITH_RETURN" | "WITHOUT_RETURN"; - customsClearingEnabled?: boolean; - originStation: string; - destinationStation: string; + equipmentReturn: "WITH_RETURN" | "WITHOUT_RETURN" | "NA"; + originYardId: string; + destinationYardId: string; + tradeDirection: "IMPORT" | "EXPORT" | "DOMESTIC"; + cargoTypeId: string; + cargoFreeText?: string; + shippingLineId?: string; cargoTotalWeightVgm: number; - - freightType: "BULK" | "BREAK_BULK"; - freightSubtype?: string; - isHazardous?: boolean; - isRefrigerated?: boolean; - - tradeDirection: "IMPORT" | "EXPORT"; - paymentCurrency: string; - allowConsolidation?: boolean; - + paymentCurrency: "ETB" | "USD"; + pnrCode?: string; startDate?: string; endDate?: string; financialTerms?: string; - - containers?: Array<{ - type: "20FT" | "40FT"; - qty: number; - vgm: number; - }>; + containers: CreateBookingContainerDto[]; + allowConsolidation?: boolean; }