diff --git a/apps/edr-freight-web/portal/src/components/auth/AuthLayout.tsx b/apps/edr-freight-web/portal/src/components/auth/AuthLayout.tsx
index f66df0afa..a86a4b522 100644
--- a/apps/edr-freight-web/portal/src/components/auth/AuthLayout.tsx
+++ b/apps/edr-freight-web/portal/src/components/auth/AuthLayout.tsx
@@ -1,7 +1,7 @@
-import type { ReactNode } from "react";
+import { cn } from "@/lib/utils";
import { Box, Group, Stack, Text, ThemeIcon, Title } from "@mantine/core";
import { ShieldCheck, Train } from "lucide-react";
-import { cn } from "@/lib/utils";
+import type { ReactNode } from "react";
export interface AuthLayoutProps {
children: ReactNode;
@@ -100,9 +100,9 @@ export default function AuthLayout({
contentClassName,
)}
>
-
+
{/* Mobile brand */}
-
+
;
interface PhoneInputProps {
disabled?: boolean;
- countryCode?: React.ComponentProps;
- phone?: React.ComponentProps;
+ countryCode?: InputPassthrough;
+ phone?: InputPassthrough;
countryCodeError?: { message?: string };
phoneError?: { message?: string };
label?: string;
@@ -17,27 +19,29 @@ export default function PhoneInput({
phoneError,
label = "Phone Number",
}: PhoneInputProps) {
+ const errorMsg = countryCodeError?.message ?? phoneError?.message;
return (
-
- {label}
-
-
+ {label}
+
+
-
-
-
-
+
+ {errorMsg && (
+ {errorMsg}
+ )}
+
);
}
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx
index e86c30132..80465a42f 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx
@@ -1,31 +1,25 @@
+import { Box, Button, Divider, Group, Loader, SimpleGrid, Stack, Text, TextInput, ThemeIcon } from "@mantine/core";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useQuery } from "@tanstack/react-query";
+import {
+ ArrowLeft,
+ ArrowRight,
+ Building2,
+ CheckCircle2,
+ ChevronLeft,
+ FileText,
+ Loader2,
+ UploadCloud,
+ User,
+} from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
-import { useQuery } from "@tanstack/react-query";
-import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
-import {
- ArrowRight,
- ArrowLeft,
- Building2,
- User,
- FileText,
- CheckCircle2,
- Loader2,
- ChevronLeft,
- UploadCloud,
-} from "lucide-react";
+
import type { AuthUser } from "@/types/auth";
import type { CreateCompanyPayload } from "@/services/companies.service";
import PhoneInput from "@/components/auth/PhoneInput";
-import {
- Button,
- Input,
- Field,
- FieldLabel,
- FieldError,
- FieldGroup,
- SmartFileInput,
-} from "@edr/ui-common";
+import { SmartFileInput } from "@edr/ui-common";
import { api } from "@/services/api";
type CompanyStep = "company" | "personnel" | "poa" | "documents" | "confirm";
@@ -38,10 +32,7 @@ const onboardingSchema = z.object({
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"),
+ 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"),
@@ -61,26 +52,8 @@ const onboardingSchema = z.object({
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",
- ],
+ company: ["companyName", "companyEmail", "companyPhone", "companyPhoneCountryCode", "companyLocation", "companyAddress", "tinNumber", "vatNumber", "fanNumber"],
+ personnel: ["contactPersonName", "contactPersonPhone", "contactPersonPhoneCountryCode", "generalManagerName", "generalManagerEmail", "generalManagerPhone", "generalManagerPhoneCountryCode"],
poa: [],
documents: [],
confirm: [],
@@ -103,10 +76,7 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
generalManagerEmail: data.generalManagerEmail,
generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`,
poaName: data.poaName || undefined,
- poaPhone:
- data.poaPhone && data.poaPhoneCountryCode
- ? `${data.poaPhoneCountryCode}${data.poaPhone}`
- : undefined,
+ poaPhone: data.poaPhone && data.poaPhoneCountryCode ? `${data.poaPhoneCountryCode}${data.poaPhone}` : undefined,
poaAddress: data.poaAddress || undefined,
poaEmail: data.poaEmail || undefined,
poaLocation: data.poaLocation || undefined,
@@ -125,59 +95,29 @@ export default function CompanyProfileForm({
}: {
documentSettingCode: string;
documentFiles?: Record;
- onDocumentFilesChange?: (
- files: Record,
- ) => void;
+ onDocumentFilesChange?: (files: Record) => void;
user: AuthUser;
onSubmit: (data: CreateCompanyPayload) => void;
isPending: boolean;
onBack: () => void;
}) {
const [step, setStep] = useState("company");
- const [internalFiles, setInternalFiles] = useState<
- Record
- >({});
+ const [internalFiles, setInternalFiles] = useState>({});
const documentFiles = controlledFiles ?? internalFiles;
const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles;
const { data: uploadSetting, isLoading: loadingDocuments } = useQuery(
- api.fileUploadSettings.getByCode.queryOptions({
- input: { code: documentSettingCode },
- refetchOnMount: false,
- }),
+ api.fileUploadSettings.getByCode.queryOptions({ input: { code: documentSettingCode }, refetchOnMount: false }),
);
- const {
- register,
- handleSubmit,
- trigger,
- watch,
- formState: { errors },
- } = useForm({
+ const { register, handleSubmit, trigger, watch, 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: "",
+ 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: "",
},
});
@@ -186,468 +126,299 @@ export default function CompanyProfileForm({
const totalSteps = 5;
const nextStep = async () => {
- if (step === "poa") {
- setStep("documents");
- return;
- }
- if (step === "documents") {
- setStep("confirm");
- return;
- }
- if (step === "confirm") {
- handleSubmit((data) => onSubmit(buildPayload(data, user)))();
- return;
- }
- const fields = stepFields[step];
- const isValid = await trigger(fields);
+ if (step === "poa") { setStep("documents"); return; }
+ if (step === "documents") { setStep("confirm"); return; }
+ if (step === "confirm") { handleSubmit((data) => onSubmit(buildPayload(data, user)))(); return; }
+ const isValid = await trigger(stepFields[step]);
if (!isValid) return;
setStep(step === "company" ? "personnel" : "poa");
};
const prevStep = () => {
- if (step === "company") {
- onBack();
- } else if (step === "personnel") {
- setStep("company");
- } else if (step === "poa") {
- setStep("personnel");
- } else if (step === "documents") {
- setStep("poa");
- } else {
- setStep("documents");
- }
+ if (step === "company") onBack();
+ else if (step === "personnel") setStep("company");
+ else if (step === "poa") setStep("personnel");
+ else if (step === "documents") setStep("poa");
+ else setStep("documents");
};
+ const STEPS: { key: CompanyStep; icon: React.ReactNode }[] = [
+ { key: "company", icon: },
+ { key: "personnel", icon: },
+ { key: "poa", icon: },
+ { key: "documents", icon: },
+ { key: "confirm", icon: },
+ ];
+
+ const STEP_LABELS: Record = {
+ company: `Step 1 of ${totalSteps} — Company Information`,
+ personnel: `Step 2 of ${totalSteps} — Personnel Details`,
+ poa: `Step 3 of ${totalSteps} — Power of Attorney (Optional)`,
+ documents: `Step 4 of ${totalSteps} — Upload Documents (Optional)`,
+ confirm: `Step 5 of ${totalSteps} — Review & Confirm`,
+ };
+
+ const stepOrder: CompanyStep[] = ["company", "personnel", "poa", "documents", "confirm"];
+ const currentIdx = stepOrder.indexOf(step);
+
return (
<>
-
-
+
-
-
-
}
- active={step === "company"}
- completed={step !== "company"}
- />
-
}
- active={step === "personnel"}
- completed={
- step === "poa" || step === "documents" || step === "confirm"
- }
- />
-
}
- active={step === "poa"}
- completed={step === "documents" || step === "confirm"}
- />
-
}
- active={step === "documents"}
- completed={step === "confirm"}
- />
-
}
- active={step === "confirm"}
- completed={false}
- />
-
-
- {step === "company" &&
- `Step 1 of ${totalSteps} — Company Information`}
- {step === "personnel" &&
- `Step 2 of ${totalSteps} — Personnel Details`}
- {step === "poa" &&
- `Step 3 of ${totalSteps} — Power of Attorney (Optional)`}
- {step === "documents" &&
- `Step 4 of ${totalSteps} — Upload Documents (Optional)`}
- {step === "confirm" && `Step 5 of ${totalSteps} — Review & Confirm`}
-
-
+
+
+ {STEPS.map(({ key, icon }, i) => {
+ const done = i < currentIdx;
+ const active = i === currentIdx;
+ return done || active ? (
+
+ {done ? : icon}
+
+ ) : (
+
+ {icon}
+
+ );
+ })}
+
-
+
+
)}
-
-
-
-
- {step === "company"
- ? "Change Type"
- : step === "confirm"
- ? "Back to Documents"
- : "Back"}
-
-
-
+
+ }>
+ {step === "company" ? "Change Type" : step === "confirm" ? "Back to Documents" : "Back"}
+
onSubmit(buildPayload(data, user))) : nextStep}
disabled={isPending || (step === "documents" && !hasDocuments && loadingDocuments)}
+ loading={isPending}
+ rightSection={!isPending && step !== "confirm" && step !== "documents" ? : undefined}
>
- {isPending ? (
- <>
-
- Submitting...
- >
- ) : step === "documents" ? (
- "Continue"
- ) : step === "confirm" ? (
- "Submit Registration"
- ) : (
- <>
- Next Step
-
- >
- )}
+ {step === "documents" ? "Continue" : step === "confirm" ? "Submit Registration" : "Next Step"}
-
-
+
+
>
);
@@ -655,36 +426,13 @@ export default function CompanyProfileForm({
function ReviewRow({ label, value }: { label: string; value?: string | null }) {
return (
-
-
+
+
{label}
-
-
+
+
{value?.trim() ? value : "Not provided"}
-
-
- );
-}
-
-function StepIcon({
- icon,
- active,
- completed,
-}: {
- icon: React.ReactNode;
- active: boolean;
- completed: boolean;
-}) {
- return (
-
- {completed ? : icon}
-
+
+
);
}
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/DjiboutiAgentForm.tsx b/apps/edr-freight-web/portal/src/pages/accounts/DjiboutiAgentForm.tsx
index 2730c878d..eb58b98c1 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/DjiboutiAgentForm.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/DjiboutiAgentForm.tsx
@@ -1,30 +1,23 @@
-import { useState } from "react";
-import { useForm } from "react-hook-form";
-import { useQuery } from "@tanstack/react-query";
+import { Box, Button, Group, Loader, SimpleGrid, Stack, Text, TextInput, ThemeIcon } from "@mantine/core";
import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
+import { useQuery } from "@tanstack/react-query";
import {
- ArrowRight,
ArrowLeft,
+ ArrowRight,
Building2,
- UserRound,
CheckCircle2,
- Loader2,
ChevronLeft,
UploadCloud,
+ UserRound,
} from "lucide-react";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
import type { AuthUser } from "@/types/auth";
import type { CreateCompanyPayload } from "@/services/companies.service";
import PhoneInput from "@/components/auth/PhoneInput";
-import {
- Button,
- Input,
- Field,
- FieldLabel,
- FieldError,
- FieldGroup,
- SmartFileInput,
-} from "@edr/ui-common";
+import { SmartFileInput } from "@edr/ui-common";
import { api } from "@/services/api";
type DjiboutiStep = "company" | "representative" | "documents" | "confirm";
@@ -45,20 +38,8 @@ const djiboutiSchema = z.object({
type FormData = z.infer;
const stepFields: Record = {
- company: [
- "companyName",
- "companyEmail",
- "companyPhone",
- "companyPhoneCountryCode",
- "companyLocation",
- "companyAddress",
- ],
- representative: [
- "repName",
- "repEmail",
- "repPhone",
- "repPhoneCountryCode",
- ],
+ company: ["companyName", "companyEmail", "companyPhone", "companyPhoneCountryCode", "companyLocation", "companyAddress"],
+ representative: ["repName", "repEmail", "repPhone", "repPhoneCountryCode"],
documents: [],
confirm: [],
};
@@ -92,47 +73,26 @@ export default function DjiboutiAgentForm({
}: {
documentSettingCode: string;
documentFiles?: Record;
- onDocumentFilesChange?: (
- files: Record,
- ) => void;
+ onDocumentFilesChange?: (files: Record) => void;
user: AuthUser;
onSubmit: (data: CreateCompanyPayload) => void;
isPending: boolean;
onBack: () => void;
}) {
const [step, setStep] = useState("company");
- const [internalFiles, setInternalFiles] = useState<
- Record
- >({});
+ const [internalFiles, setInternalFiles] = useState>({});
const documentFiles = controlledFiles ?? internalFiles;
const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles;
const { data: uploadSetting, isLoading: loadingDocuments } = useQuery(
- api.fileUploadSettings.getByCode.queryOptions({
- input: { code: documentSettingCode },
- refetchOnMount: false,
- }),
+ api.fileUploadSettings.getByCode.queryOptions({ input: { code: documentSettingCode }, refetchOnMount: false }),
);
- const {
- register,
- handleSubmit,
- trigger,
- watch,
- formState: { errors },
- } = useForm({
+ const { register, handleSubmit, trigger, watch, formState: { errors } } = useForm({
resolver: zodResolver(djiboutiSchema),
defaultValues: {
- companyName: "",
- companyEmail: "",
- companyPhone: "",
- companyPhoneCountryCode: "+253",
- companyLocation: "",
- companyAddress: "",
- repName: "",
- repEmail: "",
- repPhone: "",
- repPhoneCountryCode: "+253",
+ companyName: "", companyEmail: "", companyPhone: "", companyPhoneCountryCode: "+253",
+ companyLocation: "", companyAddress: "", repName: "", repEmail: "", repPhone: "", repPhoneCountryCode: "+253",
},
});
@@ -141,224 +101,178 @@ export default function DjiboutiAgentForm({
const totalSteps = 4;
const nextStep = async () => {
- if (step === "representative") {
- setStep("documents");
- return;
- }
- if (step === "documents") {
- setStep("confirm");
- return;
- }
- if (step === "confirm") {
- handleSubmit((data) => onSubmit(buildPayload(data, user)))();
- return;
- }
- const fields = stepFields[step];
- const isValid = await trigger(fields);
+ if (step === "representative") { setStep("documents"); return; }
+ if (step === "documents") { setStep("confirm"); return; }
+ if (step === "confirm") { handleSubmit((data) => onSubmit(buildPayload(data, user)))(); return; }
+ const isValid = await trigger(stepFields[step]);
if (!isValid) return;
setStep("representative");
};
- const skipDocuments = () => {
- setStep("confirm");
- };
+ const skipDocuments = () => setStep("confirm");
const prevStep = () => {
- if (step === "company") {
- onBack();
- } else if (step === "representative") {
- setStep("company");
- } else if (step === "documents") {
- setStep("representative");
- } else {
- setStep("documents");
- }
+ if (step === "company") onBack();
+ else if (step === "representative") setStep("company");
+ else if (step === "documents") setStep("representative");
+ else setStep("documents");
};
+ const STEPS: { key: DjiboutiStep; icon: React.ReactNode }[] = [
+ { key: "company", icon: },
+ { key: "representative", icon: },
+ { key: "documents", icon: },
+ { key: "confirm", icon: },
+ ];
+
+ const STEP_LABELS: Record = {
+ company: `Step 1 of ${totalSteps} — Company Information`,
+ representative: `Step 2 of ${totalSteps} — Representative Details`,
+ documents: `Step 3 of ${totalSteps} — Upload Documents (Optional)`,
+ confirm: `Step 4 of ${totalSteps} — Review & Confirm`,
+ };
+
+ const stepOrder: DjiboutiStep[] = ["company", "representative", "documents", "confirm"];
+ const currentIdx = stepOrder.indexOf(step);
+
return (
<>
-
-
+ }
onClick={prevStep}
- className="mb-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
-
Change account type
-
+
-
-
-
}
- active={step === "company"}
- completed={step !== "company"}
- />
-
}
- active={step === "representative"}
- completed={step === "documents" || step === "confirm"}
- />
-
}
- active={step === "documents"}
- completed={step === "confirm"}
- />
-
}
- active={step === "confirm"}
- completed={false}
- />
-
-
- {step === "company" && `Step 1 of ${totalSteps} — Company Information`}
- {step === "representative" && `Step 2 of ${totalSteps} — Representative Details`}
- {step === "documents" && `Step 3 of ${totalSteps} — Upload Documents (Optional)`}
- {step === "confirm" && `Step 4 of ${totalSteps} — Review & Confirm`}
-
-
+
+
+ {STEPS.map(({ key, icon }, i) => {
+ const done = i < currentIdx;
+ const active = i === currentIdx;
+ return done || active ? (
+
+ {done ? : icon}
+
+ ) : (
+
+ {icon}
+
+ );
+ })}
+
-
>
);
@@ -427,37 +314,13 @@ export default function DjiboutiAgentForm({
function ReviewRow({ label, value }: { label: string; value?: string | null }) {
return (
-
-
+
+
{label}
-
-
+
+
{value?.trim() ? value : "Not provided"}
-
-
- );
-}
-
-function StepIcon({
- icon,
- active,
- completed,
-}: {
- icon: React.ReactNode;
- active: boolean;
- completed: boolean;
-}) {
- return (
-
- {completed ? : icon}
-
+
+
);
}
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/ForwarderForm.tsx b/apps/edr-freight-web/portal/src/pages/accounts/ForwarderForm.tsx
index 418b8bcc4..71db345ec 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/ForwarderForm.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/ForwarderForm.tsx
@@ -1,31 +1,24 @@
+import { Box, Button, Divider, Group, Loader, SimpleGrid, Stack, Text, TextInput, ThemeIcon } from "@mantine/core";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useQuery } from "@tanstack/react-query";
+import {
+ ArrowLeft,
+ ArrowRight,
+ Building2,
+ CheckCircle2,
+ ChevronLeft,
+ FileText,
+ UploadCloud,
+ User,
+} from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
-import { useQuery } from "@tanstack/react-query";
-import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
-import {
- ArrowRight,
- ArrowLeft,
- Building2,
- User,
- FileText,
- CheckCircle2,
- Loader2,
- ChevronLeft,
- UploadCloud,
-} from "lucide-react";
+
import type { AuthUser } from "@/types/auth";
import type { CreateCompanyPayload } from "@/services/companies.service";
import PhoneInput from "@/components/auth/PhoneInput";
-import {
- Button,
- Input,
- Field,
- FieldLabel,
- FieldError,
- FieldGroup,
- SmartFileInput,
-} from "@edr/ui-common";
+import { SmartFileInput } from "@edr/ui-common";
import { api } from "@/services/api";
type ForwarderStep = "company" | "personnel" | "poa" | "documents" | "confirm";
@@ -38,10 +31,7 @@ const forwarderSchema = z.object({
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"),
+ 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"),
@@ -61,26 +51,8 @@ const forwarderSchema = z.object({
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",
- ],
+ company: ["companyName", "companyEmail", "companyPhone", "companyPhoneCountryCode", "companyLocation", "companyAddress", "tinNumber", "vatNumber", "fanNumber"],
+ personnel: ["contactPersonName", "contactPersonPhone", "contactPersonPhoneCountryCode", "generalManagerName", "generalManagerEmail", "generalManagerPhone", "generalManagerPhoneCountryCode"],
poa: [],
documents: [],
confirm: [],
@@ -103,10 +75,7 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
generalManagerEmail: data.generalManagerEmail,
generalManagerPhone: `${data.generalManagerPhoneCountryCode}${data.generalManagerPhone}`,
poaName: data.poaName || undefined,
- poaPhone:
- data.poaPhone && data.poaPhoneCountryCode
- ? `${data.poaPhoneCountryCode}${data.poaPhone}`
- : undefined,
+ poaPhone: data.poaPhone && data.poaPhoneCountryCode ? `${data.poaPhoneCountryCode}${data.poaPhone}` : undefined,
poaAddress: data.poaAddress || undefined,
poaEmail: data.poaEmail || undefined,
poaLocation: data.poaLocation || undefined,
@@ -125,59 +94,29 @@ export default function ForwarderForm({
}: {
documentSettingCode: string;
documentFiles?: Record;
- onDocumentFilesChange?: (
- files: Record,
- ) => void;
+ onDocumentFilesChange?: (files: Record) => void;
user: AuthUser;
onSubmit: (data: CreateCompanyPayload) => void;
isPending: boolean;
onBack: () => void;
}) {
const [step, setStep] = useState("company");
- const [internalFiles, setInternalFiles] = useState<
- Record
- >({});
+ const [internalFiles, setInternalFiles] = useState>({});
const documentFiles = controlledFiles ?? internalFiles;
const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles;
const { data: uploadSetting, isLoading: loadingDocuments } = useQuery(
- api.fileUploadSettings.getByCode.queryOptions({
- input: { code: documentSettingCode },
- refetchOnMount: false,
- }),
+ api.fileUploadSettings.getByCode.queryOptions({ input: { code: documentSettingCode }, refetchOnMount: false }),
);
- const {
- register,
- handleSubmit,
- trigger,
- watch,
- formState: { errors },
- } = useForm({
+ const { register, handleSubmit, trigger, watch, formState: { errors } } = useForm({
resolver: zodResolver(forwarderSchema),
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: "",
+ 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: "",
},
});
@@ -186,371 +125,265 @@ export default function ForwarderForm({
const totalSteps = 5;
const nextStep = async () => {
- if (step === "poa") {
- setStep("documents");
- return;
- }
- if (step === "documents") {
- setStep("confirm");
- return;
- }
- if (step === "confirm") {
- handleSubmit((data) => onSubmit(buildPayload(data, user)))();
- return;
- }
- const fields = stepFields[step];
- const isValid = await trigger(fields);
+ if (step === "poa") { setStep("documents"); return; }
+ if (step === "documents") { setStep("confirm"); return; }
+ if (step === "confirm") { handleSubmit((data) => onSubmit(buildPayload(data, user)))(); return; }
+ const isValid = await trigger(stepFields[step]);
if (!isValid) return;
setStep(step === "company" ? "personnel" : "poa");
};
- const skipDocuments = () => {
- setStep("confirm");
- };
+ const skipDocuments = () => setStep("confirm");
const prevStep = () => {
- if (step === "company") {
- onBack();
- } else if (step === "personnel") {
- setStep("company");
- } else if (step === "poa") {
- setStep("personnel");
- } else if (step === "documents") {
- setStep("poa");
- } else {
- setStep("documents");
- }
+ if (step === "company") onBack();
+ else if (step === "personnel") setStep("company");
+ else if (step === "poa") setStep("personnel");
+ else if (step === "documents") setStep("poa");
+ else setStep("documents");
};
+ const STEPS: { key: ForwarderStep; icon: React.ReactNode }[] = [
+ { key: "company", icon: },
+ { key: "personnel", icon: },
+ { key: "poa", icon: },
+ { key: "documents", icon: },
+ { key: "confirm", icon: },
+ ];
+
+ const STEP_LABELS: Record = {
+ company: `Step 1 of ${totalSteps} — Company Information`,
+ personnel: `Step 2 of ${totalSteps} — Personnel Details`,
+ poa: `Step 3 of ${totalSteps} — Power of Attorney (Optional)`,
+ documents: `Step 4 of ${totalSteps} — Upload Documents (Optional)`,
+ confirm: `Step 5 of ${totalSteps} — Review & Confirm`,
+ };
+
+ const stepOrder: ForwarderStep[] = ["company", "personnel", "poa", "documents", "confirm"];
+ const currentIdx = stepOrder.indexOf(step);
+
return (
<>
-
-
+ }
onClick={prevStep}
- className="mb-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
-
Change account type
-
+
-
-
-
}
- active={step === "company"}
- completed={step !== "company"}
- />
-
}
- active={step === "personnel"}
- completed={step === "poa" || step === "documents" || step === "confirm"}
- />
-
}
- active={step === "poa"}
- completed={step === "documents" || step === "confirm"}
- />
-
}
- active={step === "documents"}
- completed={step === "confirm"}
- />
-
}
- active={step === "confirm"}
- completed={false}
- />
-
-
- {step === "company" &&
- `Step 1 of ${totalSteps} — Company Information`}
- {step === "personnel" &&
- `Step 2 of ${totalSteps} — Personnel Details`}
- {step === "poa" &&
- `Step 3 of ${totalSteps} — Power of Attorney (Optional)`}
- {step === "documents" &&
- `Step 4 of ${totalSteps} — Upload Documents (Optional)`}
- {step === "confirm" && `Step 5 of ${totalSteps} — Review & Confirm`}
-
-
+
+
+ {STEPS.map(({ key, icon }, i) => {
+ const done = i < currentIdx;
+ const active = i === currentIdx;
+ return done || active ? (
+
+ {done ? : icon}
+
+ ) : (
+
+ {icon}
+
+ );
+ })}
+
-
>
);
@@ -656,36 +434,13 @@ export default function ForwarderForm({
function ReviewRow({ label, value }: { label: string; value?: string | null }) {
return (
-
-
+
+
{label}
-
-
+
+
{value?.trim() ? value : "Not provided"}
-
-
- );
-}
-
-function StepIcon({
- icon,
- active,
- completed,
-}: {
- icon: React.ReactNode;
- active: boolean;
- completed: boolean;
-}) {
- return (
-
- {completed ? : icon}
-
+
+
);
}
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx
index 4cfc64619..f8ba4ff45 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/LoginPage.tsx
@@ -1,22 +1,17 @@
+import { Alert, Box, Button, Group, PasswordInput, SegmentedControl, Stack, Text, TextInput } from "@mantine/core";
+import { ArrowRight, Mail, Phone } from "lucide-react";
import { useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { ArrowRight, Mail, Phone, Loader2 } from "lucide-react";
+import { useLocation, useNavigate } from "react-router-dom";
+
import useAuth from "@/hooks/useAuth";
import AuthLayout from "@/components/auth/AuthLayout";
-import {
- Button,
- Input,
- Field,
- FieldLabel,
- FieldError,
- FieldGroup,
-} from "@edr/ui-common";
import PhoneInput from "@/components/auth/PhoneInput";
type LoginMethod = "email" | "phone";
export default function LoginPage() {
const navigate = useNavigate();
+ const location = useLocation();
const { login } = useAuth();
const [method, setMethod] = useState("email");
const [identifier, setIdentifier] = useState("");
@@ -31,12 +26,15 @@ export default function LoginPage() {
setError(null);
setLoading(true);
try {
- const loginId = method === "email"
- ? identifier
- : `${countryCode}${phoneNumber.startsWith("0") ? phoneNumber.slice(1) : phoneNumber}`;
+ const loginId =
+ method === "email"
+ ? identifier
+ : `${countryCode}${phoneNumber.startsWith("0") ? phoneNumber.slice(1) : phoneNumber}`;
const result = await login({ email: loginId, password });
if (result.success) {
- navigate("/");
+ const from = (location.state as { from?: { pathname: string } } | null)
+ ?.from?.pathname;
+ navigate(from ?? "/portal", { replace: true });
} else {
setError(result.error.message);
}
@@ -60,130 +58,141 @@ export default function LoginPage() {
"Enterprise-grade operations",
"Multi-corridor freight monitoring",
],
- stats: {
- label: "Active Corridors",
- value: "24+",
- footer: "Operational",
- progress: "w-[95%]",
- },
+ stats: { label: "Active Corridors", value: "24+", footer: "Operational", progress: "w-[95%]" },
}}
>
-
-
-
-
-
Welcome back
-
- Enter your credentials to access your portal
-
-
+
+
+
+
+
+
+ Welcome back
+
+
+ Enter your credentials to access your portal
+
+
+
-
-
-
setMethod("email")}
- className={`flex-1 hover:bg-background/40! ${method === "email" ? "bg-background shadow border" : ""}`}
- >
-
- Email
-
-
setMethod("phone")}
- className={`flex-1 hover:bg-background/40! ${method === "phone" ? "bg-background shadow border" : ""}`}
- >
-
- Phone
-
-
+
+
+ setMethod(v as LoginMethod)}
+ fullWidth
+ radius="md"
+ data={[
+ {
+ label: (
+
+
+ Email
+
+ ),
+ value: "email",
+ },
+ {
+ label: (
+
+
+ Phone
+
+ ),
+ value: "phone",
+ },
+ ]}
+ />
-
{method === "email" ? (
-
- Email Address
- setIdentifier(e.target.value)}
- required
- disabled={loading}
- />
-
+ setIdentifier(e.target.value)}
+ required
+ disabled={loading}
+ />
) : (
) => setCountryCode(e.target.value),
+ onChange: (e: React.ChangeEvent) =>
+ setCountryCode(e.target.value),
}}
phone={{
value: phoneNumber,
- onChange: (e: React.ChangeEvent) => setPhoneNumber(e.target.value),
+ onChange: (e: React.ChangeEvent) =>
+ setPhoneNumber(e.target.value),
}}
/>
)}
-
-
- Password
+
+
+
+ Password
+
Forgot password?
-
-
+ setPassword(e.target.value)}
required
disabled={loading}
/>
-
-
+
- {error && (
-
- {error}
-
- )}
-
-
- {loading ? (
- <>
-
- Signing in...
- >
- ) : (
- <>
- Sign In
-
- >
+ {error && (
+
+ {error}
+
)}
-
-
- Don't have an account?{" "}
navigate("/signup")}
- className="h-auto p-0 font-semibold"
+ type="submit"
+ disabled={loading}
+ loading={loading}
+ size="lg"
+ color="edr-green"
+ fullWidth
+ rightSection={!loading ? : undefined}
>
- Create an account
+ Sign In
-
+
+
+ Don't have an account?{" "}
+ navigate("/signup")}
+ >
+ Create an account
+
+
+
);
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx
index ec1d724f8..939088973 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/OnboardingPage.tsx
@@ -1,21 +1,23 @@
-import { useState } from "react";
+import { Box, Group, SimpleGrid, Stack, Text, ThemeIcon, UnstyledButton } from "@mantine/core";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import {
ArrowDownToLine,
ArrowUpFromLine,
Building2,
+ ChevronRight,
Ship,
Truck,
- Check,
} from "lucide-react";
+import { useState } from "react";
+
+import AuthLayout from "@/components/auth/AuthLayout";
import useAuth from "@/hooks/useAuth";
import { api } from "@/services/api";
-import { companiesService } from "@/services/companies.service";
import type { CreateCompanyPayload } from "@/services/companies.service";
-import AuthLayout from "@/components/auth/AuthLayout";
+import { companiesService } from "@/services/companies.service";
import CompanyProfileForm from "./CompanyProfileForm";
-import ForwarderForm from "./ForwarderForm";
import DjiboutiAgentForm from "./DjiboutiAgentForm";
+import ForwarderForm from "./ForwarderForm";
import TransporterForm from "./TransporterForm";
import type { OnboardingUserType } from "./types";
@@ -25,45 +27,41 @@ const USER_TYPE_CARDS: {
description: string;
icon: React.ReactNode;
}[] = [
- {
- id: "importer",
- label: "Importer",
- description: "Import goods into Ethiopia via the railway corridor.",
- icon: ,
- },
- {
- id: "exporter",
- label: "Exporter",
- description: "Export goods from Ethiopia via rail.",
- icon: ,
- },
- {
- id: "freight-forwarder-et",
- label: "Freight Forwarder (Ethiopia)",
- description: "Ethiopian freight forwarding company handling client cargo.",
- icon: ,
- },
- {
- id: "freight-forwarder-dj",
- label: "FF Agent (Djibouti)",
- description: "Djibouti-based agent coordinating cross-border logistics.",
- icon: ,
- },
- {
- id: "transporter",
- label: "Transporter",
- description: "Trucking company providing first/last-mile services.",
- icon: ,
- },
- ];
+ {
+ id: "importer",
+ label: "Importer",
+ description: "Import goods into Ethiopia via the railway corridor.",
+ icon: ,
+ },
+ {
+ id: "exporter",
+ label: "Exporter",
+ description: "Export goods from Ethiopia via rail.",
+ icon: ,
+ },
+ {
+ id: "freight-forwarder-et",
+ label: "Freight Forwarder (Ethiopia)",
+ description: "Ethiopian freight forwarding company handling client cargo.",
+ icon: ,
+ },
+ {
+ id: "freight-forwarder-dj",
+ label: "FF Agent (Djibouti)",
+ description: "Djibouti-based agent coordinating cross-border logistics.",
+ icon: ,
+ },
+ {
+ id: "transporter",
+ label: "Transporter",
+ description: "Trucking company providing first/last-mile services.",
+ icon: ,
+ },
+];
const USER_TYPE_LEFT_MAP: Record<
OnboardingUserType,
- {
- badge: string;
- title: string;
- description: string;
- }
+ { badge: string; title: string; description: string }
> = {
importer: {
badge: "Importer Registration",
@@ -107,12 +105,7 @@ const PREFLIGHT_LEFT = {
"Freight Forwarders (Ethiopia & Djibouti)",
"Transporters & Fleet Operators",
],
- stats: {
- label: "Active Customers",
- value: "500+",
- footer: "And growing",
- progress: "w-[95%]",
- },
+ stats: { label: "Active Customers", value: "500+", footer: "And growing", progress: "w-[95%]" },
};
const DOCUMENT_SETTING_CODE_MAP: Record = {
@@ -127,9 +120,7 @@ export default function OnboardingPage() {
const queryClient = useQueryClient();
const { user } = useAuth();
const [userType, setUserType] = useState(null);
- const [documentFiles, setDocumentFiles] = useState<
- Record
- >({});
+ const [documentFiles, setDocumentFiles] = useState>({});
const COMPANY_TYPE_MAP: Record = {
importer: "customer",
@@ -140,8 +131,7 @@ export default function OnboardingPage() {
};
const createCompanyMutation = useMutation({
- mutationFn: (payload: CreateCompanyPayload) =>
- api.companies.create.call(payload),
+ mutationFn: (payload: CreateCompanyPayload) => api.companies.create.call(payload),
onSuccess: async (data) => {
const hasFiles = Object.values(documentFiles).some(
(f) => f !== null && (Array.isArray(f) ? f.length > 0 : true),
@@ -149,100 +139,81 @@ export default function OnboardingPage() {
if (hasFiles) {
await companiesService.uploadDocuments(data.company.id, documentFiles);
}
- await queryClient.invalidateQueries({
- queryKey: api.companies.getInfo.queryKey(),
- });
+ await queryClient.invalidateQueries({ queryKey: api.companies.getInfo.queryKey() });
},
});
if (!user) return null;
const handleSubmit = (payload: CreateCompanyPayload) => {
- const enriched: CreateCompanyPayload = {
- ...payload,
- companyType: COMPANY_TYPE_MAP[userType!],
- };
+ const enriched: CreateCompanyPayload = { ...payload, companyType: COMPANY_TYPE_MAP[userType!] };
createCompanyMutation.mutate(enriched);
};
- const handleSelectType = (type: OnboardingUserType) => {
- setUserType(type);
- };
+ const handleSelectType = (type: OnboardingUserType) => setUserType(type);
+ const handleBack = () => setUserType(null);
- const handleBack = () => {
- setUserType(null);
- };
-
- // Preflight: user type selection
if (!userType) {
return (
-
-
-
+
+
+
Select Account Type
-
-
+
+
Choose the account type that fits your role.
-
-
+
+
-
+
{USER_TYPE_CARDS.map((card) => (
- handleSelectType(card.id)}
- className="group relative flex flex-col items-start gap-3 rounded-xl border-2 border-border bg-card p-4 text-left transition-all hover:border-primary hover:bg-primary/[0.03] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+ className="group block rounded-lg shadow-lg! border! border-edr-border! bg-edr-card! p-5! text-left transition-all duration-200 hover:-translate-y-0.5 hover:border-[var(--mantine-color-edr-green-5)]! hover:bg-edr-soft hover:shadow-[0_12px_28px_-14px_rgba(14,163,83,0.55)]"
>
-
- {card.icon}
-
-
-
{card.label}
-
- {card.description}
-
-
-
-
-
-
+
+
+ {card.icon}
+
+
+
+ {card.label}
+
+
+ {card.description}
+
+
+
+
+
))}
-
-
+
+
);
}
- // Render the appropriate form based on user type
const leftConfig = USER_TYPE_LEFT_MAP[userType];
const leftProps = {
...leftConfig,
features:
userType === "transporter"
- ? [
- "Vehicle & fleet registration",
- "TIN & FAN verification",
- "First-mile / Last-mile eligibility",
- ]
+ ? ["Vehicle & fleet registration", "TIN & FAN verification", "First-mile / Last-mile eligibility"]
: userType === "freight-forwarder-dj"
- ? [
- "Company details",
- "Representative information",
- "Cross-border operations",
- ]
- : [
- "Company registration details",
- "Contact and management personnel",
- "Power of Attorney (optional)",
- ],
- stats: {
- label: "Active Customers",
- value: "500+",
- footer: "And growing",
- progress: "w-[95%]",
- },
+ ? ["Company details", "Representative information", "Cross-border operations"]
+ : ["Company registration details", "Contact and management personnel", "Power of Attorney (optional)"],
+ stats: { label: "Active Customers", value: "500+", footer: "And growing", progress: "w-[95%]" },
};
return (
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx
index 44ae45afa..87702bdd8 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/SetPasswordPage.tsx
@@ -1,20 +1,13 @@
-import { useState, useMemo } from "react";
-import { useNavigate } from "react-router-dom";
-import { useForm } from "react-hook-form";
+import { Alert, Box, Button, Group, PasswordInput, Stack, Text, ThemeIcon } from "@mantine/core";
import { zodResolver } from "@hookform/resolvers/zod";
+import { ArrowRight, Check, LockKeyhole, X } from "lucide-react";
+import { useMemo, useState } from "react";
+import { useForm } from "react-hook-form";
+import { useNavigate } from "react-router-dom";
import { z } from "zod";
-import { ArrowRight, Check, Eye, EyeOff, LockKeyhole, Loader2, X } from "lucide-react";
+
import useAuth from "@/hooks/useAuth";
import AuthLayout from "@/components/auth/AuthLayout";
-import { cn } from "@/lib/utils";
-import {
- Button,
- Input,
- Field,
- FieldLabel,
- FieldError,
- FieldGroup,
-} from "@edr/ui-common";
const passwordRequirements = [
{ label: "At least 8 characters", test: (v: string) => v.length >= 8 },
@@ -45,8 +38,6 @@ type FormData = z.infer;
export default function SetPasswordPage() {
const navigate = useNavigate();
const { setPassword } = useAuth();
- const [showPassword, setShowPassword] = useState(false);
- const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
@@ -105,112 +96,77 @@ export default function SetPasswordPage() {
stats: { label: "Security Protection", value: "256-bit", footer: "Encrypted", progress: "w-[98%]" },
}}
>
-
-
-
-
-
Set Password
-
- Create a secure password for your account.
-
-
+
+
+
+
+
+
+ Set Password
+
+
+ Create a secure password for your account.
+
+
+
{error && (
-
+
)}
-
-
-
- Password
-
-
- setShowPassword(!showPassword)}
- className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground"
- >
- {showPassword ? : }
-
-
-
-
+
+
+
+
+ {password && (
+
+ {requirements.map((req) => (
+
+
+ {req.met ? : }
+
+
+ {req.label}
+
+
+ ))}
+
+ )}
+
- {password && (
-
- {requirements.map((req) => (
- -
- {req.met ? (
-
- ) : (
-
- )}
- {req.label}
-
- ))}
-
- )}
+
-
- Confirm Password
-
-
- setShowConfirmPassword(!showConfirmPassword)}
- className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground"
- >
- {showConfirmPassword ? : }
-
-
-
-
-
-
-
- {loading ? (
- <>
-
- Saving...
- >
- ) : (
- <>
- Save Password
-
- >
- )}
-
+ : undefined}
+ >
+ Save Password
+
+
);
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
index f8c8bc3c9..f85d11750 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx
@@ -1,60 +1,33 @@
-import { useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { useForm } from "react-hook-form";
+import { Alert, Box, Button, Group, PasswordInput, SimpleGrid, Stack, Text, TextInput, ThemeIcon } from "@mantine/core";
import { zodResolver } from "@hookform/resolvers/zod";
+import { Check, ArrowRight, UserPlus, X } from "lucide-react";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { useNavigate } from "react-router-dom";
import { z } from "zod";
-import {
- ArrowRight,
- Eye,
- EyeOff,
- UserPlus,
- Loader2,
- Check,
- X,
-} from "lucide-react";
+
import { userType } from "@/enums/userType";
import useAuth from "@/hooks/useAuth";
import type { SignupPayload } from "@/types/auth";
import AuthLayout from "@/components/auth/AuthLayout";
import PhoneInput from "@/components/auth/PhoneInput";
-import { cn } from "@/lib/utils";
-import {
- Button,
- Input,
- Field,
- FieldLabel,
- FieldError,
- FieldGroup,
-} from "@edr/ui-common";
const passwordRequirements = [
{ label: "At least 8 characters", test: (v: string) => v.length >= 8 },
{ label: "One uppercase letter", test: (v: string) => /[A-Z]/.test(v) },
{ label: "One lowercase letter", test: (v: string) => /[a-z]/.test(v) },
{ label: "One number", test: (v: string) => /\d/.test(v) },
- {
- label: "One special character",
- test: (v: string) => /[^A-Za-z0-9]/.test(v),
- },
+ { label: "One special character", test: (v: string) => /[^A-Za-z0-9]/.test(v) },
] as const;
const userSchema = z
.object({
email: z.string().email("Invalid email address"),
countryCode: z.string().min(1, "Country code is required"),
- phone: z
- .string()
- .min(9, "Phone number is too short")
- .max(9, "Phone number is too long"),
+ phone: z.string().min(9, "Phone number is too short").max(9, "Phone number is too long"),
userType: z.string(),
- firstName: z.object({
- en: z.string().min(2, "Name is required"),
- am: z.string().nullable(),
- }),
- lastName: z.object({
- en: z.string().min(2, "Name is required"),
- am: z.string().nullable(),
- }),
+ firstName: z.object({ en: z.string().min(2, "Name is required"), am: z.string().nullable() }),
+ lastName: z.object({ en: z.string().min(2, "Name is required"), am: z.string().nullable() }),
password: z
.string()
.min(8, "Password must be at least 8 characters")
@@ -76,8 +49,6 @@ export default function SignupPage() {
const { signup } = useAuth();
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
- const [showPassword, setShowPassword] = useState(false);
- const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const {
register,
@@ -102,9 +73,7 @@ export default function SignupPage() {
setError(null);
setLoading(true);
try {
- const normalizedPhone = data.phone.startsWith("0")
- ? data.phone.slice(1)
- : data.phone;
+ const normalizedPhone = data.phone.startsWith("0") ? data.phone.slice(1) : data.phone;
const payload: SignupPayload = {
email: data.email,
username: data.email,
@@ -120,7 +89,6 @@ export default function SignupPage() {
const result = await signup(payload);
if (result.success) {
navigate("/portal");
- // navigate("/otp");
} else {
setError(result.error.message);
}
@@ -131,6 +99,8 @@ export default function SignupPage() {
}
};
+ const passwordValue = watch("password") ?? "";
+
return (
-
-
-
-
-
Create Account
-
- Register to access EDR Freight services.
-
-
+
+
+
+
+
+
+ Create Account
+
+
+ Register to access EDR Freight services.
+
+
+
{error && (
-
+
)}
-
-
-
-
- First Name
-
-
-
-
-
- Last Name
-
-
-
-
-
- Email Address
-
+
+
+
-
-
+
+
+
+
-
- Password
-
-
- setShowPassword(!showPassword)}
- className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
- tabIndex={-1}
- >
- {showPassword ? (
-
- ) : (
-
- )}
-
-
-
-
- {passwordRequirements.map((req) => {
- const met = req.test(watch("password") ?? "");
- return (
-
- {met ? (
-
- ) : (
-
- )}
- {req.label}
-
- );
- })}
-
-
+
+
+ {passwordValue.length > 0 && (
+
+ {passwordRequirements.map((req) => {
+ const met = req.test(passwordValue);
+ return (
+
+
+ {met ? : }
+
+
+ {req.label}
+
+
+ );
+ })}
+
+ )}
+
-
- Confirm Password
-
-
- setShowConfirmPassword(!showConfirmPassword)}
- className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
- tabIndex={-1}
- >
- {showConfirmPassword ? (
-
- ) : (
-
- )}
-
-
-
-
-
+
-
- {loading ? (
- <>
-
- Creating...
- >
- ) : (
- <>
- Create Account
-
- >
- )}
-
-
-
- Already have an account?
navigate("/login")}
- className="h-auto p-0 font-semibold"
+ type="submit"
+ disabled={loading}
+ loading={loading}
+ size="lg"
+ color="edr-green"
+ fullWidth
+ rightSection={!loading ? : undefined}
>
- Sign In
+ Create Account
-
+
+
+ Already have an account?{" "}
+ navigate("/login")}
+ >
+ Sign In
+
+
+
);
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/TransporterForm.tsx b/apps/edr-freight-web/portal/src/pages/accounts/TransporterForm.tsx
index eea5c0051..ef6f894fd 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/TransporterForm.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/TransporterForm.tsx
@@ -1,42 +1,24 @@
-import { useState } from "react";
-import { useForm, Controller } from "react-hook-form";
-import { useQuery } from "@tanstack/react-query";
+import { Box, Button, Divider, Group, Loader, Select, SimpleGrid, Stack, Text, TextInput, ThemeIcon } from "@mantine/core";
import { zodResolver } from "@hookform/resolvers/zod";
-import { z } from "zod";
+import { useQuery } from "@tanstack/react-query";
import {
- ArrowRight,
ArrowLeft,
+ ArrowRight,
+ CheckCircle2,
ChevronLeft,
Truck,
- CheckCircle2,
- Loader2,
UploadCloud,
} from "lucide-react";
+import { useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { z } from "zod";
+
import type { AuthUser } from "@/types/auth";
import type { CreateCompanyPayload } from "@/services/companies.service";
-import {
- Button,
- Input,
- Field,
- FieldLabel,
- FieldError,
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
- SmartFileInput,
-} from "@edr/ui-common";
-import { cn } from "@/lib/utils";
+import { SmartFileInput } from "@edr/ui-common";
import { api } from "@/services/api";
-const TRUCK_TYPES = [
- "Casoni",
- "Truck Trailer",
- "High Bed",
- "Low Bed",
- "Others",
-] as const;
+const TRUCK_TYPES = ["Casoni", "Truck Trailer", "High Bed", "Low Bed", "Others"] as const;
type TransporterStep = "vehicle" | "documents" | "confirm";
@@ -54,10 +36,7 @@ const transporterSchema = z
.regex(/^\d{4}$/, "Enter a valid year (e.g. 2023)"),
})
.superRefine((data, ctx) => {
- if (
- data.truckType === "Casoni" &&
- (!data.plateNumber2 || data.plateNumber2.trim().length === 0)
- ) {
+ if (data.truckType === "Casoni" && (!data.plateNumber2 || data.plateNumber2.trim().length === 0)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ["plateNumber2"],
@@ -99,45 +78,25 @@ export default function TransporterForm({
}: {
documentSettingCode: string;
documentFiles?: Record;
- onDocumentFilesChange?: (
- files: Record,
- ) => void;
+ onDocumentFilesChange?: (files: Record) => void;
user: AuthUser;
onSubmit: (data: CreateCompanyPayload) => void;
isPending: boolean;
onBack: () => void;
}) {
const [step, setStep] = useState("vehicle");
- const [internalFiles, setInternalFiles] = useState<
- Record
- >({});
+ const [internalFiles, setInternalFiles] = useState>({});
const documentFiles = controlledFiles ?? internalFiles;
const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles;
const { data: uploadSetting, isLoading: loadingDocuments } = useQuery(
- api.fileUploadSettings.getByCode.queryOptions({
- input: { code: documentSettingCode },
- refetchOnMount: false,
- }),
+ api.fileUploadSettings.getByCode.queryOptions({ input: { code: documentSettingCode }, refetchOnMount: false }),
);
- const {
- register,
- handleSubmit,
- trigger,
- watch,
- control,
- formState: { errors },
- } = useForm({
+ const { register, handleSubmit, trigger, watch, control, formState: { errors } } = useForm({
resolver: zodResolver(transporterSchema),
defaultValues: {
- tinNumber: "",
- fanNumber: "",
- truckType: "",
- plateNumber: "",
- plateNumber2: "",
- vehicleModel: "",
- yearOfManufacturing: "",
+ tinNumber: "", fanNumber: "", truckType: "", plateNumber: "", plateNumber2: "", vehicleModel: "", yearOfManufacturing: "",
},
});
@@ -148,298 +107,220 @@ export default function TransporterForm({
const totalSteps = 3;
const nextStep = async () => {
- if (step === "documents") {
- setStep("confirm");
- return;
- }
- if (step === "confirm") {
- handleSubmit((data) => onSubmit(buildPayload(data, user)))();
- return;
- }
- const fields: (keyof FormData)[] = [
- "tinNumber",
- "fanNumber",
- "truckType",
- "plateNumber",
- "vehicleModel",
- "yearOfManufacturing",
- ];
+ if (step === "documents") { setStep("confirm"); return; }
+ if (step === "confirm") { handleSubmit((data) => onSubmit(buildPayload(data, user)))(); return; }
+ const fields: (keyof FormData)[] = ["tinNumber", "fanNumber", "truckType", "plateNumber", "vehicleModel", "yearOfManufacturing"];
const isValid = await trigger(fields);
if (!isValid) return;
setStep("documents");
};
- const skipDocuments = () => {
- setStep("confirm");
- };
+ const skipDocuments = () => setStep("confirm");
const prevStep = () => {
- if (step === "vehicle") {
- onBack();
- } else if (step === "documents") {
- setStep("vehicle");
- } else {
- setStep("documents");
- }
+ if (step === "vehicle") onBack();
+ else if (step === "documents") setStep("vehicle");
+ else setStep("documents");
};
+ const STEPS: { key: TransporterStep; icon: React.ReactNode }[] = [
+ { key: "vehicle", icon: },
+ { key: "documents", icon: },
+ { key: "confirm", icon: },
+ ];
+
+ const STEP_LABELS: Record = {
+ vehicle: `Step 1 of ${totalSteps} — Vehicle Information`,
+ documents: `Step 2 of ${totalSteps} — Upload Documents (Optional)`,
+ confirm: `Step 3 of ${totalSteps} — Review & Confirm`,
+ };
+
+ const stepOrder: TransporterStep[] = ["vehicle", "documents", "confirm"];
+ const currentIdx = stepOrder.indexOf(step);
+
return (
<>
-
-
+ }
onClick={prevStep}
- className="mb-4 flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
-
Change account type
-
+
-
-
-
}
- active={step === "vehicle"}
- completed={step !== "vehicle"}
- />
-
}
- active={step === "documents"}
- completed={step === "confirm"}
- />
-
}
- active={step === "confirm"}
- completed={false}
- />
-
-
- {step === "vehicle" && `Step 1 of ${totalSteps} — Vehicle Information`}
- {step === "documents" && `Step 2 of ${totalSteps} — Upload Documents (Optional)`}
- {step === "confirm" && `Step 3 of ${totalSteps} — Review & Confirm`}
-
-
+
+
+ {STEPS.map(({ key, icon }, i) => {
+ const done = i < currentIdx;
+ const active = i === currentIdx;
+ return done || active ? (
+
+ {done ? : icon}
+
+ ) : (
+
+ {icon}
+
+ );
+ })}
+
- e.preventDefault()}
- className="flex flex-col gap-4"
- >
- {step === "vehicle" && (
- <>
-
-
- TIN Number (10 digits)
-
+ {STEP_LABELS[step]}
+
+
+
+ e.preventDefault()}>
+
+ {step === "vehicle" && (
+ <>
+
+
-
-
-
-
- FAN Number (16 digits)
-
-
-
-
+
-
+
-
- Vehicle / Truck Information
-
+ Vehicle / Truck Information
- (
-
- Truck Type
-
-
-
- )}
- />
+ (
+
>
);
@@ -447,37 +328,13 @@ export default function TransporterForm({
function ReviewRow({ label, value }: { label: string; value?: string | null }) {
return (
-
-
+
+
{label}
-
-
+
+
{value?.trim() ? value : "Not provided"}
-
-
- );
-}
-
-function StepIcon({
- icon,
- active,
- completed,
-}: {
- icon: React.ReactNode;
- active: boolean;
- completed: boolean;
-}) {
- return (
-
- {completed ? : icon}
-
+
+
);
}
diff --git a/apps/edr-freight-web/portal/src/pages/accounts/VerificationOtpPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/VerificationOtpPage.tsx
index a09417c01..2a95e7c00 100644
--- a/apps/edr-freight-web/portal/src/pages/accounts/VerificationOtpPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/accounts/VerificationOtpPage.tsx
@@ -1,20 +1,14 @@
-import { useState } from "react";
-import { useNavigate } from "react-router-dom";
-import { useForm } from "react-hook-form";
+import { Alert, Box, Button, Stack, Text, TextInput } from "@mantine/core";
import { zodResolver } from "@hookform/resolvers/zod";
+import { ArrowRight, MailCheck, RotateCw } from "lucide-react";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import { useNavigate } from "react-router-dom";
import { z } from "zod";
-import { ArrowRight, MailCheck, RotateCw, Loader2 } from "lucide-react";
+
import { verificationCodeType } from "@/enums/verificationCodeType";
import useAuth from "@/hooks/useAuth";
import AuthLayout from "@/components/auth/AuthLayout";
-import {
- Button,
- Input,
- Field,
- FieldLabel,
- FieldError,
- FieldGroup,
-} from "@edr/ui-common";
const otpSchema = z.object({
code: z.string().regex(/^\d{6}$/, "OTP must be exactly 6 digits"),
@@ -96,106 +90,101 @@ export default function VerificationOtpPage() {
stats: { label: "Verification Security", value: "99.9%", footer: "Protected", progress: "w-[99%]" },
}}
>
-
-
-
-
-
OTP Verification
-
Enter the 6-digit code sent to:
-
-
+
+
+
+
+
+
+ OTP Verification
+
+
+ Enter the 6-digit code sent to:
+
+
+ {maskedPhone}
+
+
+
{error && (
-
+
)}
{resentMessage && (
-
+
)}
-
-
-
- Verification Code
-
+
+
+
-
- {errors.code ? (
-
- ) : (
-
Enter the OTP sent to your phone
- )}
-
{otpValue.length}/6
-
-
-
+
+ {otpValue.length}/6
+
+
-
- {verifying ? (
- <>
-
- Verifying...
- >
- ) : (
- <>
- Verify Account
-
- >
- )}
-
+ : undefined}
+ >
+ Verify Account
+
-
- {resending ? (
- <>
-
- Sending...
- >
- ) : (
- <>
-
- Resend Code
- >
- )}
-
-
-
- Didn't receive the code?
: undefined}
>
- Send again
+ Resend Code
-
+
+
+ Didn't receive the code?{" "}
+
+ Send again
+
+
+
);