-
+
- {step === 1 && (
-
- )}
+ {step === 1 && }
{step === 2 && }
- {step === 3 && }
- {step === 4 && }
- {step === 5 && (
+ {step === 3 && }
+ {step === 4 && (
)}
- {step === 6 && }
- {step === 7 && }
- {step === 8 && (
-
+ {step === 5 && (
+
)}
@@ -277,11 +207,11 @@ export default function NewBookingPage() {
{step < STEPS.length ? (
Continue
-
+
) : (
-
+
Submit Contract Request
)}
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 5ea5d4ded..4da8ed6ef 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
@@ -1,3 +1,4 @@
+import { DeepPartial, Path } from "react-hook-form";
import * as z from "zod";
export const STATIONS = [
@@ -42,123 +43,69 @@ export const MOCK_VALID_CONTRACTS = [
"EDR-2022-55442",
];
-export const REQUIRED_DOC_KEYS = [
- "tin_certificate",
- "business_license",
- "business_registration",
- // "national_id",
+export const CONTAINER_TYPES = [
+ "Dry Container",
+ "High Cubic",
+ "Reefer Container",
+ "Open Top",
+ "Flat Rack",
+ "Tank Container",
+ "Open Side",
+] as const;
+
+export const SHIPPING_LINES = [
+ "MSC",
+ "CMA CGM",
+ "Evergreen",
+ "COSCO",
+ "Hapag-Lloyd",
+ "ONE",
+ "Yang Ming",
+ "ZIM",
+ "Messina Line",
+ "Safmarine",
+ "Wan Hai",
+ "Ethiopian Shipping Lines (ESLSE)",
] as const;
export 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" },
+ { id: 2, label: "Service Type & Mile", short: "Service" },
+ { id: 3, label: "Route", short: "Route" },
+ { id: 4, label: "Cargo Details", short: "Cargo" },
+ { id: 5, label: "Review & Submit", short: "Submit" },
] as const;
-export 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: "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 fileValueSchema = z.union([
- z.custom
(),
- z.array(z.custom()),
- z.null(),
-]);
-
export const bookingFormSchema = z
.object({
contractType: z.enum(["new", "renewal"], "Select a contract type."),
previousContractRef: z.string(),
serviceType: z.enum(["rail", "rail_forwarding"], "Select a service type."),
- firstMileEnabled: z.boolean(),
- pickUpAddress: z.string(),
- lastMileEnabled: z.boolean(),
- deliveryAddress: z.string(),
- equipmentReturn: z.enum(["with_return", "without_return"]),
- originYard: z.string(),
- destinationYard: z.string(),
+ firstMile: z
+ .object({
+ enabled: z.boolean().default(false),
+ pickUpAddress: z.string(),
+ })
+ .refine((data) => !(data.enabled && !data.pickUpAddress.trim()), {
+ message: "Enter the pick-up address.",
+ path: ["pickUpAddress"],
+ }),
+ lastMile: z
+ .object({
+ enabled: z.boolean().default(false),
+ deliveryAddress: z.string(),
+ })
+ .refine((data) => !(data.enabled && !data.deliveryAddress.trim()), {
+ message: "Enter the delivery address.",
+ path: ["deliveryAddress"],
+ }),
+ equipmentReturn: z
+ .enum(["with_return", "without_return"])
+ .default("with_return"),
+ customsClearingEnabled: z.boolean().default(false),
+ originYard: z.string().min(1, "Select an origin yard."),
+ destinationYard: z.string().min(1, "Select a destination yard."),
+ shippingLine: z.string(),
cargoType: z.enum(["container", "bulk"], "Select a cargo type."),
cargoWeight: z.string(),
freightType: z.enum(["bulk", "break_bulk"]).optional(),
@@ -171,142 +118,116 @@ export const bookingFormSchema = z
containers: z.array(
z.object({
type: z.enum(["20ft", "40ft"]),
+ containerType: z.string().min(1, "Select a container type."),
qty: z
.string()
+ .refine((q) => q.length !== 0, "Quantity is required.")
.refine((q) => !isNaN(+q), "Enter a valid Number")
.refine((qty) => Number(qty) >= 1, "Must be greater than 0"),
vgm: z
.string()
+ .refine((vgm) => vgm.length !== 0, "VGM is required.")
.refine((vgm) => !isNaN(+vgm), "Enter a valid Number")
.refine((vgm) => Number(vgm) >= 0, "Must be greater than 0"),
}),
),
consolidationEnabled: z.boolean(),
- documents: z.record(z.string(), fileValueSchema),
notes: z.string(),
termsAccepted: z.boolean(),
})
- .superRefine((data, ctx) => {
- if (data.contractType === "renewal" && !data.previousContractRef.trim()) {
- ctx.addIssue({
- code: "custom",
- path: ["previousContractRef"],
- message: "Enter a previous contract reference.",
- });
- }
-
- if (data.firstMileEnabled && !data.pickUpAddress.trim()) {
- ctx.addIssue({
- code: "custom",
- path: ["pickUpAddress"],
- message: "Enter the pick-up address.",
- });
- }
-
- if (data.lastMileEnabled && !data.deliveryAddress.trim()) {
- ctx.addIssue({
- code: "custom",
- path: ["deliveryAddress"],
- message: "Enter the delivery address.",
- });
- }
-
- if (!data.originYard) {
- ctx.addIssue({
- code: "custom",
- path: ["originYard"],
- message: "Select an origin yard.",
- });
- }
-
- if (!data.destinationYard) {
- ctx.addIssue({
- code: "custom",
- path: ["destinationYard"],
- message: "Select a destination yard.",
- });
- }
-
- if (
- data.originYard &&
- data.destinationYard &&
- data.originYard === data.destinationYard
- ) {
- ctx.addIssue({
- code: "custom",
- path: ["destinationYard"],
- message: "Destination must be different from origin.",
- });
- }
-
- if (data.cargoType === "bulk") {
- if (!data.freightType) {
- ctx.addIssue({
- code: "custom",
- path: ["freightType"],
- message: "Select a freight type.",
- });
- }
-
- if (data.freightType === "bulk") {
- if (!data.bulkCommodity) {
- ctx.addIssue({
- code: "custom",
- path: ["bulkCommodity"],
- message: "Select a commodity.",
- });
- }
- if (
- data.bulkCommodity === "Others" &&
- !data.bulkCommodityOther.trim()
- ) {
- ctx.addIssue({
- code: "custom",
- path: ["bulkCommodityOther"],
- message: "Specify the commodity.",
- });
- }
- }
-
- if (data.freightType === "break_bulk") {
- if (!data.breakBulkType) {
- ctx.addIssue({
- code: "custom",
- path: ["breakBulkType"],
- message: "Select a break-bulk type.",
- });
- }
- if (
- data.breakBulkType === "Others" &&
- !data.breakBulkTypeOther.trim()
- ) {
- ctx.addIssue({
- code: "custom",
- path: ["breakBulkTypeOther"],
- message: "Specify the break-bulk type.",
- });
- }
- }
-
+ .refine(
+ (data) =>
+ !(data.contractType === "renewal" && !data.previousContractRef.trim()),
+ {
+ message: "Enter a previous contract reference.",
+ path: ["previousContractRef"],
+ },
+ )
+ .refine((data) => data.originYard !== "", {
+ message: "Select an origin yard.",
+ path: ["originYard"],
+ })
+ .refine((data) => data.destinationYard !== "", {
+ message: "Select a destination yard.",
+ path: ["destinationYard"],
+ })
+ .refine(
+ (data) =>
+ !(
+ data.originYard &&
+ data.destinationYard &&
+ data.originYard === data.destinationYard
+ ),
+ {
+ message: "Destination must be different from origin.",
+ path: ["destinationYard"],
+ },
+ )
+ .refine((data) => !(data.cargoType === "bulk" && !data.freightType), {
+ message: "Select a freight type.",
+ path: ["freightType"],
+ })
+ .refine(
+ (data) =>
+ !(
+ data.cargoType === "bulk" &&
+ data.freightType === "bulk" &&
+ !data.bulkCommodity
+ ),
+ { message: "Select a commodity.", path: ["bulkCommodity"] },
+ )
+ .refine(
+ (data) =>
+ !(
+ data.cargoType === "bulk" &&
+ data.freightType === "bulk" &&
+ data.bulkCommodity === "Others" &&
+ !data.bulkCommodityOther.trim()
+ ),
+ { message: "Specify the commodity.", path: ["bulkCommodityOther"] },
+ )
+ .refine(
+ (data) =>
+ !(
+ data.cargoType === "bulk" &&
+ data.freightType === "break_bulk" &&
+ !data.breakBulkType
+ ),
+ { message: "Select a break-bulk type.", path: ["breakBulkType"] },
+ )
+ .refine(
+ (data) =>
+ !(
+ data.cargoType === "bulk" &&
+ data.freightType === "break_bulk" &&
+ data.breakBulkType === "Others" &&
+ !data.breakBulkTypeOther.trim()
+ ),
+ {
+ message: "Specify the break-bulk type.",
+ path: ["breakBulkTypeOther"],
+ },
+ )
+ .refine(
+ (data) => {
+ if (data.cargoType !== "bulk") return true;
const cargoWeight = Number(data.cargoWeight);
- if (!data.cargoWeight || Number.isNaN(cargoWeight) || cargoWeight <= 0) {
- ctx.addIssue({
- code: "custom",
- path: ["cargoWeight"],
- message: "Enter a cargo weight greater than 0.",
- });
- }
- }
-
+ return (
+ !!data.cargoWeight && !Number.isNaN(cargoWeight) && cargoWeight > 0
+ );
+ },
+ { message: "Enter a cargo weight greater than 0.", path: ["cargoWeight"] },
+ )
+ .refine(
+ (data) => !(data.cargoType === "container" && data.containers.length === 0),
+ { message: "Add at least one container.", path: ["containers"] },
+ )
+ .refine((data) => data.termsAccepted, {
+ message: "Accept the freight contract terms to submit.",
+ path: ["termsAccepted"],
+ })
+ .superRefine((data, ctx) => {
if (data.cargoType === "container") {
- if (data.containers.length === 0) {
- ctx.addIssue({
- code: "custom",
- path: ["containers"],
- message: "Add at least one container.",
- });
- }
-
data.containers.forEach((c, i) => {
if (!c.qty || +c.qty < 1) {
ctx.addIssue({
@@ -325,39 +246,26 @@ export const bookingFormSchema = z
}
});
}
-
- for (const key of REQUIRED_DOC_KEYS) {
- const value = data.documents[key];
- const hasFile = Array.isArray(value) ? value.length > 0 : Boolean(value);
- if (!hasFile) {
- ctx.addIssue({
- code: "custom",
- path: ["documents", key],
- message: "Upload this required document.",
- });
- }
- }
-
- if (!data.termsAccepted) {
- ctx.addIssue({
- code: "custom",
- path: ["termsAccepted"],
- message: "Accept the freight contract terms to submit.",
- });
- }
});
export type BookingFormValues = z.infer;
-export const initialBookingFormValues: Partial = {
+export const initialBookingFormValues: DeepPartial = {
previousContractRef: "",
- firstMileEnabled: false,
- pickUpAddress: "",
- lastMileEnabled: false,
- deliveryAddress: "",
+
+ firstMile: {
+ enabled: false,
+ pickUpAddress: "",
+ },
+ lastMile: {
+ enabled: false,
+ deliveryAddress: "",
+ },
equipmentReturn: "with_return",
+ customsClearingEnabled: false,
originYard: "",
destinationYard: "",
+ shippingLine: "",
cargoWeight: "",
bulkCommodity: "",
bulkCommodityOther: "",
@@ -365,25 +273,29 @@ export const initialBookingFormValues: Partial = {
breakBulkTypeOther: "",
isHazardous: false,
isRefrigerated: false,
- containers: [{ type: "20ft", qty: "1", vgm: "" }],
+ containers: [{ type: "20ft", containerType: "", qty: "1", vgm: "" }],
consolidationEnabled: false,
- documents: {},
notes: "",
termsAccepted: false,
};
-export const stepFields: Record> = {
+export const stepFields: Record>> = {
1: ["contractType", "previousContractRef"],
- 2: ["serviceType"],
- 3: [
- "firstMileEnabled",
- "pickUpAddress",
- "lastMileEnabled",
- "deliveryAddress",
+ 2: [
+ "serviceType",
+ "firstMile",
+ "lastMile",
"equipmentReturn",
+ "customsClearingEnabled",
],
- 4: ["originYard", "destinationYard", "isHazardous", "isRefrigerated"],
- 5: [
+ 3: [
+ "originYard",
+ "destinationYard",
+ "isHazardous",
+ "isRefrigerated",
+ "shippingLine",
+ ],
+ 4: [
"cargoType",
"cargoWeight",
"freightType",
@@ -392,16 +304,16 @@ export const stepFields: Record> = {
"breakBulkType",
"breakBulkTypeOther",
"containers",
+ "consolidationEnabled",
],
- 6: ["consolidationEnabled"],
- 7: ["documents"],
- 8: ["notes", "termsAccepted"],
+ 5: ["notes", "termsAccepted"],
};
export type RouteDirection = "import" | "export" | "domestic" | null;
export interface ContainerConfig {
type: "20ft" | "40ft";
+ containerType: string;
qty: string;
vgm: string;
}
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 6cfff801a..120aa48a3 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
@@ -22,18 +22,8 @@ import {
SelectValue,
} from "@edr/ui-common";
import type { BookingFormValues } from "./schema";
-import { REQUIRED_DOC_KEYS } from "./schema";
import { cn } from "@/lib/utils";
-export function getUploadedRequiredCount(
- documents: BookingFormValues["documents"],
-) {
- return REQUIRED_DOC_KEYS.filter((key) => {
- const file = documents[key];
- return Array.isArray(file) ? file.length > 0 : Boolean(file);
- }).length;
-}
-
export function OptionFieldError({ error }: { error?: { message?: string } }) {
return ;
}
@@ -160,6 +150,8 @@ export function SelectField({
);
}
+export { SelectItem };
+
export function SelectOptions({ options }: { options: readonly string[] }) {
return (
<>
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 e8e045e5c..62a66d0e1 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,22 +1,19 @@
import { Controller, type UseFormReturn } from "react-hook-form";
-import { FileText, Loader2, RefreshCw } from "lucide-react";
-import { Button, Field, FieldLabel, Input } from "@edr/ui-common";
-import { type BookingFormValues } from "./schema";
-import { AlertBox, OptionCard, OptionFieldError, StepHeader } from "./shared";
+import { FileText, RefreshCw } from "lucide-react";
+import { Field } from "@edr/ui-common";
+import { MOCK_VALID_CONTRACTS, type BookingFormValues } from "./schema";
+import {
+ AlertBox,
+ OptionCard,
+ OptionFieldError,
+ SelectField,
+ SelectItem,
+ StepHeader,
+} from "./shared";
type BookingForm = UseFormReturn;
-export function Step1ContractType({
- form,
- renewalValid,
- renewalValidating,
- onValidate,
-}: {
- form: BookingForm;
- renewalValid: boolean | null;
- renewalValidating: boolean;
- onValidate: () => void;
-}) {
+export function Step1ContractType({ form }: { form: BookingForm }) {
const contractType = form.watch("contractType");
const previousContractRef = form.watch("previousContractRef");
@@ -38,6 +35,7 @@ export function Step1ContractType({
onClick={() => {
field.onChange("new");
form.clearErrors(["contractType", "previousContractRef"]);
+ form.setValue("previousContractRef", "");
}}
>
@@ -45,7 +43,7 @@ export function Step1ContractType({
New Contract
- Blank contract form. A draft ID is auto-generated.
+ Create a new contract.
@@ -61,7 +59,7 @@ export function Step1ContractType({
- Enter a previous reference to auto-populate historical
+ Select a previous reference to auto-populate historical
parameters.
@@ -77,46 +75,26 @@ export function Step1ContractType({
name="previousContractRef"
control={form.control}
render={({ field, fieldState }) => (
-
)}
/>
- {renewalValid === true && (
+ {previousContractRef && (