diff --git a/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx b/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx index a359a3ff0..f8eb58e8f 100644 --- a/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx +++ b/apps/edr-freight-web/portal/src/pages/settings/TabCompanyProfile.tsx @@ -11,6 +11,7 @@ import { Button, Card, Group, + Select, SimpleGrid, Stack, Text, @@ -20,18 +21,22 @@ import { import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Building2, CheckCircle2, Save, XCircle } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; +import type { FieldErrors, UseFormRegister, UseFormSetValue } from "react-hook-form"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import type { CompanyRegistrationData } from "@edr/types"; +import { ETHIOPIAN_REGIONS, type CompanyRegistrationData } from "@edr/types"; import OnboardingRoleSelect from "./OnboardingRoleSelect"; import ETradeInfo, { type ETradeStatus, } from "@/components/onboarding/ETradeInfo"; import { ReadOnlyField } from "@/pages/accounts/companyProfileForm/ReadOnlyField"; import StepSection from "@/pages/accounts/companyProfileForm/StepSection"; -import { ETRADE_BUNDLE_FIELDS as SHARED_ETRADE_FIELDS } from "@/pages/accounts/companyProfileForm/schema"; +import { + ETRADE_BUNDLE_FIELDS as SHARED_ETRADE_FIELDS, + onboardingSchema, +} from "@/pages/accounts/companyProfileForm/schema"; -export const COMPANY_PROFILE_SCHEMA = z.object({ +const BASE_COMPANY_PROFILE_SCHEMA = z.object({ // eTrade-sourced and read-only, like the registration block below. companyName: z.string().optional(), companyLocation: z.string().min(1, "Location is required"), @@ -39,12 +44,13 @@ export const COMPANY_PROFILE_SCHEMA = z.object({ // no standalone input. companyAddress: z.string().optional(), tinNumber: z.string().regex(/^\d{10}$/, "TIN must be exactly 10 digits"), - // Same rule as onboarding — the two forms write the same column, so they must - // not disagree about what is acceptable in it. - vatNumber: z - .string() - .min(1, "VAT number is required") - .regex(/^\d{10,11}$/, "VAT number must be 10 or 11 digits"), + // Onboarding's own rule, reused rather than restated: the two forms write the + // same column, and the copy here had drifted into a 10-or-11-digit check that + // onboarding and the API both refuse to make. A foreign company's VAT is + // whatever its tax authority issues and a co-operative's follows neither, so + // the stricter copy locked those customers out of their own Company tab + // entirely — every save on it, not just the VAT. + vatNumber: onboardingSchema.shape.vatNumber, ownerPassportNumber: z.string().optional(), // Registration/address fields are eTrade-sourced and never typed by hand — // not even when eTrade leaves one blank, so none of them may be required @@ -62,7 +68,48 @@ export const COMPANY_PROFILE_SCHEMA = z.object({ houseNo: z.string().optional(), }); -export type CompanyProfileFormData = z.infer; +/** + * The registration fields a manual-registration company types by hand. + * + * eTrade holds no record for a co-operative union or farm, nor for a foreign + * investor on an Investment Commission licence, so the block every other + * company gets read-only off the licence is typed by these two — and is + * therefore required of them, exactly as the onboarding wizard requires it. + * House number stays optional: plenty of addresses have none. + */ +const TYPED_REGISTRATION_LABELS = { + companyName: "Company name", + region: "Region", + zone: "Zone", + woreda: "Woreda", + kebele: "Kebele", +} as const; + +/** + * `manualRegistration` is the only thing that changes here, and it changes the + * same way it does in the wizard: **a field is required iff there is an input + * on screen for it.** For an eTrade company these are read-only rows, so + * requiring one would be a Save button failing on a field with nothing to fix. + */ +export const buildCompanyProfileSchema = (manualRegistration: boolean) => + manualRegistration + ? BASE_COMPANY_PROFILE_SCHEMA.superRefine((d, ctx) => { + for (const key of Object.keys( + TYPED_REGISTRATION_LABELS, + ) as (keyof typeof TYPED_REGISTRATION_LABELS)[]) { + if (d[key]?.trim()) continue; + ctx.addIssue({ + code: "custom", + path: [key], + message: `${TYPED_REGISTRATION_LABELS[key]} is required`, + }); + } + }) + : BASE_COMPANY_PROFILE_SCHEMA; + +export type CompanyProfileFormData = z.infer< + typeof BASE_COMPANY_PROFILE_SCHEMA +>; /** * `etradePhone` is not on this form, so the shared list is filtered down to the @@ -86,6 +133,17 @@ export default function TabCompanyProfile({ }: TabCompanyProfileProps) { const queryClient = useQueryClient(); const isCreate = mode === "create"; + /** + * eTrade has nothing to say about this company — a co-operative holds no + * business licence, a foreign investor's is the Investment Commission's — so + * its registration was typed during onboarding and has to stay editable here. + * Rendering the eTrade card instead left that data invisible and frozen: the + * one company that owns its registration details was the one that could not + * see them. + */ + const manualRegistration = Boolean( + profile?.cooperative || profile?.investorLicence, + ); const [selectedRoles, setSelectedRoles] = useState([]); const [tinStatus, setTinStatus] = useState("idle"); @@ -140,7 +198,7 @@ export default function TabCompanyProfile({ setValue, formState: { errors, isDirty, dirtyFields }, } = useForm({ - resolver: zodResolver(COMPANY_PROFILE_SCHEMA), + resolver: zodResolver(buildCompanyProfileSchema(manualRegistration)), values: defaultValues, }); @@ -315,7 +373,6 @@ export default function TabCompanyProfile({ @@ -325,13 +382,21 @@ export default function TabCompanyProfile({ - {tinVerified && ( + {!manualRegistration && tinVerified && ( )} + {manualRegistration && ( + + + + )} + ; + watch: ReturnType>["watch"]; + setValue: UseFormSetValue; + errors: FieldErrors; +}) { + const region = watch("region") ?? ""; + + return ( + + + + Registered address + + +