mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 07:10:57 +00:00
customer registration
This commit is contained in:
@@ -32,6 +32,8 @@ import NewBookingPage from "./pages/bookings/NewBookingPage";
|
||||
import TrackingPage from "./pages/tracking/TrackingPage";
|
||||
import BillingPage from "./pages/billing/BillingPage";
|
||||
import { useEffect } from "react";
|
||||
import CustomerOnBoarding from "./pages/customers/on_boarding/TransportrOnBoarding";
|
||||
import CustomerOnboardingPage from "./pages/customers/on_boarding/CustomerOnboardingPage";
|
||||
|
||||
const sidebarItems: SidebarItem[] = [
|
||||
{ label: "Home", href: "/", icon: <Home /> },
|
||||
@@ -77,6 +79,8 @@ const App = () => {
|
||||
return <OnboardingPage />;
|
||||
}
|
||||
|
||||
return <CustomerOnboardingPage />
|
||||
|
||||
const displayName = user?.name?.en || user?.username || user?.email || "User";
|
||||
const userEmail = user?.email;
|
||||
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
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";
|
||||
import TransporterOnboarding from "./TransportrOnBoarding";
|
||||
import DjiboutiForwardingAgentForm from "./DjiboutiFreightForwardingAgent";
|
||||
import ImportExportOnBoarding from "./ImportExportOnBoarding";
|
||||
|
||||
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<typeof onboardingSchema>;
|
||||
|
||||
const stepFields: Record<OnboardingStep, (keyof FormData)[]> = {
|
||||
company: [
|
||||
"companyName",
|
||||
"companyEmail",
|
||||
"companyPhone",
|
||||
"companyPhoneCountryCode",
|
||||
"companyLocation",
|
||||
"companyAddress",
|
||||
"tinNumber",
|
||||
"vatNumber",
|
||||
"fanNumber",
|
||||
],
|
||||
personnel: [
|
||||
"contactPersonName",
|
||||
"contactPersonPhone",
|
||||
"contactPersonPhoneCountryCode",
|
||||
"generalManagerName",
|
||||
"generalManagerEmail",
|
||||
"generalManagerPhone",
|
||||
"generalManagerPhoneCountryCode",
|
||||
],
|
||||
poa: [],
|
||||
};
|
||||
|
||||
export default function CustomerOnboardingPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { user } = useAuth();
|
||||
const [step, setStep] = useState<OnboardingStep>("company");
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
trigger,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({
|
||||
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 (
|
||||
<AuthLayout
|
||||
left={{
|
||||
badge: "Complete Your Profile",
|
||||
title: "Set up your company profile",
|
||||
description:
|
||||
"Provide your business details to start using EDR Freight for managing shipments, tracking consignments, and streamlining logistics operations across Ethiopia and Djibouti.",
|
||||
features: [
|
||||
"Company registration details",
|
||||
"Contact and management personnel",
|
||||
"Power of Attorney (optional)",
|
||||
],
|
||||
stats: {
|
||||
label: "Active Customers",
|
||||
value: "500+",
|
||||
footer: "And growing",
|
||||
progress: "w-[95%]",
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<TransporterOnboarding />
|
||||
{/* <DjiboutiForwardingAgentForm /> */}
|
||||
{/* <ImportExportOnBoarding /> */}
|
||||
{/* <div className="mb-8 lg:col-span-2">
|
||||
<div className="flex items-center justify-between max-w-lg mx-auto relative px-2">
|
||||
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-border -translate-y-1/2 z-0" />
|
||||
<StepIcon
|
||||
icon={<Building2 className="size-5" />}
|
||||
active={step === "company"}
|
||||
completed={step !== "company"}
|
||||
/>
|
||||
<StepIcon
|
||||
icon={<User className="size-5" />}
|
||||
active={step === "personnel"}
|
||||
completed={step === "poa"}
|
||||
/>
|
||||
<StepIcon
|
||||
icon={<FileText className="size-5" />}
|
||||
active={step === "poa"}
|
||||
completed={false}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-center text-sm text-muted-foreground mt-3">
|
||||
{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)"}
|
||||
</p>
|
||||
</div> */}
|
||||
|
||||
{/* <form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<FieldGroup className="gap-4">
|
||||
{step === "company" && (
|
||||
<>
|
||||
<Field data-invalid={Boolean(errors.companyName)}>
|
||||
<FieldLabel>Company Name</FieldLabel>
|
||||
<Input
|
||||
placeholder="Global Logistics Ltd"
|
||||
aria-invalid={Boolean(errors.companyName)}
|
||||
{...register("companyName")}
|
||||
/>
|
||||
<FieldError errors={[errors.companyName]} />
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field data-invalid={Boolean(errors.companyEmail)}>
|
||||
<FieldLabel>Company Email</FieldLabel>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="ops@company.com"
|
||||
aria-invalid={Boolean(errors.companyEmail)}
|
||||
{...register("companyEmail")}
|
||||
/>
|
||||
<FieldError errors={[errors.companyEmail]} />
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
countryCode={{ ...register("companyPhoneCountryCode") }}
|
||||
phone={{
|
||||
...register("companyPhone"),
|
||||
placeholder: "912345678",
|
||||
}}
|
||||
countryCodeError={errors.companyPhoneCountryCode}
|
||||
phoneError={errors.companyPhone}
|
||||
label="Company Phone"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field data-invalid={Boolean(errors.companyLocation)}>
|
||||
<FieldLabel>Location</FieldLabel>
|
||||
<Input
|
||||
placeholder="Addis Ababa, Ethiopia"
|
||||
aria-invalid={Boolean(errors.companyLocation)}
|
||||
{...register("companyLocation")}
|
||||
/>
|
||||
<FieldError errors={[errors.companyLocation]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={Boolean(errors.companyAddress)}>
|
||||
<FieldLabel>Address</FieldLabel>
|
||||
<Input
|
||||
placeholder="Bole Subcity, Woreda 03"
|
||||
aria-invalid={Boolean(errors.companyAddress)}
|
||||
{...register("companyAddress")}
|
||||
/>
|
||||
<FieldError errors={[errors.companyAddress]} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field data-invalid={Boolean(errors.tinNumber)}>
|
||||
<FieldLabel>TIN Number (10 digits)</FieldLabel>
|
||||
<Input
|
||||
placeholder="1234567890"
|
||||
maxLength={10}
|
||||
aria-invalid={Boolean(errors.tinNumber)}
|
||||
{...register("tinNumber")}
|
||||
/>
|
||||
<FieldError errors={[errors.tinNumber]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={Boolean(errors.vatNumber)}>
|
||||
<FieldLabel>VAT Number</FieldLabel>
|
||||
<Input
|
||||
placeholder="VAT-12345"
|
||||
aria-invalid={Boolean(errors.vatNumber)}
|
||||
maxLength={10}
|
||||
{...register("vatNumber")}
|
||||
/>
|
||||
<FieldError errors={[errors.vatNumber]} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field data-invalid={Boolean(errors.fanNumber)}>
|
||||
<FieldLabel>FAN Number (16 digits)</FieldLabel>
|
||||
<Input
|
||||
placeholder="1234567890123456"
|
||||
maxLength={16}
|
||||
aria-invalid={Boolean(errors.fanNumber)}
|
||||
{...register("fanNumber")}
|
||||
/>
|
||||
<FieldError errors={[errors.fanNumber]} />
|
||||
</Field>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === "personnel" && (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Personal details are pulled from your account. Contact and
|
||||
management info is collected below.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-foreground mb-3">
|
||||
Contact Person
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field data-invalid={Boolean(errors.contactPersonName)}>
|
||||
<FieldLabel>Name</FieldLabel>
|
||||
<Input
|
||||
placeholder="Jane Smith"
|
||||
aria-invalid={Boolean(errors.contactPersonName)}
|
||||
{...register("contactPersonName")}
|
||||
/>
|
||||
<FieldError errors={[errors.contactPersonName]} />
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
countryCode={{
|
||||
...register("contactPersonPhoneCountryCode"),
|
||||
}}
|
||||
phone={{
|
||||
...register("contactPersonPhone"),
|
||||
placeholder: "912345678",
|
||||
}}
|
||||
countryCodeError={errors.contactPersonPhoneCountryCode}
|
||||
phoneError={errors.contactPersonPhone}
|
||||
label="Phone"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="border-border" />
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-foreground mb-3">
|
||||
General Manager
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
className="col-span-2"
|
||||
data-invalid={Boolean(errors.generalManagerName)}
|
||||
>
|
||||
<FieldLabel>Name</FieldLabel>
|
||||
<Input
|
||||
placeholder="Abebe Bikila"
|
||||
aria-invalid={Boolean(errors.generalManagerName)}
|
||||
{...register("generalManagerName")}
|
||||
/>
|
||||
<FieldError errors={[errors.generalManagerName]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={Boolean(errors.generalManagerEmail)}>
|
||||
<FieldLabel>Email</FieldLabel>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="gm@company.com"
|
||||
aria-invalid={Boolean(errors.generalManagerEmail)}
|
||||
{...register("generalManagerEmail")}
|
||||
/>
|
||||
<FieldError errors={[errors.generalManagerEmail]} />
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
countryCode={{
|
||||
...register("generalManagerPhoneCountryCode"),
|
||||
}}
|
||||
phone={{
|
||||
...register("generalManagerPhone"),
|
||||
placeholder: "912345678",
|
||||
}}
|
||||
countryCodeError={errors.generalManagerPhoneCountryCode}
|
||||
phoneError={errors.generalManagerPhone}
|
||||
label="Phone"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{step === "poa" && (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Power of Attorney details are optional. Skip if not applicable.
|
||||
</p>
|
||||
|
||||
<Field>
|
||||
<FieldLabel>PoA Name</FieldLabel>
|
||||
<Input
|
||||
placeholder="Authorized Representative Name"
|
||||
{...register("poaName")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel>PoA Email</FieldLabel>
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="poa@company.com"
|
||||
{...register("poaEmail")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
countryCode={{ ...register("poaPhoneCountryCode") }}
|
||||
phone={{ ...register("poaPhone"), placeholder: "912345678" }}
|
||||
label="PoA Phone"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel>PoA Location</FieldLabel>
|
||||
<Input
|
||||
placeholder="City, Country"
|
||||
{...register("poaLocation")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel>PoA Address</FieldLabel>
|
||||
<Input
|
||||
placeholder="Full Address"
|
||||
{...register("poaAddress")}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</FieldGroup>
|
||||
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={prevStep}
|
||||
disabled={step === "company"}
|
||||
>
|
||||
<ArrowLeft />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
disabled={createCustomerMutation.isPending}
|
||||
>
|
||||
{createCustomerMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="animate-spin" />
|
||||
Submitting...
|
||||
</>
|
||||
) : step === "poa" ? (
|
||||
"Complete Registration"
|
||||
) : (
|
||||
<>
|
||||
Next Step
|
||||
<ArrowRight />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form> */}
|
||||
</AuthLayout>
|
||||
);
|
||||
}
|
||||
|
||||
function StepIcon({
|
||||
icon,
|
||||
active,
|
||||
completed,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
active: boolean;
|
||||
completed: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`relative z-10 flex h-10 w-10 items-center justify-center rounded-full border-2 transition-all ${completed
|
||||
? "bg-primary border-primary text-primary-foreground"
|
||||
: active
|
||||
? "bg-background border-primary text-primary shadow-md"
|
||||
: "bg-background border-border text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{completed ? <CheckCircle2 className="size-5" /> : icon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,558 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Building2,
|
||||
User,
|
||||
CheckCircle2,
|
||||
} from "lucide-react";
|
||||
|
||||
import PhoneInput from "@/components/auth/PhoneInput";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Field,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
Input,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
type OnboardingStep =
|
||||
| "personal"
|
||||
| "company"
|
||||
| "representative";
|
||||
|
||||
const schema = z.object({
|
||||
// PERSONAL
|
||||
firstName: z.string().min(1, "First name is required"),
|
||||
|
||||
lastName: z.string().min(1, "Last name is required"),
|
||||
|
||||
email: z.string().email("Invalid email address"),
|
||||
|
||||
phoneNumber: z
|
||||
.string()
|
||||
.min(1, "Phone number is required"),
|
||||
|
||||
phoneCountryCode: z.string().min(1),
|
||||
|
||||
// COMPANY
|
||||
companyName: z
|
||||
.string()
|
||||
.min(1, "Company name is required"),
|
||||
|
||||
companyEmail: z
|
||||
.string()
|
||||
.email("Invalid company email"),
|
||||
|
||||
companyPhone: z
|
||||
.string()
|
||||
.min(1, "Company phone is required"),
|
||||
|
||||
companyPhoneCountryCode: z.string().min(1),
|
||||
|
||||
companyLocation: z
|
||||
.string()
|
||||
.min(1, "Company location is required"),
|
||||
|
||||
companyAddress: z
|
||||
.string()
|
||||
.min(1, "Company address is required"),
|
||||
|
||||
// REPRESENTATIVE
|
||||
representativeName: z
|
||||
.string()
|
||||
.min(1, "Representative name is required"),
|
||||
|
||||
representativeEmail: z
|
||||
.string()
|
||||
.email("Invalid representative email"),
|
||||
|
||||
representativePhone: z
|
||||
.string()
|
||||
.min(1, "Representative phone is required"),
|
||||
|
||||
representativePhoneCountryCode: z.string().min(1),
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
const stepFields: Record<
|
||||
OnboardingStep,
|
||||
(keyof FormData)[]
|
||||
> = {
|
||||
personal: [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"phoneNumber",
|
||||
"phoneCountryCode",
|
||||
],
|
||||
|
||||
company: [
|
||||
"companyName",
|
||||
"companyEmail",
|
||||
"companyPhone",
|
||||
"companyPhoneCountryCode",
|
||||
"companyLocation",
|
||||
"companyAddress",
|
||||
],
|
||||
|
||||
representative: [
|
||||
"representativeName",
|
||||
"representativeEmail",
|
||||
"representativePhone",
|
||||
"representativePhoneCountryCode",
|
||||
],
|
||||
};
|
||||
|
||||
export default function DjiboutiForwardingAgentForm() {
|
||||
const [step, setStep] =
|
||||
useState<OnboardingStep>("personal");
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
trigger,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
|
||||
defaultValues: {
|
||||
phoneCountryCode: "+253",
|
||||
companyPhoneCountryCode: "+253",
|
||||
representativePhoneCountryCode:
|
||||
"+253",
|
||||
},
|
||||
});
|
||||
|
||||
const nextStep = async () => {
|
||||
if (step === "representative") {
|
||||
handleSubmit(onSubmit)();
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = await trigger(
|
||||
stepFields[step]
|
||||
);
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
if (step === "personal") {
|
||||
setStep("company");
|
||||
} else {
|
||||
setStep("representative");
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (step === "company") {
|
||||
setStep("personal");
|
||||
} else if (
|
||||
step === "representative"
|
||||
) {
|
||||
setStep("company");
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (
|
||||
data: FormData
|
||||
) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* STEPPER */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between max-w-xl mx-auto relative px-2">
|
||||
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-border -translate-y-1/2 z-0" />
|
||||
|
||||
<StepIcon
|
||||
icon={<User className="size-5" />}
|
||||
active={step === "personal"}
|
||||
completed={
|
||||
step !== "personal"
|
||||
}
|
||||
/>
|
||||
|
||||
<StepIcon
|
||||
icon={
|
||||
<Building2 className="size-5" />
|
||||
}
|
||||
active={step === "company"}
|
||||
completed={
|
||||
step === "representative"
|
||||
}
|
||||
/>
|
||||
|
||||
<StepIcon
|
||||
icon={<User className="size-5" />}
|
||||
active={
|
||||
step === "representative"
|
||||
}
|
||||
completed={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground mt-3">
|
||||
{step === "personal" &&
|
||||
"Step 1 of 3 — Personal Information"}
|
||||
|
||||
{step === "company" &&
|
||||
"Step 2 of 3 — Company Information"}
|
||||
|
||||
{step === "representative" &&
|
||||
"Step 3 of 3 — Representative Information"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* FORM */}
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<FieldGroup className="gap-4">
|
||||
{/* PERSONAL */}
|
||||
{step === "personal" && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.firstName
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
First Name
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Ahmed"
|
||||
{...register("firstName")}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.firstName]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.lastName
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Last Name
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Ali"
|
||||
{...register("lastName")}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.lastName]}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.email
|
||||
)}
|
||||
>
|
||||
<FieldLabel>Email</FieldLabel>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="agent@company.com"
|
||||
{...register("email")}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.email]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
label="Phone Number"
|
||||
countryCode={{
|
||||
...register(
|
||||
"phoneCountryCode"
|
||||
),
|
||||
}}
|
||||
phone={{
|
||||
...register(
|
||||
"phoneNumber"
|
||||
),
|
||||
placeholder: "77123456",
|
||||
}}
|
||||
countryCodeError={
|
||||
errors.phoneCountryCode
|
||||
}
|
||||
phoneError={errors.phoneNumber}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* COMPANY */}
|
||||
{step === "company" && (
|
||||
<>
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.companyName
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Company Name
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Djibouti Freight Co."
|
||||
{...register("companyName")}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.companyName]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.companyEmail
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Company Email
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="ops@company.com"
|
||||
{...register(
|
||||
"companyEmail"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.companyEmail]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
label="Company Phone"
|
||||
countryCode={{
|
||||
...register(
|
||||
"companyPhoneCountryCode"
|
||||
),
|
||||
}}
|
||||
phone={{
|
||||
...register(
|
||||
"companyPhone"
|
||||
),
|
||||
placeholder: "77123456",
|
||||
}}
|
||||
countryCodeError={
|
||||
errors.companyPhoneCountryCode
|
||||
}
|
||||
phoneError={
|
||||
errors.companyPhone
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.companyLocation
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Company Location / Country
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Djibouti"
|
||||
{...register(
|
||||
"companyLocation"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[
|
||||
errors.companyLocation,
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.companyAddress
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Company Address
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Rue de Venise"
|
||||
{...register(
|
||||
"companyAddress"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[
|
||||
errors.companyAddress,
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* REPRESENTATIVE */}
|
||||
{step ===
|
||||
"representative" && (
|
||||
<>
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.representativeName
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Company Representative Person
|
||||
Name
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Mohamed Hassan"
|
||||
{...register(
|
||||
"representativeName"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[
|
||||
errors.representativeName,
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.representativeEmail
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Representative Email
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="rep@company.com"
|
||||
{...register(
|
||||
"representativeEmail"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[
|
||||
errors.representativeEmail,
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
label="Representative Phone"
|
||||
countryCode={{
|
||||
...register(
|
||||
"representativePhoneCountryCode"
|
||||
),
|
||||
}}
|
||||
phone={{
|
||||
...register(
|
||||
"representativePhone"
|
||||
),
|
||||
placeholder: "77123456",
|
||||
}}
|
||||
countryCodeError={
|
||||
errors.representativePhoneCountryCode
|
||||
}
|
||||
phoneError={
|
||||
errors.representativePhone
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</FieldGroup>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={prevStep}
|
||||
disabled={step === "personal"}
|
||||
>
|
||||
<ArrowLeft />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{step === "representative" ? (
|
||||
"Complete Registration"
|
||||
) : (
|
||||
<>
|
||||
Next Step
|
||||
<ArrowRight />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function StepIcon({
|
||||
icon,
|
||||
active,
|
||||
completed,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
active: boolean;
|
||||
completed: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`relative z-10 flex h-10 w-10 items-center justify-center rounded-full border-2 transition-all ${
|
||||
completed
|
||||
? "bg-primary border-primary text-primary-foreground"
|
||||
: active
|
||||
? "bg-background border-primary text-primary shadow-md"
|
||||
: "bg-background border-border text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{completed ? (
|
||||
<CheckCircle2 className="size-5" />
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,771 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Building2,
|
||||
User,
|
||||
FileText,
|
||||
CheckCircle2,
|
||||
} from "lucide-react";
|
||||
|
||||
import PhoneInput from "@/components/auth/PhoneInput";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Field,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
Input,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
type OnboardingStep =
|
||||
| "personal"
|
||||
| "company"
|
||||
| "personnel"
|
||||
| "poa";
|
||||
|
||||
const onboardingSchema = z.object({
|
||||
// PERSONAL
|
||||
firstName: z.string().min(1, "First name is required"),
|
||||
lastName: z.string().min(1, "Last name is required"),
|
||||
email: z.string().email("Invalid email address"),
|
||||
phoneNumber: z.string().min(1, "Phone number is required"),
|
||||
phoneCountryCode: z.string().min(1),
|
||||
|
||||
// COMPANY
|
||||
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),
|
||||
companyLocation: z.string().min(1, "Location is required"),
|
||||
companyAddress: z.string().min(1, "Address is required"),
|
||||
|
||||
// LEGAL
|
||||
tinNumber: z.string().regex(/^\d{10}$/, {
|
||||
message: "TIN must be exactly 10 digits",
|
||||
}),
|
||||
|
||||
vatNumber: z.string().min(1, "VAT number is required"),
|
||||
|
||||
fanNumber: z.string().regex(/^\d{16}$/, {
|
||||
message: "FAN must be exactly 16 digits",
|
||||
}),
|
||||
|
||||
// CONTACT PERSON
|
||||
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),
|
||||
|
||||
// GENERAL MANAGER
|
||||
generalManagerName: z
|
||||
.string()
|
||||
.min(1, "General manager name is required"),
|
||||
|
||||
generalManagerEmail: z
|
||||
.string()
|
||||
.email("Invalid email"),
|
||||
|
||||
generalManagerPhone: z
|
||||
.string()
|
||||
.min(1, "General manager phone is required"),
|
||||
|
||||
generalManagerPhoneCountryCode: z.string().min(1),
|
||||
|
||||
// OPTIONAL POA
|
||||
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<typeof onboardingSchema>;
|
||||
|
||||
const stepFields: Record<
|
||||
OnboardingStep,
|
||||
(keyof FormData)[]
|
||||
> = {
|
||||
personal: [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"phoneNumber",
|
||||
"phoneCountryCode",
|
||||
],
|
||||
|
||||
company: [
|
||||
"companyName",
|
||||
"companyEmail",
|
||||
"companyPhone",
|
||||
"companyPhoneCountryCode",
|
||||
"companyLocation",
|
||||
"companyAddress",
|
||||
"tinNumber",
|
||||
"vatNumber",
|
||||
"fanNumber",
|
||||
],
|
||||
|
||||
personnel: [
|
||||
"contactPersonName",
|
||||
"contactPersonPhone",
|
||||
"contactPersonPhoneCountryCode",
|
||||
"generalManagerName",
|
||||
"generalManagerEmail",
|
||||
"generalManagerPhone",
|
||||
"generalManagerPhoneCountryCode",
|
||||
],
|
||||
|
||||
poa: [],
|
||||
};
|
||||
|
||||
export default function ImportExportOnBoarding() {
|
||||
const [step, setStep] =
|
||||
useState<OnboardingStep>("personal");
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
trigger,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(onboardingSchema),
|
||||
|
||||
defaultValues: {
|
||||
phoneCountryCode: "+251",
|
||||
companyPhoneCountryCode: "+251",
|
||||
contactPersonPhoneCountryCode: "+251",
|
||||
generalManagerPhoneCountryCode: "+251",
|
||||
poaPhoneCountryCode: "+251",
|
||||
},
|
||||
});
|
||||
|
||||
const nextStep = async () => {
|
||||
if (step === "poa") {
|
||||
handleSubmit(onSubmit)();
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = await trigger(stepFields[step]);
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
if (step === "personal") {
|
||||
setStep("company");
|
||||
} else if (step === "company") {
|
||||
setStep("personnel");
|
||||
} else {
|
||||
setStep("poa");
|
||||
}
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (step === "company") {
|
||||
setStep("personal");
|
||||
} else if (step === "personnel") {
|
||||
setStep("company");
|
||||
} else if (step === "poa") {
|
||||
setStep("personnel");
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (data: FormData) => {
|
||||
console.log(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* STEP HEADER */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between max-w-2xl mx-auto relative px-2">
|
||||
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-border -translate-y-1/2 z-0" />
|
||||
|
||||
<StepIcon
|
||||
icon={<User className="size-5" />}
|
||||
active={step === "personal"}
|
||||
completed={
|
||||
step !== "personal"
|
||||
}
|
||||
/>
|
||||
|
||||
<StepIcon
|
||||
icon={<Building2 className="size-5" />}
|
||||
active={step === "company"}
|
||||
completed={
|
||||
step === "personnel" ||
|
||||
step === "poa"
|
||||
}
|
||||
/>
|
||||
|
||||
<StepIcon
|
||||
icon={<User className="size-5" />}
|
||||
active={step === "personnel"}
|
||||
completed={step === "poa"}
|
||||
/>
|
||||
|
||||
<StepIcon
|
||||
icon={<FileText className="size-5" />}
|
||||
active={step === "poa"}
|
||||
completed={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground mt-3">
|
||||
{step === "personal" &&
|
||||
"Step 1 of 4 — Personal Information"}
|
||||
|
||||
{step === "company" &&
|
||||
"Step 2 of 4 — Company Information"}
|
||||
|
||||
{step === "personnel" &&
|
||||
"Step 3 of 4 — Personnel Information"}
|
||||
|
||||
{step === "poa" &&
|
||||
"Step 4 of 4 — Power of Attorney"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex flex-col gap-4"
|
||||
>
|
||||
<FieldGroup className="gap-4">
|
||||
{/* PERSONAL */}
|
||||
{step === "personal" && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.firstName
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
First Name
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="John"
|
||||
{...register("firstName")}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.firstName]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.lastName
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Last Name
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Doe"
|
||||
{...register("lastName")}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.lastName]}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.email
|
||||
)}
|
||||
>
|
||||
<FieldLabel>Email</FieldLabel>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="john@example.com"
|
||||
{...register("email")}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.email]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
label="Phone Number"
|
||||
countryCode={{
|
||||
...register(
|
||||
"phoneCountryCode"
|
||||
),
|
||||
}}
|
||||
phone={{
|
||||
...register(
|
||||
"phoneNumber"
|
||||
),
|
||||
placeholder: "912345678",
|
||||
}}
|
||||
countryCodeError={
|
||||
errors.phoneCountryCode
|
||||
}
|
||||
phoneError={errors.phoneNumber}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* COMPANY */}
|
||||
{step === "company" && (
|
||||
<>
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.companyName
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Company Name
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Global Logistics Ltd"
|
||||
{...register("companyName")}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.companyName]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.companyEmail
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Company Email
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="ops@company.com"
|
||||
{...register(
|
||||
"companyEmail"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.companyEmail]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
label="Company Phone"
|
||||
countryCode={{
|
||||
...register(
|
||||
"companyPhoneCountryCode"
|
||||
),
|
||||
}}
|
||||
phone={{
|
||||
...register(
|
||||
"companyPhone"
|
||||
),
|
||||
placeholder: "912345678",
|
||||
}}
|
||||
countryCodeError={
|
||||
errors.companyPhoneCountryCode
|
||||
}
|
||||
phoneError={
|
||||
errors.companyPhone
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.companyLocation
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Company Location
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Addis Ababa"
|
||||
{...register(
|
||||
"companyLocation"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[
|
||||
errors.companyLocation,
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.companyAddress
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Company Address
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Bole, Woreda 03"
|
||||
{...register(
|
||||
"companyAddress"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[
|
||||
errors.companyAddress,
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.tinNumber
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
TIN Number
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
maxLength={10}
|
||||
placeholder="1234567890"
|
||||
{...register("tinNumber")}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.tinNumber]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.vatNumber
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
VAT Number
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="VAT123456"
|
||||
{...register("vatNumber")}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.vatNumber]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.fanNumber
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
FAN Number
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
maxLength={16}
|
||||
placeholder="1234567890123456"
|
||||
{...register("fanNumber")}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[errors.fanNumber]}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* PERSONNEL */}
|
||||
{step === "personnel" && (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-foreground mb-3">
|
||||
Contact Person
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.contactPersonName
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
Contact Person Name
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Jane Smith"
|
||||
{...register(
|
||||
"contactPersonName"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[
|
||||
errors.contactPersonName,
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
label="Contact Person Phone"
|
||||
countryCode={{
|
||||
...register(
|
||||
"contactPersonPhoneCountryCode"
|
||||
),
|
||||
}}
|
||||
phone={{
|
||||
...register(
|
||||
"contactPersonPhone"
|
||||
),
|
||||
placeholder: "912345678",
|
||||
}}
|
||||
countryCodeError={
|
||||
errors.contactPersonPhoneCountryCode
|
||||
}
|
||||
phoneError={
|
||||
errors.contactPersonPhone
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr className="border-border" />
|
||||
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-foreground mb-3">
|
||||
General Manager
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field
|
||||
className="col-span-2"
|
||||
data-invalid={Boolean(
|
||||
errors.generalManagerName
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
General Manager Name
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Abebe Bikila"
|
||||
{...register(
|
||||
"generalManagerName"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[
|
||||
errors.generalManagerName,
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
data-invalid={Boolean(
|
||||
errors.generalManagerEmail
|
||||
)}
|
||||
>
|
||||
<FieldLabel>
|
||||
General Manager Email
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="gm@company.com"
|
||||
{...register(
|
||||
"generalManagerEmail"
|
||||
)}
|
||||
/>
|
||||
|
||||
<FieldError
|
||||
errors={[
|
||||
errors.generalManagerEmail,
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
label="General Manager Phone"
|
||||
countryCode={{
|
||||
...register(
|
||||
"generalManagerPhoneCountryCode"
|
||||
),
|
||||
}}
|
||||
phone={{
|
||||
...register(
|
||||
"generalManagerPhone"
|
||||
),
|
||||
placeholder: "912345678",
|
||||
}}
|
||||
countryCodeError={
|
||||
errors.generalManagerPhoneCountryCode
|
||||
}
|
||||
phoneError={
|
||||
errors.generalManagerPhone
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* POA */}
|
||||
{step === "poa" && (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Power of Attorney details are
|
||||
optional.
|
||||
</p>
|
||||
|
||||
<Field>
|
||||
<FieldLabel>PoA Name</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Authorized Representative"
|
||||
{...register("poaName")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel>
|
||||
PoA Email
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="poa@company.com"
|
||||
{...register("poaEmail")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
label="PoA Phone"
|
||||
countryCode={{
|
||||
...register(
|
||||
"poaPhoneCountryCode"
|
||||
),
|
||||
}}
|
||||
phone={{
|
||||
...register("poaPhone"),
|
||||
placeholder: "912345678",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel>
|
||||
PoA Location
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="City, Country"
|
||||
{...register("poaLocation")}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel>
|
||||
PoA Address
|
||||
</FieldLabel>
|
||||
|
||||
<Input
|
||||
placeholder="Full Address"
|
||||
{...register("poaAddress")}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</FieldGroup>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={prevStep}
|
||||
disabled={step === "personal"}
|
||||
>
|
||||
<ArrowLeft />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{step === "poa" ? (
|
||||
"Complete Registration"
|
||||
) : (
|
||||
<>
|
||||
Next Step
|
||||
<ArrowRight />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function StepIcon({
|
||||
icon,
|
||||
active,
|
||||
completed,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
active: boolean;
|
||||
completed: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`relative z-10 flex h-10 w-10 items-center justify-center rounded-full border-2 transition-all ${
|
||||
completed
|
||||
? "bg-primary border-primary text-primary-foreground"
|
||||
: active
|
||||
? "bg-background border-primary text-primary shadow-md"
|
||||
: "bg-background border-border text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{completed ? (
|
||||
<CheckCircle2 className="size-5" />
|
||||
) : (
|
||||
icon
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
User,
|
||||
Truck,
|
||||
CheckCircle2,
|
||||
} from "lucide-react";
|
||||
|
||||
import PhoneInput from "@/components/auth/PhoneInput";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Field,
|
||||
FieldError,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
Input,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
type Step = "personal" | "transport";
|
||||
|
||||
const schema = z.object({
|
||||
// PERSONAL
|
||||
firstName: z.string().min(1),
|
||||
lastName: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
phoneNumber: z.string().min(1),
|
||||
phoneCountryCode: z.string().min(1),
|
||||
|
||||
// TRANSPORT
|
||||
fanNumber: z.string().min(1),
|
||||
tinNumber: z.string().min(1),
|
||||
|
||||
truckType: z.enum([
|
||||
"Casoni",
|
||||
"Truck Trailer",
|
||||
"High Bed",
|
||||
"Low Bed",
|
||||
"Others",
|
||||
]),
|
||||
|
||||
plateNumber: z.string().min(1),
|
||||
|
||||
plateNumber2: z.string().optional(),
|
||||
|
||||
vehicleModel: z.string().min(1),
|
||||
yearOfManufacturing: z.string().min(1),
|
||||
});
|
||||
|
||||
type FormData = z.infer<typeof schema>;
|
||||
|
||||
const stepFields: Record<Step, (keyof FormData)[]> = {
|
||||
personal: [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"phoneNumber",
|
||||
"phoneCountryCode",
|
||||
],
|
||||
transport: [
|
||||
"fanNumber",
|
||||
"tinNumber",
|
||||
"truckType",
|
||||
"plateNumber",
|
||||
"plateNumber2",
|
||||
"vehicleModel",
|
||||
"yearOfManufacturing",
|
||||
],
|
||||
};
|
||||
|
||||
export default function TransporterOnboarding() {
|
||||
const [step, setStep] = useState<Step>("personal");
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
trigger,
|
||||
watch,
|
||||
formState: { errors, isSubmitting },
|
||||
} = useForm<FormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: {
|
||||
phoneCountryCode: "+251",
|
||||
},
|
||||
});
|
||||
|
||||
const truckType = watch("truckType");
|
||||
|
||||
const nextStep = async () => {
|
||||
const valid = await trigger(stepFields[step]);
|
||||
if (!valid) return;
|
||||
|
||||
if (step === "personal") setStep("transport");
|
||||
else handleSubmit(onSubmit)();
|
||||
};
|
||||
|
||||
const prevStep = () => {
|
||||
if (step === "transport") setStep("personal");
|
||||
};
|
||||
|
||||
const onSubmit = (data: FormData) => {
|
||||
console.log("TRANSPORTER:", data);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* STEPPER */}
|
||||
<div className="mb-8 lg:col-span-2">
|
||||
<div className="flex items-center justify-between max-w-lg mx-auto relative px-2">
|
||||
<div className="absolute top-1/2 left-0 w-full h-0.5 bg-border -translate-y-1/2 z-0" />
|
||||
|
||||
<StepIcon
|
||||
icon={<User className="size-5" />}
|
||||
active={step === "personal"}
|
||||
completed={step !== "personal"}
|
||||
/>
|
||||
|
||||
<StepIcon
|
||||
icon={<Truck className="size-5" />}
|
||||
active={step === "transport"}
|
||||
completed={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-center text-sm text-muted-foreground mt-3">
|
||||
{step === "personal" && "Step 1 of 2 — Personal Information"}
|
||||
{step === "transport" && "Step 2 of 2 — Transport Information"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* FORM */}
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="flex flex-col gap-4">
|
||||
<FieldGroup className="gap-4">
|
||||
|
||||
{/* PERSONAL */}
|
||||
{step === "personal" && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field data-invalid={!!errors.firstName}>
|
||||
<FieldLabel>First Name</FieldLabel>
|
||||
<Input {...register("firstName")} />
|
||||
<FieldError errors={[errors.firstName]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.lastName}>
|
||||
<FieldLabel>Last Name</FieldLabel>
|
||||
<Input {...register("lastName")} />
|
||||
<FieldError errors={[errors.lastName]} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field data-invalid={!!errors.email}>
|
||||
<FieldLabel>Email</FieldLabel>
|
||||
<Input type="email" {...register("email")} />
|
||||
<FieldError errors={[errors.email]} />
|
||||
</Field>
|
||||
|
||||
<PhoneInput
|
||||
label="Phone Number"
|
||||
countryCode={{ ...register("phoneCountryCode") }}
|
||||
phone={{ ...register("phoneNumber") }}
|
||||
countryCodeError={errors.phoneCountryCode}
|
||||
phoneError={errors.phoneNumber}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* TRANSPORT */}
|
||||
{step === "transport" && (
|
||||
<>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field data-invalid={!!errors.fanNumber}>
|
||||
<FieldLabel>FAN Number</FieldLabel>
|
||||
<Input {...register("fanNumber")} />
|
||||
<FieldError errors={[errors.fanNumber]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.tinNumber}>
|
||||
<FieldLabel>TIN Number</FieldLabel>
|
||||
<Input {...register("tinNumber")} />
|
||||
<FieldError errors={[errors.tinNumber]} />
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
<Field data-invalid={!!errors.truckType}>
|
||||
<FieldLabel>Truck Type</FieldLabel>
|
||||
<select
|
||||
className="w-full border rounded-md p-2 bg-background"
|
||||
{...register("truckType")}
|
||||
>
|
||||
<option value="">Select type</option>
|
||||
<option value="Casoni">Casoni</option>
|
||||
<option value="Truck Trailer">Truck Trailer</option>
|
||||
<option value="High Bed">High Bed</option>
|
||||
<option value="Low Bed">Low Bed</option>
|
||||
<option value="Others">Others</option>
|
||||
</select>
|
||||
<FieldError errors={[errors.truckType]} />
|
||||
</Field>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field data-invalid={!!errors.plateNumber}>
|
||||
<FieldLabel>Plate Number</FieldLabel>
|
||||
<Input {...register("plateNumber")} />
|
||||
<FieldError errors={[errors.plateNumber]} />
|
||||
</Field>
|
||||
|
||||
{truckType === "Casoni" && (
|
||||
<Field data-invalid={!!errors.plateNumber2}>
|
||||
<FieldLabel>Second Plate Number (Casoni)</FieldLabel>
|
||||
<Input {...register("plateNumber2")} />
|
||||
<FieldError errors={[errors.plateNumber2]} />
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field data-invalid={!!errors.vehicleModel}>
|
||||
<FieldLabel>Vehicle Model</FieldLabel>
|
||||
<Input {...register("vehicleModel")} />
|
||||
<FieldError errors={[errors.vehicleModel]} />
|
||||
</Field>
|
||||
|
||||
<Field data-invalid={!!errors.yearOfManufacturing}>
|
||||
<FieldLabel>Year of Manufacturing</FieldLabel>
|
||||
<Input {...register("yearOfManufacturing")} />
|
||||
<FieldError errors={[errors.yearOfManufacturing]} />
|
||||
</Field>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
</FieldGroup>
|
||||
|
||||
{/* FOOTER */}
|
||||
<div className="flex items-center justify-between pt-2">
|
||||
<Button type="button" variant="outline" onClick={prevStep} disabled={step === "personal"}>
|
||||
<ArrowLeft />
|
||||
Back
|
||||
</Button>
|
||||
|
||||
<Button type="button" onClick={nextStep} disabled={isSubmitting}>
|
||||
{step === "transport" ? (
|
||||
"Complete Registration"
|
||||
) : (
|
||||
<>
|
||||
Next Step
|
||||
<ArrowRight />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function StepIcon({
|
||||
icon,
|
||||
active,
|
||||
completed,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
active: boolean;
|
||||
completed: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`relative z-10 flex h-10 w-10 items-center justify-center rounded-full border-2 transition-all ${
|
||||
completed
|
||||
? "bg-primary border-primary text-primary-foreground"
|
||||
: active
|
||||
? "bg-background border-primary text-primary shadow-md"
|
||||
: "bg-background border-border text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{completed ? <CheckCircle2 className="size-5" /> : icon}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user