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 7420f90d3..34f87db00 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -106,16 +106,10 @@ export default function NewBookingPage() { : Number(data.cargoWeight || 0); // ── Reference data lookups ────────────────────────────────────────── - const services = referenceData?.service ?? []; const shippingLines = referenceData?.shipping_line ?? []; const cargoTree = referenceData?.cargo_type ?? []; const containerGroups = referenceData?.containers ?? []; - 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; @@ -127,32 +121,32 @@ export default function NewBookingPage() { return ""; }; - const selectedChild = - data.cargoType !== "container" && data.bulkCommoditytype - ? cargoTree - .find((g) => g.code.toLowerCase() === data.freightType) - ?.children?.find((c) => c.name === data.bulkCommoditytype) - : undefined; + const bulkChild = cargoTree + .flatMap((g) => g.children ?? []) + .find((c) => c.id === data.bulkCommoditytype); - const cargoTypeId = selectedChild?.id; + const cargoTypeId = + data.cargoType === "bulk" ? data.bulkCommoditytype : undefined; - const cargoFreeText = - data.cargoType === "container" - ? undefined - : selectedChild?.show_free_text_box - ? data.cargoFreeText - : undefined; + const cargoFreeText = bulkChild?.show_free_text_box + ? data.cargoFreeText + : undefined; + + const serviceType = referenceData?.service.find( + (s) => s.id === data.serviceTypeId, + )!; // ── Build API payload ─────────────────────────────────────────────── const apiPayload: CreateBookingPayload = { scheduledDate: new Date().toISOString(), contractType: data.contractType.toUpperCase() as CreateBookingPayload["contractType"], - serviceTypeId: findServiceTypeId(), + serviceTypeId: data.serviceTypeId, equipmentReturn: data.equipmentReturn === "with_return" ? "WITH_RETURN" : "WITHOUT_RETURN", + paymentCurrency: "USD", originYardId: data.originYard, destinationYardId: data.destinationYard, tradeDirection: direction!, @@ -180,10 +174,10 @@ export default function NewBookingPage() { ...(data.contractType === "renewal" && data.previousContractRef ? { pnrCode: data.previousContractRef } : {}), - ...(data.serviceType === "rail_forwarding" && data.firstMile.enabled + ...(serviceType.includesFirstMile && data.firstMile.enabled ? { firstMilePickupAddress: data.firstMile.pickUpAddress } : {}), - ...(data.serviceType === "rail_forwarding" && data.lastMile.enabled + ...(serviceType.includesLastMile && data.lastMile.enabled ? { lastMileDeliveryAddress: data.lastMile.deliveryAddress } : {}), ...(data.shippingLine @@ -265,7 +259,9 @@ export default function NewBookingPage() { )} {step === 1 && } - {step === 2 && } + {step === 2 && ( + + )} {step === 3 && ( = { export const stepFields: Record>> = { 1: ["contractType", "previousContractRef"], 2: [ - "serviceType", + "serviceTypeId", "firstMile", "lastMile", "equipmentReturn", 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 9c72a5356..5597d2fa0 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 @@ -1,18 +1,52 @@ import { useEffect, useRef } from "react"; import { Controller, type UseFormReturn } from "react-hook-form"; -import { FileText, Package, Train, Truck } from "lucide-react"; -import { Badge, Switch, TextInput } from "@mantine/core"; +import { FileText, Train, Truck } from "lucide-react"; +import { Switch, TextInput } from "@mantine/core"; import { BookingFormInputValues, type BookingFormValues } from "./schema"; import { OptionCard, OptionFieldError, StepHeader } from "./shared"; -type BookingForm = UseFormReturn; +import type { Freight } from "@edr/types"; -export function Step2ServiceType({ form }: { form: BookingForm }) { - const serviceType = form.watch("serviceType"); +type BookingForm = UseFormReturn< + BookingFormInputValues, + any, + BookingFormValues +>; + +export function Step2ServiceType({ + form, + referenceData, +}: { + form: BookingForm; + + referenceData?: Freight.BookingReferenceData; +}) { + const serviceTypeId = form.watch("serviceTypeId"); + const serviceType = referenceData?.service.find( + (s) => s.id === serviceTypeId, + ); + + const { includesCustoms, includesFirstMile, includesLastMile } = + serviceType ?? {}; const firstMileEnabled = form.watch("firstMile.enabled"); const lastMileEnabled = form.watch("lastMile.enabled"); const prevServiceType = useRef(serviceType); + useEffect(() => { + form.setValue( + "firstMile", + { enabled: false, pickUpAddress: "" }, + { shouldValidate: true }, + ); + }, [includesFirstMile]); + + useEffect(() => { + form.setValue( + "lastMile", + { enabled: false, deliveryAddress: "" }, + { shouldValidate: true }, + ); + }, [includesLastMile]); useEffect(() => { const prev = prevServiceType.current; @@ -20,35 +54,12 @@ export function Step2ServiceType({ form }: { form: BookingForm }) { if (!prev || prev === serviceType) return; - if (serviceType === "rail") { - form.setValue( - "firstMile", - { enabled: false, pickUpAddress: "" }, - { shouldDirty: true, shouldValidate: true }, - ); - form.setValue( - "lastMile", - { enabled: false, deliveryAddress: "" }, - { shouldDirty: true, shouldValidate: true }, - ); - form.setValue("equipmentReturn", "with_return", { shouldDirty: true }); + if (!includesCustoms) form.setValue("customsClearingEnabled", false, { shouldDirty: true }); - } else if (serviceType === "rail_forwarding") { - form.setValue( - "firstMile", - { enabled: false, pickUpAddress: "" }, - { shouldDirty: false, shouldValidate: false }, - ); - form.setValue( - "lastMile", - { enabled: false, deliveryAddress: "" }, - { shouldDirty: false, shouldValidate: false }, - ); - } - }, [serviceType, form]); - - const showServiceSections = serviceType === "rail_forwarding"; + }, [serviceTypeId, form]); + const showServiceSections = + includesCustoms || includesFirstMile || includesLastMile; return (
(
- field.onChange("rail")} - > -
- -
-

Rail Transport Only

-

- Rail transport along the EDR corridor, with optional - first/last mile trucking. -

- - Option A - -
- - field.onChange("rail_forwarding")} - > -
- -
-

Logistics

-

- Rail transport plus documentation, customs liaison, and a - dedicated coordinator. -

- - Option B - -
+ {referenceData?.service + .filter((s) => s.canBeBookedAlone) + .map((s) => { + return ( + field.onChange(s.id)} + > +
+ +
+

{s.serviceName}

+

+ {s.description} +

+
+ ); + })}
@@ -104,112 +100,120 @@ export function Step2ServiceType({ form }: { form: BookingForm }) { {showServiceSections && (
{/* First Mile */} -
- ( -
-
- -
-

First Mile — Pick-up

-

- Truck pick-up from your premises (Door to Port) to the - origin rail yard. -

-
-
- { - const value = e.currentTarget.checked; - field.onChange(value); - if (!value) { - form.setValue("firstMile.pickUpAddress", "", { - shouldDirty: true, - shouldValidate: true, - }); - } - }} - color="edr-green" - /> -
- )} - /> - {firstMileEnabled && ( + {includesFirstMile && ( +
( - + render={({ field }) => ( +
+
+ +
+

+ First Mile — Pick-up +

+

+ Truck pick-up from your premises (Door to Port) to the + origin rail yard. +

+
+
+ { + const value = e.currentTarget.checked; + field.onChange(value); + if (!value) { + form.setValue("firstMile.pickUpAddress", "", { + shouldDirty: true, + shouldValidate: true, + }); + } + }} + color="edr-green" + /> +
)} /> - )} -
+ {firstMileEnabled && ( + ( + + )} + /> + )} +
+ )} {/* Last Mile */} -
- ( -
-
- -
-

Last Mile — Delivery

-

- Truck delivery from the destination rail yard to the - final address (Port to Door). -

-
-
- { - const value = e.currentTarget.checked; - field.onChange(value); - if (!value) { - form.setValue("lastMile.deliveryAddress", "", { - shouldDirty: true, - shouldValidate: true, - }); - form.setValue("equipmentReturn", "with_return", { - shouldDirty: true, - }); - } - }} - color="edr-green" - /> -
- )} - /> - {lastMileEnabled && ( + {includesLastMile && ( +
( - + render={({ field }) => ( +
+
+ +
+

+ Last Mile — Delivery +

+

+ Truck delivery from the destination rail yard to the + final address (Port to Door). +

+
+
+ { + const value = e.currentTarget.checked; + field.onChange(value); + if (!value) { + form.setValue("lastMile.deliveryAddress", "", { + shouldDirty: true, + shouldValidate: true, + }); + form.setValue("equipmentReturn", "with_return", { + shouldDirty: true, + }); + } + }} + color="edr-green" + /> +
)} /> - )} -
+ {lastMileEnabled && ( + ( + + )} + /> + )} +
+ )} {/* Equipment Return */} - {lastMileEnabled && ( + {includesLastMile && lastMileEnabled && (
{ field.onChange( - e.currentTarget.checked ? "with_return" : "without_return", + e.currentTarget.checked + ? "with_return" + : "without_return", ); }} color="edr-green" @@ -240,31 +246,35 @@ export function Step2ServiceType({ form }: { form: BookingForm }) { )} {/* Customs Clearing */} -
- ( -
-
- -
-

Customs Clearing Service

-

- EDR handles customs documentation and clearance on your - behalf. -

+ {includesCustoms && ( +
+ ( +
+
+ +
+

+ Customs Clearing Service +

+

+ EDR handles customs documentation and clearance on + your behalf. +

+
+ field.onChange(e.currentTarget.checked)} + color="edr-green" + />
- field.onChange(e.currentTarget.checked)} - color="edr-green" - /> -
- )} - /> -
+ )} + /> +
+ )}
)}
diff --git a/packages/types/src/freight/index.ts b/packages/types/src/freight/index.ts index 833a5d437..40d40e719 100644 --- a/packages/types/src/freight/index.ts +++ b/packages/types/src/freight/index.ts @@ -5,16 +5,16 @@ export * from "./dropdown_settings"; export * from "./overview"; export enum TradeDirection { - IMPORT = 'IMPORT', - EXPORT = 'EXPORT', - BOTH = 'BOTH', + IMPORT = "IMPORT", + EXPORT = "EXPORT", + BOTH = "BOTH", } export enum PriorityType { - USD_PAYER = 'USD_PAYER', - RAIL_AND_FORWARDING = 'RAIL_AND_FORWARDING', - GOVERNMENT_ACCOUNT = 'GOVERNMENT_ACCOUNT', - HIGH_VOLUME_SHIPMENT = 'HIGH_VOLUME_SHIPMENT', + USD_PAYER = "USD_PAYER", + RAIL_AND_FORWARDING = "RAIL_AND_FORWARDING", + GOVERNMENT_ACCOUNT = "GOVERNMENT_ACCOUNT", + HIGH_VOLUME_SHIPMENT = "HIGH_VOLUME_SHIPMENT", } /** Bonus applied to government bookings so they outrank commercial priority. */ @@ -26,19 +26,19 @@ export interface GovernmentBookingFields { } export enum ExceededAction { - WARNING_ONLY = 'WARNING_ONLY', - HARD_BLOCK = 'HARD_BLOCK', + WARNING_ONLY = "WARNING_ONLY", + HARD_BLOCK = "HARD_BLOCK", } export enum CalculationMethod { - PER_TON = 'PER_TON', - FLAT_FEE = 'FLAT_FEE', - PERCENTAGE = 'PERCENTAGE', + PER_TON = "PER_TON", + FLAT_FEE = "FLAT_FEE", + PERCENTAGE = "PERCENTAGE", } export enum FreightType { - Container = 'CONTAINER', - Bulk = 'BULK', + Container = "CONTAINER", + Bulk = "BULK", } export enum BookingStatus { @@ -390,6 +390,18 @@ export interface BookingReferenceService { id: string; name: string; code: string; + serviceName: string; + description?: string | null | undefined; + canBeBookedAlone: boolean; + includesFirstMile: boolean; + includesLastMile: boolean; + includesCustoms: boolean; + priorityBonusPoints: number; + isActive: boolean; + displayOrder: number; + createdAt: string; + updatedAt: string; + deletedAt?: string | null | undefined; } export interface BookingReferenceShippingLine { @@ -462,31 +474,34 @@ export interface CreateBookingContainerDto { } export interface CreateBookingDto { - reference?: string; - companyId?: string; - trainId?: string; - trainScheduleId?: string; + freightShapeValidation?: boolean | undefined; + reference?: string | undefined; + isGovernment?: boolean | undefined; + governmentInstitution?: string | undefined; + companyId?: string | undefined; + trainId?: string | undefined; + trainScheduleId?: string | undefined; scheduledDate: string; - contractType: "NEW" | "RENEWAL"; - previousContractId?: string; + contractType: string; + previousContractId?: string | undefined; serviceTypeId: string; - firstMilePickupAddress?: string; - lastMileDeliveryAddress?: string; - equipmentReturn: "WITH_RETURN" | "WITHOUT_RETURN" | "NA"; + firstMilePickupAddress?: string | undefined; + lastMileDeliveryAddress?: string | undefined; + equipmentReturn: string; originYardId: string; destinationYardId: string; - tradeDirection: "IMPORT" | "EXPORT" | "DOMESTIC"; - freightType: FreightType; - cargoTypeId?: string; - cargoFreeText?: string; - shippingLineId?: string; + tradeDirection: string; + freightType: string; + cargoTypeId?: string | undefined; + cargoFreeText?: string | undefined; + shippingLineId?: string | undefined; cargoTotalWeightVgm: number; - isHazardous?: boolean; - paymentCurrency: "ETB" | "USD"; - pnrCode?: string; - startDate?: string; - endDate?: string; - financialTerms?: string; + isHazardous?: boolean | undefined; + paymentCurrency: string; + pnrCode?: string | undefined; + startDate?: string | undefined; + endDate?: string | undefined; + financialTerms?: string | undefined; containers?: CreateBookingContainerDto[]; allowConsolidation?: boolean; }