From 723c403699412a7cadad9f7f4210cdf33bf32217 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Fri, 26 Jun 2026 13:00:11 +0000 Subject: [PATCH] fix: finish cleaning up the file syncing --- .../components/onboarding/RoleLicenseStep.tsx | 108 +++--- .../src/pages/accounts/CompanyProfileForm.tsx | 324 +----------------- .../companyProfileForm/LinkCheckboxCard.tsx | 51 +++ .../companyProfileForm/ReadOnlyField.tsx | 23 ++ .../accounts/companyProfileForm/helpers.ts | 142 ++++++++ .../accounts/companyProfileForm/schema.ts | 110 ++++++ 6 files changed, 404 insertions(+), 354 deletions(-) create mode 100644 apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/LinkCheckboxCard.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/ReadOnlyField.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/helpers.ts create mode 100644 apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/schema.ts diff --git a/apps/edr-freight-web/portal/src/components/onboarding/RoleLicenseStep.tsx b/apps/edr-freight-web/portal/src/components/onboarding/RoleLicenseStep.tsx index 5206a4b32..adbeffc7c 100644 --- a/apps/edr-freight-web/portal/src/components/onboarding/RoleLicenseStep.tsx +++ b/apps/edr-freight-web/portal/src/components/onboarding/RoleLicenseStep.tsx @@ -2,13 +2,15 @@ import { Anchor, Badge, Card, - FileInput, Group, Stack, Text, ThemeIcon, } from "@mantine/core"; -import { FileText, Paperclip, Upload } from "lucide-react"; +import { FileText, Paperclip } from "lucide-react"; + +import { SmartFileInput } from "@edr/ui-common"; +import type { IFileUploadSetting } from "@edr/types/freight"; import type { LicenseFile } from "@/services/companies.service"; @@ -20,6 +22,48 @@ const ROLE_LABELS: Record = { transporter: "Transporter", }; +/** Field key the synthesized per-profile upload setting is keyed on. */ +const LICENSE_FILE_KEY = "business_license"; + +/** + * Build a single-field upload setting so each profile's license input can reuse + * the shared SmartFileInput (same dropzone + "uploaded" state as the documents + * step), instead of a bespoke file picker. + */ +function buildLicenseSetting( + profileId: string, + profileName: string, +): IFileUploadSetting { + return { + id: `license-setting-${profileId}`, + createdAt: "", + updatedAt: "", + deletedAt: null, + code: "business_license", + label: "Business license", + description: null, + entity: "customer", + fields: [ + { + id: `${LICENSE_FILE_KEY}-${profileId}`, + createdAt: "", + updatedAt: "", + deletedAt: null, + settingId: `license-setting-${profileId}`, + fileKey: LICENSE_FILE_KEY, + fileLabel: `Upload ${profileName} Business license file(s)`, + helpText: null, + isRequired: true, + isMultiple: true, + maxFiles: 10, + allowedExtensions: ["pdf", "png", "jpg", "jpeg"], + maxSizeMb: 10, + order: 1, + }, + ], + }; +} + export interface RoleLicenseProfile { id: string; type: string; @@ -38,8 +82,9 @@ interface RoleLicenseStepProps { /** * Final onboarding step: collect a business license (one or more files) for - * each operational role the company holds. Each role gets its own multi-file - * input; already-uploaded files are listed for context. + * each operational role the company holds. Each role gets its own SmartFileInput + * dropzone; already-uploaded files are listed (with download links) for context + * and surface the input's "uploaded" state. */ export default function RoleLicenseStep({ profiles, @@ -60,37 +105,11 @@ export default function RoleLicenseStep({ {profiles.map((profile) => { const label = ROLE_LABELS[profile.type] ?? profile.type; const selected = value[profile.id] ?? []; - const hasAny = selected.length > 0 || profile.existingFiles.length > 0; + const hasExisting = profile.existingFiles.length > 0; return ( - - - - - - -
- - {label} — Business License - - - {profile.reference} - -
-
- {hasAny && ( - - Provided - - )} -
- - {profile.existingFiles.length > 0 && ( + <> + {hasExisting && ( {profile.existingFiles.map((f) => ( @@ -108,20 +127,17 @@ export default function RoleLicenseStep({ )} - } - placeholder={ - profile.existingFiles.length > 0 - ? "Upload more / replace files" - : "Select license file(s)" - } - value={selected} - onChange={(files) => setFiles(profile.id, files ?? [])} + { + const next = v[LICENSE_FILE_KEY]; + const files = Array.isArray(next) ? next : next ? [next] : []; + setFiles(profile.id, files); + }} /> -
+ ); })} 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 09c632d56..bfde5c43b 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx @@ -9,7 +9,6 @@ import { Stack, Text, TextInput, - UnstyledButton, } from "@mantine/core"; import { zodResolver } from "@hookform/resolvers/zod"; import { useQuery } from "@tanstack/react-query"; @@ -17,7 +16,6 @@ import { AlertCircle, ArrowLeft, ArrowRight, - Check, CheckCircle2, RotateCw, Smartphone, @@ -25,17 +23,12 @@ import { } from "lucide-react"; import { useEffect, useRef, 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 type { ProfileResponse, UpdateProfilePayload } from "@/types/profile"; import type { CompanyRegistrationData } from "@edr/types"; -import { - ControlledPhoneField, - isValidPhone, - toEthiopianE164, -} from "@/components/PhoneField"; +import { ControlledPhoneField, toEthiopianE164 } from "@/components/PhoneField"; import { SmartFileInput } from "@edr/ui-common"; import { api } from "@/services/api"; import RoleLicenseStep, { @@ -43,306 +36,21 @@ import RoleLicenseStep, { } from "@/components/onboarding/RoleLicenseStep"; import ETradeInfo from "@/components/onboarding/ETradeInfo"; import { extractApiError } from "@/utils/result"; - -type CompanyStep = - | "company" - | "personnel" - | "contact" - | "verify" - | "poa" - | "documents" - | "additional"; - -/** Last 9 digits (Ethiopian national significant number) for tolerant compare. */ -const phoneDigits = (p?: string | null) => - (p ?? "").replace(/\D/g, "").slice(-9); -const samePhone = (a?: string | null, b?: string | null) => { - const da = phoneDigits(a); - return da.length === 9 && da === phoneDigits(b); -}; -/** Mask all but the first 7 chars of an E.164 phone for display. */ -const maskPhone = (p: string) => - p.length > 4 ? `${p.slice(0, 7)}${"*".repeat(Math.max(0, p.length - 7))}` : p; - -const onboardingSchema = z.object({ - companyName: z.string().min(1, "Company name is required"), - companyEmail: z.string().email("Invalid email address"), - companyPhone: z - .string() - .min(1, "Company phone is required") - .refine(isValidPhone, "Enter a valid phone number"), - companyLocation: z.string().min(1, "Location is required"), - // Derived from the eTrade address parts (kebele/woreda/zone/region); no - // standalone input — the granular fields live in the registration section. - companyAddress: z.string().optional(), - tinNumber: z.string().length(10, "TIN must be exactly 10 digits"), - vatNumber: z - .string() - .min(1, "VAT number is required") - .length(10, "VAT number must be exactly 10 digits"), - fanNumber: z.string().length(16, "FAN must be exactly 16 digits"), - licenceNumber: z.string().optional(), - statusDescription: z.string().optional(), - dateRegistered: z.string().optional(), - renewedFrom: z.string().optional(), - renewalDate: z.string().optional(), - renewedTo: z.string().optional(), - // Address fields are user-entered and required (the registration/license - // fields above are read-only confirmations pulled from eTrade). - region: z.string().min(1, "Region is required"), - zone: z.string().min(1, "Zone is required"), - woreda: z.string().min(1, "Woreda is required"), - kebele: z.string().min(1, "Kebele is required"), - houseNo: z.string().min(1, "House number is required"), - contactPersonName: z.string().min(1, "Contact person name is required"), - contactPersonPosition: z.string().optional(), - contactPersonEmail: z - .string() - .email("Invalid email address") - .optional() - .or(z.literal("")), - contactPersonPhone: z - .string() - .min(1, "Contact person phone is required") - .refine(isValidPhone, "Enter a valid phone number"), - generalManagerName: z.string().min(1, "Manager name is required"), - generalManagerEmail: z.string().email("Invalid Manager email"), - generalManagerPhone: z - .string() - .min(1, "Manager phone is required") - .refine(isValidPhone, "Enter a valid phone number"), - poaName: z.string().optional(), - poaPhone: z - .string() - .optional() - .refine((v) => !v || isValidPhone(v), "Enter a valid phone number"), - poaAddress: z.string().optional(), - poaEmail: z.string().optional(), - poaLocation: z.string().optional(), -}); - -type FormData = z.infer; - -const stepFields: Record = { - company: [ - "companyName", - "companyEmail", - "companyPhone", - "companyLocation", - "companyAddress", - "tinNumber", - "vatNumber", - "fanNumber", - "licenceNumber", - "statusDescription", - "dateRegistered", - "renewedFrom", - "renewalDate", - "renewedTo", - "region", - "zone", - "woreda", - "kebele", - "houseNo", - ], - personnel: [ - "generalManagerName", - "generalManagerEmail", - "generalManagerPhone", - ], - contact: [ - "contactPersonName", - "contactPersonPosition", - "contactPersonEmail", - "contactPersonPhone", - ], - verify: [], - poa: [], - documents: [], - additional: [], -}; - -function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload { - return { - companyName: data.companyName, - companyEmail: data.companyEmail, - companyPhone: data.companyPhone, - companyLocation: data.companyLocation, - companyAddress: data.companyAddress, - tin: data.tinNumber, - vatNumber: data.vatNumber, - fanNumber: data.fanNumber, - attributes: { - contactPersonName: data.contactPersonName, - contactPersonPosition: data.contactPersonPosition || undefined, - contactPersonEmail: data.contactPersonEmail || undefined, - contactPersonPhone: data.contactPersonPhone, - generalManagerName: data.generalManagerName, - generalManagerEmail: data.generalManagerEmail, - generalManagerPhone: data.generalManagerPhone, - poaName: data.poaName || undefined, - poaPhone: data.poaPhone || undefined, - poaAddress: data.poaAddress || undefined, - poaEmail: data.poaEmail || undefined, - poaLocation: data.poaLocation || undefined, - }, - }; -} - -/** Map one wizard step's form values to the profile-update payload it saves. */ -function stepPayload( - step: CompanyStep, - d: FormData, -): Partial { - switch (step) { - case "company": - return { - companyName: d.companyName, - companyEmail: d.companyEmail, - companyPhone: d.companyPhone, - companyLocation: d.companyLocation, - companyAddress: d.companyAddress, - tin: d.tinNumber, - vatNumber: d.vatNumber, - fanNumber: d.fanNumber, - licenceNumber: d.licenceNumber, - statusDescription: d.statusDescription, - dateRegistered: d.dateRegistered, - renewedFrom: d.renewedFrom, - renewalDate: d.renewalDate, - renewedTo: d.renewedTo, - region: d.region, - zone: d.zone, - woreda: d.woreda, - kebele: d.kebele, - houseNo: d.houseNo, - etradePhone: d.companyPhone, - }; - case "personnel": - return { - generalManagerName: d.generalManagerName, - generalManagerEmail: d.generalManagerEmail, - generalManagerPhone: d.generalManagerPhone, - }; - case "contact": - return { - contactPersonName: d.contactPersonName, - contactPersonPosition: d.contactPersonPosition || undefined, - contactPersonEmail: d.contactPersonEmail || undefined, - contactPersonPhone: d.contactPersonPhone, - }; - case "poa": - return { - poaName: d.poaName || undefined, - poaPhone: d.poaPhone || undefined, - poaEmail: d.poaEmail || undefined, - poaLocation: d.poaLocation || undefined, - poaAddress: d.poaAddress || undefined, - }; - default: - return {}; - } -} - -/** Seed the form from previously-saved profile data. */ -function toFormValues(p: ProfileResponse): FormData { - // The draft placeholder TIN ("D…") shouldn't show as a real value. - const tin = p.tinNumber && !p.tinNumber.startsWith("D") ? p.tinNumber : ""; - return { - companyName: p.companyName ?? "", - companyEmail: p.companyEmail ?? "", - companyPhone: p.companyPhone ?? "", - companyLocation: p.companyLocation ?? "", - companyAddress: p.companyAddress ?? "", - tinNumber: tin, - vatNumber: p.vatNumber ?? "", - fanNumber: p.fanNumber ?? "", - licenceNumber: p.licenceNumber ?? "", - statusDescription: p.statusDescription ?? "", - dateRegistered: p.dateRegistered ?? "", - renewedFrom: p.renewedFrom ?? "", - renewalDate: p.renewalDate ?? "", - renewedTo: p.renewedTo ?? "", - region: p.region ?? "", - zone: p.zone ?? "", - woreda: p.woreda ?? "", - kebele: p.kebele ?? "", - houseNo: p.houseNo ?? "", - contactPersonName: p.contactPersonName ?? "", - contactPersonPosition: p.contactPersonPosition ?? "", - contactPersonEmail: p.contactPersonEmail ?? "", - contactPersonPhone: p.contactPersonPhone ?? "", - generalManagerName: p.generalManagerName ?? "", - generalManagerEmail: p.generalManagerEmail ?? "", - generalManagerPhone: p.generalManagerPhone ?? "", - poaName: p.poaName ?? "", - poaPhone: p.poaPhone ?? "", - poaAddress: p.poaAddress ?? "", - poaEmail: p.poaEmail ?? "", - poaLocation: p.poaLocation ?? "", - }; -} - -/** - * A card styled as a large checkbox: clicking it toggles `checked`, which the - * caller uses to prefill + lock a set of fields (and clear them on uncheck). - */ -function LinkCheckboxCard({ - checked, - onToggle, - title, - description, -}: { - checked: boolean; - onToggle: (checked: boolean) => void; - title: string; - description: string; -}) { - return ( - onToggle(!checked)} - role="checkbox" - aria-checked={checked} - className={`w-full rounded-lg border! p-3! text-left transition-colors! ${checked - ? "border-[var(--mantine-color-edr-green-6)]! bg-[var(--mantine-color-edr-green-0)]!" - : "border-[var(--mantine-color-gray-3)]! hover:border-[var(--mantine-color-edr-green-4)]! hover:bg-[var(--mantine-color-edr-green-0)]!" - }`} - > - -
- {checked && } -
-
- - {title} - - - {description} - -
-
-
- ); -} - -/** A single read-only registration value rendered as a label/value pair. */ -function ReadOnlyField({ label, value }: { label: string; value?: string }) { - return ( - - - {label} - - - {value && value.trim() ? value : "—"} - - - ); -} +import { + type CompanyStep, + type FormData, + onboardingSchema, + stepFields, +} from "./companyProfileForm/schema"; +import { + buildPayload, + maskPhone, + samePhone, + stepPayload, + toFormValues, +} from "./companyProfileForm/helpers"; +import { LinkCheckboxCard } from "./companyProfileForm/LinkCheckboxCard"; +import { ReadOnlyField } from "./companyProfileForm/ReadOnlyField"; export default function CompanyProfileForm({ documentSettingCode, diff --git a/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/LinkCheckboxCard.tsx b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/LinkCheckboxCard.tsx new file mode 100644 index 000000000..da0dd6254 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/LinkCheckboxCard.tsx @@ -0,0 +1,51 @@ +import { Group, Text, UnstyledButton } from "@mantine/core"; +import { Check } from "lucide-react"; + +/** + * A card styled as a large checkbox: clicking it toggles `checked`, which the + * caller uses to prefill + lock a set of fields (and clear them on uncheck). + */ +export function LinkCheckboxCard({ + checked, + onToggle, + title, + description, +}: { + checked: boolean; + onToggle: (checked: boolean) => void; + title: string; + description: string; +}) { + return ( + onToggle(!checked)} + role="checkbox" + aria-checked={checked} + className={`w-full rounded-lg border! p-3! text-left transition-colors! ${checked + ? "border-[var(--mantine-color-edr-green-6)]! bg-[var(--mantine-color-edr-green-0)]!" + : "border-[var(--mantine-color-gray-3)]! hover:border-[var(--mantine-color-edr-green-4)]! hover:bg-[var(--mantine-color-edr-green-0)]!" + }`} + > + +
+ {checked && } +
+
+ + {title} + + + {description} + +
+
+
+ ); +} + +export default LinkCheckboxCard; diff --git a/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/ReadOnlyField.tsx b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/ReadOnlyField.tsx new file mode 100644 index 000000000..6465375e7 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/ReadOnlyField.tsx @@ -0,0 +1,23 @@ +import { Stack, Text } from "@mantine/core"; + +/** A single read-only registration value rendered as a label/value pair. */ +export function ReadOnlyField({ + label, + value, +}: { + label: string; + value?: string; +}) { + return ( + + + {label} + + + {value && value.trim() ? value : "—"} + + + ); +} + +export default ReadOnlyField; diff --git a/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/helpers.ts b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/helpers.ts new file mode 100644 index 000000000..8f8a24eca --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/helpers.ts @@ -0,0 +1,142 @@ +import type { AuthUser } from "@/types/auth"; +import type { CreateCompanyPayload } from "@/services/companies.service"; +import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile"; + +import type { CompanyStep, FormData } from "./schema"; + +/** Last 9 digits (Ethiopian national significant number) for tolerant compare. */ +export const phoneDigits = (p?: string | null) => + (p ?? "").replace(/\D/g, "").slice(-9); + +export const samePhone = (a?: string | null, b?: string | null) => { + const da = phoneDigits(a); + return da.length === 9 && da === phoneDigits(b); +}; + +/** Mask all but the first 7 chars of an E.164 phone for display. */ +export const maskPhone = (p: string) => + p.length > 4 ? `${p.slice(0, 7)}${"*".repeat(Math.max(0, p.length - 7))}` : p; + +export function buildPayload( + data: FormData, + _user: AuthUser, +): CreateCompanyPayload { + return { + companyName: data.companyName, + companyEmail: data.companyEmail, + companyPhone: data.companyPhone, + companyLocation: data.companyLocation, + companyAddress: data.companyAddress, + tin: data.tinNumber, + vatNumber: data.vatNumber, + fanNumber: data.fanNumber, + attributes: { + contactPersonName: data.contactPersonName, + contactPersonPosition: data.contactPersonPosition || undefined, + contactPersonEmail: data.contactPersonEmail || undefined, + contactPersonPhone: data.contactPersonPhone, + generalManagerName: data.generalManagerName, + generalManagerEmail: data.generalManagerEmail, + generalManagerPhone: data.generalManagerPhone, + poaName: data.poaName || undefined, + poaPhone: data.poaPhone || undefined, + poaAddress: data.poaAddress || undefined, + poaEmail: data.poaEmail || undefined, + poaLocation: data.poaLocation || undefined, + }, + }; +} + +/** Map one wizard step's form values to the profile-update payload it saves. */ +export function stepPayload( + step: CompanyStep, + d: FormData, +): Partial { + switch (step) { + case "company": + return { + companyName: d.companyName, + companyEmail: d.companyEmail, + companyPhone: d.companyPhone, + companyLocation: d.companyLocation, + companyAddress: d.companyAddress, + tin: d.tinNumber, + vatNumber: d.vatNumber, + fanNumber: d.fanNumber, + licenceNumber: d.licenceNumber, + statusDescription: d.statusDescription, + dateRegistered: d.dateRegistered, + renewedFrom: d.renewedFrom, + renewalDate: d.renewalDate, + renewedTo: d.renewedTo, + region: d.region, + zone: d.zone, + woreda: d.woreda, + kebele: d.kebele, + houseNo: d.houseNo, + etradePhone: d.companyPhone, + }; + case "personnel": + return { + generalManagerName: d.generalManagerName, + generalManagerEmail: d.generalManagerEmail, + generalManagerPhone: d.generalManagerPhone, + }; + case "contact": + return { + contactPersonName: d.contactPersonName, + contactPersonPosition: d.contactPersonPosition || undefined, + contactPersonEmail: d.contactPersonEmail || undefined, + contactPersonPhone: d.contactPersonPhone, + }; + case "poa": + return { + poaName: d.poaName || undefined, + poaPhone: d.poaPhone || undefined, + poaEmail: d.poaEmail || undefined, + poaLocation: d.poaLocation || undefined, + poaAddress: d.poaAddress || undefined, + }; + default: + return {}; + } +} + +/** Seed the form from previously-saved profile data. */ +export function toFormValues(p: ProfileResponse): FormData { + // The draft placeholder TIN ("D…") shouldn't show as a real value. + const tin = p.tinNumber && !p.tinNumber.startsWith("D") ? p.tinNumber : ""; + return { + companyName: p.companyName ?? "", + companyEmail: p.companyEmail ?? "", + companyPhone: p.companyPhone ?? "", + companyLocation: p.companyLocation ?? "", + companyAddress: p.companyAddress ?? "", + tinNumber: tin, + vatNumber: p.vatNumber ?? "", + fanNumber: p.fanNumber ?? "", + licenceNumber: p.licenceNumber ?? "", + statusDescription: p.statusDescription ?? "", + dateRegistered: p.dateRegistered ?? "", + renewedFrom: p.renewedFrom ?? "", + renewalDate: p.renewalDate ?? "", + renewedTo: p.renewedTo ?? "", + region: p.region ?? "", + zone: p.zone ?? "", + woreda: p.woreda ?? "", + kebele: p.kebele ?? "", + houseNo: p.houseNo ?? "", + contactPersonName: p.contactPersonName ?? "", + contactPersonPosition: p.contactPersonPosition ?? "", + contactPersonEmail: p.contactPersonEmail ?? "", + contactPersonPhone: p.contactPersonPhone ?? "", + generalManagerName: p.generalManagerName ?? "", + generalManagerEmail: p.generalManagerEmail ?? "", + generalManagerPhone: p.generalManagerPhone ?? "", + poaName: p.poaName ?? "", + poaPhone: p.poaPhone ?? "", + poaAddress: p.poaAddress ?? "", + poaEmail: p.poaEmail ?? "", + poaLocation: p.poaLocation ?? "", + }; +} diff --git a/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/schema.ts b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/schema.ts new file mode 100644 index 000000000..31ee21ced --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/schema.ts @@ -0,0 +1,110 @@ +import { z } from "zod"; + +import { isValidPhone } from "@/components/PhoneField"; + +export type CompanyStep = + | "company" + | "personnel" + | "contact" + | "verify" + | "poa" + | "documents" + | "additional"; + +export const onboardingSchema = z.object({ + companyName: z.string().min(1, "Company name is required"), + companyEmail: z.string().email("Invalid email address"), + companyPhone: z + .string() + .min(1, "Company phone is required") + .refine(isValidPhone, "Enter a valid phone number"), + companyLocation: z.string().min(1, "Location is required"), + // Derived from the eTrade address parts (kebele/woreda/zone/region); no + // standalone input — the granular fields live in the registration section. + companyAddress: z.string().optional(), + tinNumber: z.string().length(10, "TIN must be exactly 10 digits"), + vatNumber: z + .string() + .min(1, "VAT number is required") + .length(10, "VAT number must be exactly 10 digits"), + fanNumber: z.string().length(16, "FAN must be exactly 16 digits"), + licenceNumber: z.string().optional(), + statusDescription: z.string().optional(), + dateRegistered: z.string().optional(), + renewedFrom: z.string().optional(), + renewalDate: z.string().optional(), + renewedTo: z.string().optional(), + // Address fields are user-entered and required (the registration/license + // fields above are read-only confirmations pulled from eTrade). + region: z.string().min(1, "Region is required"), + zone: z.string().min(1, "Zone is required"), + woreda: z.string().min(1, "Woreda is required"), + kebele: z.string().min(1, "Kebele is required"), + houseNo: z.string().min(1, "House number is required"), + contactPersonName: z.string().min(1, "Contact person name is required"), + contactPersonPosition: z.string().optional(), + contactPersonEmail: z + .string() + .email("Invalid email address") + .optional() + .or(z.literal("")), + contactPersonPhone: z + .string() + .min(1, "Contact person phone is required") + .refine(isValidPhone, "Enter a valid phone number"), + generalManagerName: z.string().min(1, "Manager name is required"), + generalManagerEmail: z.string().email("Invalid Manager email"), + generalManagerPhone: z + .string() + .min(1, "Manager phone is required") + .refine(isValidPhone, "Enter a valid phone number"), + poaName: z.string().optional(), + poaPhone: z + .string() + .optional() + .refine((v) => !v || isValidPhone(v), "Enter a valid phone number"), + poaAddress: z.string().optional(), + poaEmail: z.string().optional(), + poaLocation: z.string().optional(), +}); + +export type FormData = z.infer; + +export const stepFields: Record = { + company: [ + "companyName", + "companyEmail", + "companyPhone", + "companyLocation", + "companyAddress", + "tinNumber", + "vatNumber", + "fanNumber", + "licenceNumber", + "statusDescription", + "dateRegistered", + "renewedFrom", + "renewalDate", + "renewedTo", + "region", + "zone", + "woreda", + "kebele", + "houseNo", + ], + personnel: [ + "generalManagerName", + "generalManagerEmail", + "generalManagerPhone", + ], + contact: [ + "contactPersonName", + "contactPersonPosition", + "contactPersonEmail", + "contactPersonPhone", + ], + verify: [], + poa: [], + documents: [], + additional: [], +};