diff --git a/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx
new file mode 100644
index 000000000..cea9d1774
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx
@@ -0,0 +1,520 @@
+import { useState } from "react";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import {
+ ArrowRight,
+ ArrowLeft,
+ Building2,
+ User,
+ FileText,
+ CheckCircle2,
+ Loader2,
+} from "lucide-react";
+import useAuth from "@/hooks/useAuth";
+import { api } from "@/services/api";
+import type { CreateCustomerDto } from "@/types/customers";
+import AuthLayout from "@/components/auth/AuthLayout";
+import PhoneInput from "@/components/auth/PhoneInput";
+import {
+ Button,
+ Input,
+ Field,
+ FieldLabel,
+ FieldError,
+ FieldGroup,
+} from "@edr/ui-common";
+
+type OnboardingStep = "company" | "personnel" | "poa";
+
+const onboardingSchema = z.object({
+ companyName: z.string().min(1, "Company name is required"),
+ companyEmail: z.string().email("Invalid email address"),
+ companyPhone: z.string().min(1, "Company phone is required"),
+ companyPhoneCountryCode: z.string().min(1, "Country code is required"),
+ companyLocation: z.string().min(1, "Location is required"),
+ companyAddress: z.string().min(1, "Address is required"),
+ tinNumber: z.string().length(10, "TIN must be exactly 10 digits"),
+ vatNumber: z
+ .string()
+ .min(1, "VAT number is required")
+ .length(10, "VAT number must be exactly 10 digits"),
+ fanNumber: z.string().length(16, "FAN must be exactly 16 digits"),
+ contactPersonName: z.string().min(1, "Contact person name is required"),
+ contactPersonPhone: z.string().min(1, "Contact person phone is required"),
+ contactPersonPhoneCountryCode: z.string().min(1, "Country code is required"),
+ generalManagerName: z.string().min(1, "GM name is required"),
+ generalManagerEmail: z.string().email("Invalid GM email"),
+ generalManagerPhone: z.string().min(1, "GM phone is required"),
+ generalManagerPhoneCountryCode: z.string().min(1, "Country code is required"),
+ poaName: z.string().optional(),
+ poaPhone: z.string().optional(),
+ poaPhoneCountryCode: z.string().optional(),
+ poaAddress: z.string().optional(),
+ poaEmail: z.string().optional(),
+ poaLocation: z.string().optional(),
+});
+
+type FormData = z.infer
;
+
+const stepFields: Record = {
+ company: [
+ "companyName",
+ "companyEmail",
+ "companyPhone",
+ "companyPhoneCountryCode",
+ "companyLocation",
+ "companyAddress",
+ "tinNumber",
+ "vatNumber",
+ "fanNumber",
+ ],
+ personnel: [
+ "contactPersonName",
+ "contactPersonPhone",
+ "contactPersonPhoneCountryCode",
+ "generalManagerName",
+ "generalManagerEmail",
+ "generalManagerPhone",
+ "generalManagerPhoneCountryCode",
+ ],
+ poa: [],
+};
+
+export default function OnboardingPage() {
+ const queryClient = useQueryClient();
+ const { user } = useAuth();
+ const [step, setStep] = useState("company");
+
+ const {
+ register,
+ handleSubmit,
+ trigger,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(onboardingSchema),
+ defaultValues: {
+ companyName: "",
+ companyEmail: "",
+ companyPhone: "",
+ companyPhoneCountryCode: "+251",
+ companyLocation: "",
+ companyAddress: "",
+ tinNumber: "",
+ vatNumber: "",
+ fanNumber: "",
+ contactPersonName: "",
+ contactPersonPhone: "",
+ contactPersonPhoneCountryCode: "+251",
+ generalManagerName: "",
+ generalManagerEmail: "",
+ generalManagerPhone: "",
+ generalManagerPhoneCountryCode: "+251",
+ poaName: "",
+ poaPhone: "",
+ poaPhoneCountryCode: "+251",
+ poaAddress: "",
+ poaEmail: "",
+ poaLocation: "",
+ },
+ });
+
+ const createCustomerMutation = useMutation({
+ mutationFn: (payload: CreateCustomerDto) =>
+ api.customers.create.call(payload),
+ onSuccess: () => {
+ if (user)
+ queryClient.invalidateQueries({
+ queryKey: api.customers.getByUserId.queryKey({ id: user.id }),
+ });
+ },
+ });
+
+ const nextStep = async () => {
+ if (step === "poa") {
+ handleSubmit(onSubmit)();
+ return;
+ }
+ const fields = stepFields[step];
+ const isValid = await trigger(fields);
+ if (!isValid) return;
+ setStep(step === "company" ? "personnel" : "poa");
+ };
+
+ const prevStep = () => {
+ if (step === "personnel") setStep("company");
+ else if (step === "poa") setStep("personnel");
+ };
+
+ const onSubmit = async (data: FormData) => {
+ const nameParts = (user?.name?.en ?? "").split(" ");
+ const payload: CreateCustomerDto = {
+ userId: user!.id,
+ firstName: nameParts[0] || "",
+ lastName: nameParts.slice(-1)[0] || "",
+ email: user!.email,
+ phone: user!.phoneNumber,
+ companyName: data.companyName,
+ companyEmail: data.companyEmail,
+ companyPhone: `${data.companyPhoneCountryCode}${data.companyPhone}`,
+ companyLocation: data.companyLocation,
+ companyAddress: data.companyAddress,
+ contactPersonName: data.contactPersonName,
+ contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`,
+ tinNumber: data.tinNumber,
+ vatNumber: data.vatNumber,
+ fanNumber: data.fanNumber,
+ generalManagerName: data.generalManagerName,
+ generalManagerEmail: data.generalManagerEmail,
+ generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`,
+ poaName: data.poaName || undefined,
+ poaPhone:
+ data.poaPhone && data.poaPhoneCountryCode
+ ? `${data.poaPhoneCountryCode}${data.poaPhone}`
+ : undefined,
+ poaAddress: data.poaAddress || undefined,
+ poaEmail: data.poaEmail || undefined,
+ poaLocation: data.poaLocation || undefined,
+ };
+ createCustomerMutation.mutate(payload);
+ };
+
+ return (
+
+
+
+
+
}
+ active={step === "company"}
+ completed={step !== "company"}
+ />
+
}
+ active={step === "personnel"}
+ completed={step === "poa"}
+ />
+
}
+ active={step === "poa"}
+ completed={false}
+ />
+
+
+ {step === "company" && "Step 1 of 3 — Company Information"}
+ {step === "personnel" && "Step 2 of 3 — Personnel Details"}
+ {step === "poa" && "Step 3 of 3 — Power of Attorney (Optional)"}
+
+
+
+
+
+ );
+}
+
+function StepIcon({
+ icon,
+ active,
+ completed,
+}: {
+ icon: React.ReactNode;
+ active: boolean;
+ completed: boolean;
+}) {
+ return (
+
+ {completed ? : icon}
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/services/customers.service.ts b/apps/edr-freight-web/portal/src/services/customers.service.ts
index 3781859fa..3d809709d 100644
--- a/apps/edr-freight-web/portal/src/services/customers.service.ts
+++ b/apps/edr-freight-web/portal/src/services/customers.service.ts
@@ -7,6 +7,7 @@ import type {
Customer,
UpdateCustomerDto,
} from "@/types/customers";
+import { isAxiosError } from "axios";
const BASE = URL_CONSTANTS.CUSTOMERS_API.BASE;
@@ -23,22 +24,26 @@ export const customersService = {
return unwrap(response.data);
},
- getByUserId: async (userId: string): Promise => {
- const response = await client.get>(
- URL_CONSTANTS.CUSTOMERS_API.BY_USER_ID(userId),
- );
+ getByUserId: async (userId: string): Promise => {
+ try {
+ const response = await client.get>(
+ URL_CONSTANTS.CUSTOMERS_API.BY_USER_ID(userId),
+ );
+ return unwrap(response.data);
+ } catch (e) {
+ if (isAxiosError(e) && e.response?.status === 404) {
+ return null;
+ }
+ throw e;
+ }
+ },
+
+ create: async (payload: CreateCustomerDto): Promise => {
+ const response = await client.post>(BASE, payload);
return unwrap(response.data);
},
- create: async (payload: any): Promise => {
- const response = await client.post>(BASE, payload);
- return unwrap(response.data);
- },
-
- update: async (
- id: string,
- payload: UpdateCustomerDto,
- ): Promise => {
+ update: async (id: string, payload: UpdateCustomerDto): Promise => {
const response = await client.patch>(
URL_CONSTANTS.CUSTOMERS_API.BY_ID(id),
payload,