From 4691f5ad3f1591e4b11050deec1adc8f95dc10ec Mon Sep 17 00:00:00 2001
From: ghost2023
Date: Fri, 22 May 2026 15:43:20 +0300
Subject: [PATCH] feat: enhance the new booking form with RHC
---
.../src/pages/bookings/NewBookingPage.tsx | 1718 ++---------------
.../new-booking-form/StepIndicator.tsx | 41 +
.../pages/bookings/new-booking-form/schema.ts | 491 +++++
.../bookings/new-booking-form/shared.tsx | 157 ++
.../pages/bookings/new-booking-form/steps.tsx | 1303 +++++++++++++
5 files changed, 2124 insertions(+), 1586 deletions(-)
create mode 100644 apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/StepIndicator.tsx
create mode 100644 apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/schema.ts
create mode 100644 apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/shared.tsx
create mode 100644 apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/steps.tsx
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 84a3b9c69..2fe104665 100644
--- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx
@@ -1,1549 +1,125 @@
-import { Fragment, useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
-import {
- AlertTriangle,
- Check,
- CheckCircle2,
- ChevronLeft,
- ChevronRight,
- FileText,
- Flame,
- Info,
- Loader2,
- MapPin,
- Package,
- RefreshCw,
- Snowflake,
- Train,
- Weight,
- XCircle,
-} from "lucide-react";
-import {
- Badge,
- Button,
- Card,
- CardContent,
- CardHeader,
- CardTitle,
- Input,
- Label,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- SmartFileInput,
- Switch,
- Textarea,
-} from "@edr/ui-common";
+import { CheckCircle2, ChevronLeft, ChevronRight } from "lucide-react";
+import { Button } from "@edr/ui-common";
import Breadcrumbs from "@/components/Breadcrumbs";
-
-// ── Mock Constants ─────────────────────────────────────────────────────────────
-
-const STATIONS = [
- "Addis Ababa",
- "Adama",
- "Mojo",
- "Awash",
- "Mieso",
- "Dire Dawa",
- "Aysha",
- "Ali Sabieh",
- "Holhol",
- "Djibouti City",
-];
-
-const ETHIOPIA_STATIONS = new Set([
- "Addis Ababa",
- "Adama",
- "Mojo",
- "Awash",
- "Mieso",
- "Dire Dawa",
-]);
-
-const BULK_COMMODITIES = [
- "Coffee",
- "Beans",
- "Fertilizer",
- "Sugar",
- "Oil",
- "Livestock",
- "Steel",
- "Others",
-];
-
-const BREAK_BULK_TYPES = ["Machinery", "Ro-Ro", "Others"];
-
-const MOCK_VALID_CONTRACTS = [
- "EDR-2024-10001",
- "EDR-2024-10002",
- "EDR-2023-88123",
- "EDR-2022-55442",
-];
-
-// US-04 compliance documents (TIN, Business License, Registration, National ID, optional PoA)
-const BOOKING_DOCS_SETTING = {
- id: "booking-compliance",
- createdAt: "",
- updatedAt: "",
- deletedAt: null,
- code: "booking_compliance_docs",
- label: "Compliance Documents",
- description:
- "Upload your company's legal credentials. All mandatory documents must be submitted before the contract request can be reviewed by EDR Line Staff.",
- entity: "booking" as const,
- fields: [
- {
- id: "f1",
- createdAt: "",
- updatedAt: "",
- deletedAt: null,
- settingId: "booking-compliance",
- fileKey: "tin_certificate",
- fileLabel: "TIN Certificate",
- helpText:
- "Tax Identification Number certificate issued by ERCA (10-digit TIN).",
- isRequired: true,
- isMultiple: false,
- maxFiles: 1,
- allowedExtensions: ["pdf", "jpg", "jpeg", "png"],
- maxSizeMb: 5,
- order: 1,
- },
- {
- id: "f2",
- createdAt: "",
- updatedAt: "",
- deletedAt: null,
- settingId: "booking-compliance",
- fileKey: "business_license",
- fileLabel: "Business / Investment License",
- helpText:
- "Current business or investment license issued by the relevant government authority.",
- isRequired: true,
- isMultiple: false,
- maxFiles: 1,
- allowedExtensions: ["pdf", "jpg", "jpeg", "png"],
- maxSizeMb: 5,
- order: 2,
- },
- {
- id: "f3",
- createdAt: "",
- updatedAt: "",
- deletedAt: null,
- settingId: "booking-compliance",
- fileKey: "business_registration",
- fileLabel: "Business Registration Certificate",
- helpText: "Certificate of registration from the relevant authority.",
- isRequired: true,
- isMultiple: false,
- maxFiles: 1,
- allowedExtensions: ["pdf", "jpg", "jpeg", "png"],
- maxSizeMb: 5,
- order: 3,
- },
- {
- id: "f4",
- createdAt: "",
- updatedAt: "",
- deletedAt: null,
- settingId: "booking-compliance",
- fileKey: "national_id",
- fileLabel: "National ID / Passport",
- helpText:
- "Valid government-issued ID or passport of the authorized signatory.",
- isRequired: true,
- isMultiple: false,
- maxFiles: 1,
- allowedExtensions: ["pdf", "jpg", "jpeg", "png"],
- maxSizeMb: 5,
- order: 4,
- },
- {
- id: "f5",
- createdAt: "",
- updatedAt: "",
- deletedAt: null,
- settingId: "booking-compliance",
- fileKey: "power_of_attorney",
- fileLabel: "Power of Attorney (PoA)",
- helpText:
- "Required only if a representative is signing on behalf of the company.",
- isRequired: false,
- isMultiple: false,
- maxFiles: 1,
- allowedExtensions: ["pdf", "jpg", "jpeg", "png"],
- maxSizeMb: 5,
- order: 5,
- },
- ],
-};
-
-const REQUIRED_DOC_KEYS = [
- "tin_certificate",
- "business_license",
- "business_registration",
- "national_id",
-];
-
-const STEPS = [
- { id: 1, label: "Contract Type", short: "Contract" },
- { id: 2, label: "Service Type", short: "Service" },
- { id: 3, label: "First & Last Mile", short: "Mile" },
- { id: 4, label: "Route", short: "Route" },
- { id: 5, label: "Cargo Details", short: "Cargo" },
- { id: 6, label: "Wagon Allocation", short: "Wagons" },
- { id: 7, label: "Documents", short: "Docs" },
- { id: 8, label: "Review & Submit", short: "Submit" },
-] as const;
-
-// ── Types ──────────────────────────────────────────────────────────────────────
-
-type ContractTypeVal = "new" | "renewal" | "";
-type ServiceTypeVal = "rail" | "rail_forwarding" | "";
-type FreightTypeVal = "bulk" | "break_bulk" | "";
-type ContainerTypeVal = "20ft" | "40ft" | "";
-type EquipmentReturn = "with_return" | "without_return";
-
-interface FormData {
- contractType: ContractTypeVal;
- previousContractRef: string;
- draftContractId: string;
- serviceType: ServiceTypeVal;
- firstMileEnabled: boolean;
- pickUpAddress: string;
- lastMileEnabled: boolean;
- deliveryAddress: string;
- equipmentReturn: EquipmentReturn;
- originYard: string;
- destinationYard: string;
- cargoType: "container" | "bulk" | "";
- cargoWeight: string;
- freightType: FreightTypeVal;
- bulkCommodity: string;
- bulkCommodityOther: string;
- breakBulkType: string;
- breakBulkTypeOther: string;
- isHazardous: boolean;
- isRefrigerated: boolean;
- containerType: ContainerTypeVal;
- quantity: string;
- vgm: string;
- consolidationEnabled: boolean;
- documents: Record;
- notes: string;
- termsAccepted: boolean;
-}
-
-const INITIAL_DATA: FormData = {
- contractType: "",
- previousContractRef: "",
- draftContractId: "",
- serviceType: "",
- firstMileEnabled: false,
- pickUpAddress: "",
- lastMileEnabled: false,
- deliveryAddress: "",
- equipmentReturn: "with_return",
- originYard: "",
- destinationYard: "",
- cargoType: "",
- cargoWeight: "",
- freightType: "",
- bulkCommodity: "",
- bulkCommodityOther: "",
- breakBulkType: "",
- breakBulkTypeOther: "",
- isHazardous: false,
- isRefrigerated: false,
- containerType: "",
- quantity: "1",
- vgm: "",
- consolidationEnabled: false,
- documents: {},
- notes: "",
- termsAccepted: false,
-};
-
-// ── Helpers ────────────────────────────────────────────────────────────────────
-
-function genContractId(): string {
- const yr = new Date().getFullYear();
- const n = Math.floor(10000 + Math.random() * 90000);
- return `EDR-DRAFT-${yr}-${n}`;
-}
-
-type RouteDirection = "import" | "export" | "domestic" | null;
-
-function getRouteDirection(origin: string, dest: string): RouteDirection {
- if (!origin || !dest) return null;
- const oEth = ETHIOPIA_STATIONS.has(origin);
- const dEth = ETHIOPIA_STATIONS.has(dest);
- if (oEth && !dEth) return "export";
- if (!oEth && dEth) return "import";
- if (oEth && dEth) return "domestic";
- return null;
-}
-
-interface WagonCalcResult {
- totalWagons: number;
- hasOddUnit: boolean;
- sharedWagons: number;
-}
-
-function calcWagons(type: ContainerTypeVal, qty: number): WagonCalcResult {
- if (type === "40ft")
- return { totalWagons: qty, hasOddUnit: false, sharedWagons: qty };
- const pairs = Math.floor(qty / 2);
- const odd = qty % 2;
- return { totalWagons: pairs + odd, hasOddUnit: odd > 0, sharedWagons: pairs };
-}
-
-// ── Shared primitives ──────────────────────────────────────────────────────────
-
-function OptionCard({
- selected,
- onClick,
- disabled,
- children,
-}: {
- selected: boolean;
- onClick?: () => void;
- disabled?: boolean;
- children: React.ReactNode;
-}) {
- return (
-
- {selected && !disabled && (
-
-
-
- )}
- {children}
-
- );
-}
-
-function AlertBox({
- tone,
- children,
-}: {
- tone: "warning" | "error" | "success" | "info";
- children: React.ReactNode;
-}) {
- const s = {
- warning: "bg-amber-50 border-amber-200 text-amber-800",
- error: "bg-red-50 border-red-200 text-red-800",
- success: "bg-emerald-50 border-emerald-200 text-emerald-800",
- info: "bg-sky-50 border-sky-200 text-sky-800",
- };
- const icons = {
- warning: ,
- error: ,
- success: ,
- info: ,
- };
- return (
-
- {icons[tone]}
-
{children}
-
- );
-}
-
-function Field({
- label,
- children,
-}: {
- label: string;
- children: React.ReactNode;
-}) {
- return (
-
- {label}
- {children}
-
- );
-}
-
-function Divider() {
- return
;
-}
-
-function StepLabel({ children }: { children: React.ReactNode }) {
- return (
-
- {children}
-
- );
-}
-
-// ── Step 1: Contract Type ──────────────────────────────────────────────────────
-
-function Step1({
- d,
- set,
- renewalValid,
- renewalValidating,
- onValidate,
-}: {
- d: FormData;
- set: (k: K, v: FormData[K]) => void;
- renewalValid: boolean | null;
- renewalValidating: boolean;
- onValidate: () => void;
-}) {
- return (
-
-
-
Contract Type
-
- New contract or renewal of an existing one.
-
-
-
-
-
{
- set("contractType", "new");
- if (!d.draftContractId) set("draftContractId", genContractId());
- }}
- >
-
-
-
- New Contract
-
- Blank contract form. A draft ID is auto-generated.
-
- {d.contractType === "new" && d.draftContractId && (
-
- {d.draftContractId}
-
- )}
-
-
-
set("contractType", "renewal")}
- >
-
-
-
- Contract Renewal
-
- Enter a previous reference to auto-populate historical parameters.
-
-
-
-
- {d.contractType === "renewal" && (
-
-
-
- set("previousContractRef", e.target.value)}
- className="font-mono"
- />
-
- {renewalValidating ? (
-
- ) : (
- "Validate"
- )}
-
-
-
- {renewalValid === true && (
-
- Contract found. Company details, route, and wagon
- preferences will be pre-filled.
-
- )}
- {renewalValid === false && (
-
- Contract Reference Number not found or unauthorized. Try{" "}
- EDR-2024-10001 .
-
- )}
-
- )}
-
- );
-}
-
-// ── Step 2: Service Type ───────────────────────────────────────────────────────
-
-function Step2({
- d,
- set,
-}: {
- d: FormData;
- set: (k: K, v: FormData[K]) => void;
-}) {
- return (
-
-
-
Service Type
-
- Select the service combination you require.
-
-
-
-
-
set("serviceType", "rail")}
- >
-
-
-
- Rail Transport Only
-
- Rail transport along the EDR corridor, with optional first/last mile
- trucking.
-
-
- Option A
-
-
-
-
set("serviceType", "rail_forwarding")}
- >
-
- Rail Transport & Freight Forwarding
-
- Rail transport plus documentation, customs liaison, and a dedicated
- coordinator.
-
-
- Option B
-
-
-
-
-
- Customs and Clearance Service cannot be selected independently — it must
- be bundled with a Rail Transport service.
-
-
- );
-}
-
-// ── Step 3: First & Last Mile ──────────────────────────────────────────────────
-
-function Step3({
- d,
- set,
-}: {
- d: FormData;
- set: (k: K, v: FormData[K]) => void;
-}) {
- return (
-
-
-
First & Last Mile
-
- Configure trucking and container return options.
-
-
-
-
- {/* First Mile */}
-
-
-
-
First Mile – Pick-up
-
- Truck pick-up from your premises to the origin rail yard.
-
-
-
set("firstMileEnabled", v)}
- />
-
- {d.firstMileEnabled && (
-
- set("pickUpAddress", e.target.value)}
- />
-
- )}
-
-
- {/* Last Mile */}
-
-
-
-
Last Mile – Delivery
-
- Truck delivery from the destination rail yard to the final
- address.
-
-
-
set("lastMileEnabled", v)}
- />
-
- {d.lastMileEnabled && (
-
- set("deliveryAddress", e.target.value)}
- />
-
- )}
-
-
- {/* Equipment Return */}
-
-
Equipment Return
-
- Declare whether the container asset will be returned after
- unloading.
-
-
-
set("equipmentReturn", "with_return")}
- >
- With Return
-
- Container returned to EDR after unloading.
-
-
-
set("equipmentReturn", "without_return")}
- >
- Without Return
-
- Container retained by the customer after delivery.
-
-
-
-
-
-
- );
-}
-
-// ── Step 4: Route & Cargo ──────────────────────────────────────────────────────
-
-function Step4({
- d,
- set,
-}: {
- d: FormData;
- set: (k: K, v: FormData[K]) => void;
-}) {
- const direction = getRouteDirection(d.originYard, d.destinationYard);
- const directionStyle: Record = {
- export: "bg-sky-50 text-sky-800 border-sky-200",
- import: "bg-amber-50 text-amber-800 border-amber-200",
- domestic: "bg-muted text-muted-foreground border-border",
- };
- const directionLabel: Record = {
- export: "Export workflow (Ethiopia → Djibouti)",
- import: "Import workflow (Djibouti → Ethiopia)",
- domestic: "Domestic corridor",
- };
-
- return (
-
-
-
Route
-
- Select the origin and destination yards.
-
-
-
- {/* Route */}
-
-
Route
-
-
- set("originYard", v)}
- >
-
-
-
-
- {STATIONS.filter((s) => s !== d.destinationYard).map((s) => (
-
- {s}
-
- ))}
-
-
-
-
- set("destinationYard", v)}
- >
-
-
-
-
- {STATIONS.filter((s) => s !== d.originYard).map((s) => (
-
- {s}
-
- ))}
-
-
-
-
- {direction && (
-
-
- {directionLabel[direction]}
-
- )}
-
-
-
-
- {/* Modifiers */}
-
-
-
-
-
-
Hazardous Material
-
- Applies a Hazard Surcharge to the final bill.
-
-
-
-
set("isHazardous", v)}
- />
-
-
-
-
-
-
Refrigerated Cargo
-
- Temperature-controlled transport — applies a Refrigerator
- Surcharge.
-
-
-
-
set("isRefrigerated", v)}
- />
-
-
-
- );
-}
-
-// ── Step 5: Container Configuration ───────────────────────────────────────────
-
-function Step5({
- d,
- set,
- direction,
-}: {
- d: FormData;
- set: (k: K, v: FormData[K]) => void;
- direction: RouteDirection;
-}) {
- const qty = parseInt(d.quantity) || 1;
- const vgm = parseFloat(d.vgm) || 0;
-
- let overweightAlert: string | null = null;
- if (d.containerType === "20ft" && vgm > 0) {
- const limit = direction === "export" ? 25 : 20;
- if (vgm > limit)
- overweightAlert = `VGM ${vgm}t exceeds the ${limit}t ${direction ?? "standard"} limit for a 20ft container. An Overweight Surcharge will apply.`;
- }
- if (d.containerType === "40ft" && vgm > 32.5)
- overweightAlert = `VGM ${vgm}t exceeds the 32.5t global limit for a 40ft container. An Overweight Surcharge will apply.`;
-
- return (
-
-
-
Cargo Details
-
- Define your cargo type, weight, and container configuration.
-
-
-
- {/* Cargo Type */}
-
-
Cargo Type *
-
-
set("cargoType", "container")}
- >
-
- Container
-
- Pre-packed containerized cargo (20ft / 40ft).
-
-
-
set("cargoType", "bulk")}
- >
-
-
-
- Bulk
-
- Bulk commodities or break-bulk cargo.
-
-
-
-
-
- {/* Bulk section */}
- {d.cargoType === "bulk" && (
- <>
-
-
-
Freight Type *
-
-
set("freightType", "bulk")}
- >
- Bulk
-
- Coffee, fertilizer, grain, ore, etc.
-
-
-
set("freightType", "break_bulk")}
- >
- Break-Bulk
-
- Machinery, vehicles, project cargo, etc.
-
-
-
-
- {d.freightType === "bulk" && (
-
- {
- set("bulkCommodity", v);
- if (v !== "Others") set("bulkCommodityOther", "");
- }}
- >
-
-
-
-
- {BULK_COMMODITIES.map((c) => (
-
- {c}
-
- ))}
-
-
- {d.bulkCommodity === "Others" && (
- set("bulkCommodityOther", e.target.value)}
- />
- )}
-
- )}
-
- {d.freightType === "break_bulk" && (
-
- {
- set("breakBulkType", v);
- if (v !== "Others") set("breakBulkTypeOther", "");
- }}
- >
-
-
-
-
- {BREAK_BULK_TYPES.map((c) => (
-
- {c}
-
- ))}
-
-
- {d.breakBulkType === "Others" && (
- set("breakBulkTypeOther", e.target.value)}
- />
- )}
-
- )}
-
-
-
-
- {/* Weight */}
-
-
Weight
-
-
-
- set("cargoWeight", e.target.value)}
- className="pl-9"
- min="0"
- step="0.01"
- />
-
-
-
- >
- )}
-
- {/* Container section */}
- {d.cargoType === "container" && (
- <>
-
-
-
Container Configuration
-
- {[
- {
- 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) => (
-
set("containerType", ct.val)}
- >
-
- {ct.limit}
-
- ))}
-
-
- {direction && (
-
-
- Route detected as{" "}
-
- {direction}
- {" "}
- workflow
-
- )}
-
-
-
-
- {overweightAlert && (
-
- Overweight Alert: {overweightAlert}
-
- )}
- >
- )}
-
- );
-}
-
-// ── Step 6: Wagon Allocation ───────────────────────────────────────────────────
-
-function Step6({
- d,
- set,
- wagons,
-}: {
- d: FormData;
- set: (k: K, v: FormData[K]) => void;
- wagons: WagonCalcResult | null;
-}) {
- const qty = parseInt(d.quantity) || 1;
-
- return (
-
-
-
Wagon Allocation
-
- System-calculated wagon requirements based on your container profile.
-
-
-
- {!wagons ? (
-
- Complete the container configuration in the previous step to see wagon
- allocation.
-
- ) : (
- <>
-
-
-
- {wagons.totalWagons}
-
-
- Wagons Required
-
-
-
-
{qty}
-
- {d.containerType} Containers
-
-
-
-
{wagons.sharedWagons}
-
Shared Slots
-
-
-
-
-
Wagon Layout
-
- {Array.from({ length: wagons.totalWagons }, (_, i) => {
- const isOdd = wagons.hasOddUnit && i === wagons.totalWagons - 1;
- return (
-
- {isOdd
- ? `1 × ${d.containerType} (½)`
- : d.containerType === "20ft"
- ? "2 × 20ft"
- : "1 × 40ft"}
-
- );
- })}
-
-
-
-
- Formula:{" "}
- {d.containerType === "40ft"
- ? "1 × 40ft = 1 Rail Wagon"
- : `CEILING(${qty} ÷ 2) = ${wagons.totalWagons} Rail Wagon${wagons.totalWagons > 1 ? "s" : ""} — (2 × 20ft = 1 Wagon)`}
-
-
- {wagons.hasOddUnit && (
- <>
-
-
-
-
-
- Consolidation Option (US-07)
-
-
-
- You have 1 unpaired 20ft container. Opt in to share a wagon
- slot with another shipper to optimise costs (2 × 20ft = 1
- Wagon), or request a dedicated wagon.
-
-
-
set("consolidationEnabled", true)}
- >
- Allow Consolidation
-
- Share a wagon slot — billing split with co-loader.
-
-
-
set("consolidationEnabled", false)}
- >
- Dedicated Wagon
-
- Exclusive slot — standard single-party billing.
-
-
-
-
- >
- )}
- >
- )}
-
- );
-}
-
-// ── Step 7: Compliance Documents ───────────────────────────────────────────────
-
-function Step7Docs({
- d,
- set,
-}: {
- d: FormData;
- set: (k: K, v: FormData[K]) => void;
-}) {
- const uploadedRequired = REQUIRED_DOC_KEYS.filter((k) => {
- const f = d.documents[k];
- if (!f) return false;
- return Array.isArray(f) ? f.length > 0 : true;
- }).length;
-
- return (
-
-
-
- Compliance Documents
-
-
- Upload your company's legal credentials for EDR contract eligibility
- verification (US-04).
-
-
-
-
-
- {uploadedRequired}/{REQUIRED_DOC_KEYS.length}
-
-
- {uploadedRequired < REQUIRED_DOC_KEYS.length
- ? `${REQUIRED_DOC_KEYS.length - uploadedRequired} mandatory document(s) still needed.`
- : "All mandatory documents uploaded. Power of Attorney is optional."}
-
-
-
-
set("documents", val)}
- />
-
- );
-}
-
-// ── Step 8: Review & Submit ────────────────────────────────────────────────────
-
-function Step8Review({
- d,
- set,
- setStep,
- wagons,
- direction,
-}: {
- d: FormData;
- set: (k: K, v: FormData[K]) => void;
- setStep: (n: number) => void;
- wagons: WagonCalcResult | null;
- direction: RouteDirection;
-}) {
- function Row({
- label,
- value,
- target,
- }: {
- label: string;
- value: string;
- target: number;
- }) {
- return (
-
-
-
{label}
-
{value || "—"}
-
-
setStep(target)}
- className="shrink-0 text-xs font-medium text-primary hover:underline"
- >
- Edit
-
-
- );
- }
-
- const cargoValue =
- d.freightType === "bulk"
- ? `Bulk — ${d.bulkCommodity === "Others" ? d.bulkCommodityOther : d.bulkCommodity}`
- : d.freightType === "break_bulk"
- ? `Break-Bulk — ${d.breakBulkType === "Others" ? d.breakBulkTypeOther : d.breakBulkType}`
- : "";
-
- const uploadedCount = REQUIRED_DOC_KEYS.filter((k) => {
- const f = d.documents[k];
- if (!f) return false;
- return Array.isArray(f) ? f.length > 0 : true;
- }).length;
-
- return (
-
-
-
Review & Submit
-
- Confirm your contract request before sending it for EDR staff review.
-
-
-
-
-
-
-
- Contract & Service
-
-
-
-
-
-
-
-
-
-
-
-
- First & Last Mile
-
-
-
-
-
-
-
-
-
-
-
-
- Route & Cargo
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Container & Wagons
-
-
-
-
-
- 1 ? "s" : ""}`
- : ""
- }
- target={6}
- />
-
-
-
-
-
-
- Additional Notes
-
-
-
- set("termsAccepted", !d.termsAccepted)}
- className={`mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded border-2 transition ${d.termsAccepted ? "border-primary bg-primary" : "border-input"
- }`}
- >
- {d.termsAccepted && (
-
- )}
-
-
- I confirm the information is accurate and agree to EDR's{" "}
-
- freight contract terms and conditions
-
- .
-
-
-
- );
-}
-
-// ── Main Page ──────────────────────────────────────────────────────────────────
+import {
+ MOCK_VALID_CONTRACTS,
+ STEPS,
+ bookingFormSchema,
+ calcWagons,
+ getRouteDirection,
+ initialBookingFormValues,
+ stepFields,
+ type BookingFormValues,
+} from "./new-booking-form/schema";
+import { StepIndicator } from "./new-booking-form/StepIndicator";
+import {
+ Step1ContractType,
+ Step2ServiceType,
+ Step3FirstLastMile,
+ Step4Route,
+ Step5CargoDetails,
+ Step6WagonAllocation,
+ Step7Documents,
+ Step8Review,
+} from "./new-booking-form/steps";
export default function NewBookingPage() {
const navigate = useNavigate();
const [step, setStep] = useState(1);
- const [data, setData] = useState(INITIAL_DATA);
const [renewalValidating, setRenewalValidating] = useState(false);
const [renewalValid, setRenewalValid] = useState(null);
const [submitted, setSubmitted] = useState(false);
- const direction = useMemo(
- () => getRouteDirection(data.originYard, data.destinationYard),
- [data.originYard, data.destinationYard],
- );
- const wagons = useMemo(() => {
- if (!data.containerType || !data.quantity) return null;
- return calcWagons(data.containerType, parseInt(data.quantity) || 1);
- }, [data.containerType, data.quantity]);
+ const form = useForm({
+ resolver: zodResolver(bookingFormSchema),
+ defaultValues: initialBookingFormValues,
+ mode: "onChange",
+ });
- function set(k: K, v: FormData[K]) {
- setData((prev) => ({ ...prev, [k]: v }));
- }
+ const originYard = form.watch("originYard");
+ const destinationYard = form.watch("destinationYard");
+ const containerType = form.watch("containerType");
+ const quantity = form.watch("quantity");
+ const previousContractRef = form.watch("previousContractRef");
+ const contractId =
+ form.watch("draftContractId") || form.watch("previousContractRef");
+
+ const direction = useMemo(
+ () => getRouteDirection(originYard, destinationYard),
+ [originYard, destinationYard],
+ );
+
+ const wagons = useMemo(() => {
+ if (!containerType || !quantity) return null;
+ return calcWagons(containerType, parseInt(quantity) || 1);
+ }, [containerType, quantity]);
+
+ useEffect(() => {
+ setRenewalValid(null);
+ }, [previousContractRef]);
function validateRenewal() {
+ const previousContractRef = form.getValues("previousContractRef").trim();
+
+ if (!previousContractRef) {
+ form.setError("previousContractRef", {
+ type: "manual",
+ message: "Enter a previous contract reference.",
+ });
+ return;
+ }
+
setRenewalValidating(true);
setRenewalValid(null);
setTimeout(() => {
- setRenewalValidating(false);
- setRenewalValid(
- MOCK_VALID_CONTRACTS.includes(data.previousContractRef.toUpperCase()),
+ const valid = MOCK_VALID_CONTRACTS.includes(
+ previousContractRef.toUpperCase(),
);
+ setRenewalValidating(false);
+ setRenewalValid(valid);
+ if (!valid) {
+ form.setError("previousContractRef", {
+ type: "manual",
+ message: "Contract Reference Number not found or unauthorized.",
+ });
+ } else {
+ form.clearErrors("previousContractRef");
+ }
}, 1200);
}
- function canProceed(): boolean {
- switch (step) {
- case 1:
- if (data.contractType === "new") return !!data.draftContractId;
- if (data.contractType === "renewal") return renewalValid === true;
- return false;
- case 2:
- return !!data.serviceType;
- case 3:
- if (data.firstMileEnabled && !data.pickUpAddress) return false;
- if (data.lastMileEnabled && !data.deliveryAddress) return false;
- return true;
- case 4:
- if (!data.originYard || !data.destinationYard) return false;
- return true;
- case 5:
- if (!data.cargoType) return false;
- if (data.cargoType === "bulk") {
- if (!data.freightType) return false;
- if (data.freightType === "bulk") {
- if (!data.bulkCommodity) return false;
- if (data.bulkCommodity === "Others" && !data.bulkCommodityOther)
- return false;
- }
- if (data.freightType === "break_bulk") {
- if (!data.breakBulkType) return false;
- if (data.breakBulkType === "Others" && !data.breakBulkTypeOther)
- return false;
- }
- if (!data.cargoWeight) return false;
- return true;
- }
- return !!(data.containerType && data.quantity && data.vgm);
- case 6:
- return true;
- case 7:
- return REQUIRED_DOC_KEYS.every((k) => {
- const f = data.documents[k];
- if (!f) return false;
- return Array.isArray(f) ? f.length > 0 : true;
+ async function handleContinue() {
+ const valid = await form.trigger(stepFields[step], { shouldFocus: true });
+ if (!valid) return;
+
+ if (step === 1 && form.getValues("contractType") === "renewal") {
+ if (renewalValid !== true) {
+ form.setError("previousContractRef", {
+ type: "manual",
+ message:
+ "Validate the previous contract reference before continuing.",
});
- case 8:
- return data.termsAccepted;
- default:
- return true;
+ return;
+ }
}
+
+ setStep((currentStep) => Math.min(STEPS.length, currentStep + 1));
}
- function handleSubmit() {
+ function handleSubmit(data: BookingFormValues) {
+ if (data.contractType === "renewal" && renewalValid !== true) {
+ form.setError("previousContractRef", {
+ type: "manual",
+ message: "Validate the previous contract reference before submitting.",
+ });
+ setStep(1);
+ return;
+ }
+
setSubmitted(true);
setTimeout(() => navigate("/bookings"), 2500);
}
@@ -1561,7 +137,7 @@ export default function NewBookingPage() {
notified once approved.
- {data.draftContractId || data.previousContractRef}
+ {contractId}
@@ -1569,8 +145,11 @@ export default function NewBookingPage() {
}
return (
-
- {/* Sticky step indicator */}
+