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 451222841..fa1061622 100644 --- a/apps/edr-freight-web/portal/src/components/onboarding/RoleLicenseStep.tsx +++ b/apps/edr-freight-web/portal/src/components/onboarding/RoleLicenseStep.tsx @@ -70,6 +70,8 @@ interface RoleLicenseStepProps { /** Newly-selected files per profile id (not yet uploaded). */ value: Record; onChange: (value: Record) => void; + /** "Business license is required" style error, keyed by profile id. */ + errors?: Record; } /** @@ -82,6 +84,7 @@ export default function RoleLicenseStep({ profiles, value, onChange, + errors, }: RoleLicenseStepProps) { const setFiles = (profileId: string, files: File[]) => { onChange({ ...value, [profileId]: files }); @@ -123,6 +126,11 @@ export default function RoleLicenseStep({ file={buildLicenseSetting(profile.id, label)} value={{ [LICENSE_FILE_KEY]: selected }} uploadedKeys={hasExisting ? [LICENSE_FILE_KEY] : undefined} + errors={ + errors?.[profile.id] + ? { [LICENSE_FILE_KEY]: errors[profile.id] } + : undefined + } onChange={(v) => { const next = v[LICENSE_FILE_KEY]; const files = Array.isArray(next) ? next : next ? [next] : []; 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 d7c3b913b..cc9f81a29 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx @@ -21,6 +21,7 @@ import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile"; import type { CompanyRegistrationData } from "@edr/types"; import { ControlledPhoneField, toEthiopianE164 } from "@/components/PhoneField"; import { SmartFileInput } from "@edr/ui-common"; +import { getMinFiles } from "@/types/fileUploadSettings"; import { api } from "@/services/api"; import RoleLicenseStep, { type RoleLicenseProfile, @@ -358,6 +359,72 @@ export default function CompanyProfileForm({ const hasDocuments = Boolean(uploadSetting?.fields?.length); + // Hard verification for the documents step: required company-level + // documents and a business license per operational profile must both be + // present before the user can continue. + const [documentFieldErrors, setDocumentFieldErrors] = useState< + Record + >({}); + const [licenseFieldErrors, setLicenseFieldErrors] = useState< + Record + >({}); + + const validateRequiredDocuments = (): Record => { + const errs: Record = {}; + for (const field of uploadSetting?.fields ?? []) { + const min = getMinFiles(field); + if (min <= 0) continue; + if ((uploadedDocumentKeys ?? []).includes(field.fileKey)) continue; + const v = documentFiles[field.fileKey]; + const count = Array.isArray(v) ? v.length : v ? 1 : 0; + if (count < min) { + errs[field.fileKey] = `${field.fileLabel} is required`; + } + } + return errs; + }; + + // Every role needs at least one license file (existing or newly selected). + const validateLicenses = (): Record => { + const errs: Record = {}; + for (const p of roleProfiles ?? []) { + const hasNew = (licenseFiles?.[p.id]?.length ?? 0) > 0; + const hasExisting = p.existingFiles.length > 0; + if (!hasNew && !hasExisting) { + errs[p.id] = "Business license is required"; + } + } + return errs; + }; + + const handleDocumentFilesChange = ( + next: Record, + ) => { + setDocumentFiles(next); + setDocumentFieldErrors((prev) => { + if (Object.keys(prev).length === 0) return prev; + const updated = { ...prev }; + for (const key of Object.keys(updated)) { + const v = next[key]; + const hasValue = Array.isArray(v) ? v.length > 0 : v != null; + if (hasValue) delete updated[key]; + } + return updated; + }); + }; + + const handleLicenseFilesChange = (next: Record) => { + onLicenseChange?.(next); + setLicenseFieldErrors((prev) => { + if (Object.keys(prev).length === 0) return prev; + const updated = { ...prev }; + for (const id of Object.keys(updated)) { + if ((next[id]?.length ?? 0) > 0) delete updated[id]; + } + return updated; + }); + }; + // The registration/license details come straight from the eTrade lookup and // are not user-editable — shown as a read-only confirmation once a TIN lookup // (or rehydration) has filled them in. The address fields below are separate: @@ -402,18 +469,21 @@ export default function CompanyProfileForm({ } }; - // Every role needs at least one license file (existing or newly selected). - const licenseComplete = (roleProfiles ?? []).every( - (p) => - (licenseFiles?.[p.id]?.length ?? 0) > 0 || p.existingFiles.length > 0, - ); - const nextStep = async () => { userNavigatedRef.current = true; - // The documents step auto-uploads whatever the user selected as they - // continue (partial uploads are allowed — required-doc completeness is - // re-checked on resume). A failed upload holds them on the step. + // The documents step hard-blocks on required company documents and a + // business license per operational profile before it auto-uploads and + // submits — no partial-completion path forward. if (step === "documents") { + const docErrors = validateRequiredDocuments(); + const licenseErrors = validateLicenses(); + if (Object.keys(docErrors).length > 0 || Object.keys(licenseErrors).length > 0) { + setDocumentFieldErrors(docErrors); + setLicenseFieldErrors(licenseErrors); + setSaveError("Please upload all required documents before continuing."); + return; + } + if (onUploadDocuments) { setSaving(true); try { @@ -427,12 +497,6 @@ export default function CompanyProfileForm({ } } - if (!licenseComplete) { - setSaveError( - "Please upload a business license for each of your operational profiles.", - ); - return; - } setSaveError(null); handleSubmit((data) => onSubmit(buildPayload(data, user)))(); return; @@ -749,15 +813,17 @@ export default function CompanyProfileForm({ file={uploadSetting} value={documentFiles} uploadedKeys={uploadedDocumentKeys} + errors={documentFieldErrors} containerClassName="lg:grid grid-cols-2 items-stretch" - onChange={setDocumentFiles} + onChange={handleDocumentFilesChange} /> )} { })} + onChange={handleLicenseFilesChange} + errors={licenseFieldErrors} /> )}