feat: add verifcation on the document step

This commit is contained in:
Nathnael
2026-07-04 09:27:22 +00:00
parent a70804da1c
commit 1c15a2117b
2 changed files with 91 additions and 17 deletions

View File

@@ -70,6 +70,8 @@ interface RoleLicenseStepProps {
/** Newly-selected files per profile id (not yet uploaded). */
value: Record<string, File[]>;
onChange: (value: Record<string, File[]>) => void;
/** "Business license is required" style error, keyed by profile id. */
errors?: Record<string, string>;
}
/**
@@ -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] : [];

View File

@@ -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<string, string>
>({});
const [licenseFieldErrors, setLicenseFieldErrors] = useState<
Record<string, string>
>({});
const validateRequiredDocuments = (): Record<string, string> => {
const errs: Record<string, string> = {};
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<string, string> => {
const errs: Record<string, string> = {};
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<string, File | File[] | null>,
) => {
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<string, File[]>) => {
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}
/>
)}
<RoleLicenseStep
profiles={roleProfiles ?? []}
value={licenseFiles ?? {}}
onChange={onLicenseChange ?? (() => { })}
onChange={handleLicenseFilesChange}
errors={licenseFieldErrors}
/>
</>
)}