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} + + ); + })} + -
e.preventDefault()} - className="flex flex-col gap-4" - > - + + {STEP_LABELS[step]} + + + + e.preventDefault()}> + {step === "company" && ( <> - - Company Name - + + - - - -
- - Company Email - - - - -
- -
- - Location - - - - - - Address - - - -
- -
- - TIN Number (10 digits) - - - - - - VAT Number - - - -
- - - FAN Number (16 digits) - + + - - + + + + + + + )} {step === "personnel" && ( <> -
-

- Contact Person -

-
- - Name - - - + Contact Person + + + + - -
-
+ -
- -
-

- General Manager -

-
- - Name - - - - - - Email - - - - - -
-
+ General Manager + + + + + )} {step === "poa" && ( <> -

- Power of Attorney details are optional. Fill them in if you have - them, or skip to continue. -

- - - PoA Name - + Power of Attorney details are optional. Fill them in if you have them, or skip to continue. + + + + - - - -
- - PoA Email - - - - -
- -
- - PoA Location - - - - - - PoA Address - - - -
+ + + + + )} {step === "documents" && ( <> {loadingDocuments ? ( -
- -
+ + + ) : !uploadSetting ? ( -

+ No document requirements found for your account type. -

+ ) : ( -
- -
+ )} )} {step === "confirm" && ( -
-
-

- Review your registration -

-

- Confirm the company details below before saving. -

-
- -
- - - - + + Review your registration + + Confirm the company details below before saving. + + + + + + - - - - - - - - - -
-
+ + + + + + + + + + + )} -
-
- - -
+ + -
-
+ +
); @@ -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 ( <> -
- + -
-
- } - 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} + + ); + })} + -
e.preventDefault()} - className="flex flex-col gap-4" - > - + + {STEP_LABELS[step]} + + + + e.preventDefault()}> + {step === "company" && ( <> - - Company Name - + + - - - -
- - Company Email - - - - -
- -
- - Location / Country - - - - - - Address - - - -
+ + + + + )} {step === "representative" && ( <> -

+ Provide the company representative details for this account. -

- - - Representative Name - + + + - - - -
- - Representative Email - - - - -
+ )} {step === "documents" && ( <> {loadingDocuments ? ( -
- -
+ + + ) : !uploadSetting ? ( -

+ No document requirements found for your account type. -

+ ) : ( -
- -
+ )} )} {step === "confirm" && ( -
-
-

- Review your registration -

-

- Confirm the company details below before saving. -

-
- -
+ + Review your registration + + Confirm the company details below before saving. + + @@ -366,60 +280,33 @@ export default function DjiboutiAgentForm({ - -
-
+ + + )} -
-
- - -
- {step === "documents" && ( - - )} - - -
-
+ + {step === "documents" && ( + + )} + + + +
); @@ -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 ( <> -
- + -
-
- } - 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} + + ); + })} + -
e.preventDefault()} - className="flex flex-col gap-4" - > - + + {STEP_LABELS[step]} + + + + e.preventDefault()}> + {step === "company" && ( <> - - Company Name - + + - - - -
- - Company Email - - - - -
- -
- - Location - - - - - - Address - - - -
- -
- - TIN Number (10 digits) - - - - - - VAT Number - - - -
- - - FAN Number (16 digits) - + + - - + + + + + + + )} {step === "personnel" && ( <> -
-

- Contact Person -

-
- - Name - - - + Contact Person + + + + - -
-
+ -
- -
-

- General Manager -

-
- - Name - - - - - - Email - - - - - -
-
+ General Manager + + + + + )} {step === "poa" && ( <> -

- Power of Attorney details are optional. Fill them in if you have - them, or skip to continue. -

- - - PoA Name - + Power of Attorney details are optional. Fill them in if you have them, or skip to continue. + + + + - - - -
- - PoA Email - - - - -
- -
- - PoA Location - - - - - - PoA Address - - - -
+ + + + + )} {step === "documents" && ( <> {loadingDocuments ? ( -
- -
+ + + ) : !uploadSetting ? ( -

+ No document requirements found for your account type. -

+ ) : ( -
- -
+ )} )} {step === "confirm" && ( -
-
-

- Review your registration -

-

- Confirm the company details below before saving. -

-
- -
+ + Review your registration + + Confirm the company details below before saving. + + @@ -559,96 +392,41 @@ export default function ForwarderForm({ - - - - - - - - - -
-
+ + + + + + + + + + + )} -
-
- - -
- {step === "documents" && ( - - )} - - -
-
+ + {step === "documents" && ( + + )} + + + +
); @@ -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(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 + -
- + setPassword(e.target.value)} required disabled={loading} /> -
-
+ - {error && ( -
- {error} -
- )} - - -

- Don't have an account?{" "} -

+ + + Don't have 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) => ( - + + + {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 && ( -
+ {error} -
+ )} -
- - - Password -
- - -
- -
+ + + + + {password && ( + + {requirements.map((req) => ( + + + {req.met ? : } + + + {req.label} + + + ))} + + )} + - {password && ( -
    - {requirements.map((req) => ( -
  • - {req.met ? ( - - ) : ( - - )} - {req.label} -
  • - ))} -
- )} + - - Confirm 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 && ( -
+ {error} -
+ )} -
- -
- - First Name - - - - - - Last Name - - - -
- - Email Address - + + + - - + + + + - - Password -
- - -
- -
- {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 -
- - -
- -
-
+ - - -

- Already have an account? -

+ + + Already have an account?{" "} + + +
); 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 ( <> -
- + -
-
- } - 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 - - - - )} - /> + ( + + - - - - {isCasoni && ( - - Plate Number (Trailer) - - - - )} - - {!isCasoni && ( - - Vehicle Model - - - - )} -
+ )} + -
- {isCasoni && ( - - Vehicle Model - + {isCasoni && ( + - - - )} - - - Year of Manufacturing - - - -
- - )} + + + )} - {step === "documents" && ( - <> - {loadingDocuments ? ( -
- -
- ) : !uploadSetting ? ( -

- No document requirements found for your account type. -

- ) : ( -
- -
- )} - - )} - - {step === "confirm" && ( -
-
-

- Review your registration -

-

- Confirm the details below before saving. -

-
- -
- - - - - {formValues.plateNumber2 && ( - - )} - - -
-
- )} - -
- - -
- {step === "documents" && ( - - )} - - -
-
+ + {step === "documents" && ( + + )} + + + + ); @@ -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:

-
-

{maskedPhone}

-
-
+ + + + + + + OTP Verification + + + Enter the 6-digit code sent to: + + + {maskedPhone} + + + {error && ( -
+ {error} -
+ )} {resentMessage && ( -
+ {resentMessage} -
+ )} -
- - - Verification Code - + + + -
- {errors.code ? ( - - ) : ( -

Enter the OTP sent to your phone

- )} - {otpValue.length}/6 -
-
-
+ + {otpValue.length}/6 + + - + - - -

- Didn't receive the code? -

+ + + Didn't receive the code?{" "} + + +
);