mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 20:38:17 +00:00
feat: add verifcation on the document step
This commit is contained in:
@@ -70,6 +70,8 @@ interface RoleLicenseStepProps {
|
|||||||
/** Newly-selected files per profile id (not yet uploaded). */
|
/** Newly-selected files per profile id (not yet uploaded). */
|
||||||
value: Record<string, File[]>;
|
value: Record<string, File[]>;
|
||||||
onChange: (value: Record<string, File[]>) => void;
|
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,
|
profiles,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
errors,
|
||||||
}: RoleLicenseStepProps) {
|
}: RoleLicenseStepProps) {
|
||||||
const setFiles = (profileId: string, files: File[]) => {
|
const setFiles = (profileId: string, files: File[]) => {
|
||||||
onChange({ ...value, [profileId]: files });
|
onChange({ ...value, [profileId]: files });
|
||||||
@@ -123,6 +126,11 @@ export default function RoleLicenseStep({
|
|||||||
file={buildLicenseSetting(profile.id, label)}
|
file={buildLicenseSetting(profile.id, label)}
|
||||||
value={{ [LICENSE_FILE_KEY]: selected }}
|
value={{ [LICENSE_FILE_KEY]: selected }}
|
||||||
uploadedKeys={hasExisting ? [LICENSE_FILE_KEY] : undefined}
|
uploadedKeys={hasExisting ? [LICENSE_FILE_KEY] : undefined}
|
||||||
|
errors={
|
||||||
|
errors?.[profile.id]
|
||||||
|
? { [LICENSE_FILE_KEY]: errors[profile.id] }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
const next = v[LICENSE_FILE_KEY];
|
const next = v[LICENSE_FILE_KEY];
|
||||||
const files = Array.isArray(next) ? next : next ? [next] : [];
|
const files = Array.isArray(next) ? next : next ? [next] : [];
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile";
|
|||||||
import type { CompanyRegistrationData } from "@edr/types";
|
import type { CompanyRegistrationData } from "@edr/types";
|
||||||
import { ControlledPhoneField, toEthiopianE164 } from "@/components/PhoneField";
|
import { ControlledPhoneField, toEthiopianE164 } from "@/components/PhoneField";
|
||||||
import { SmartFileInput } from "@edr/ui-common";
|
import { SmartFileInput } from "@edr/ui-common";
|
||||||
|
import { getMinFiles } from "@/types/fileUploadSettings";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import RoleLicenseStep, {
|
import RoleLicenseStep, {
|
||||||
type RoleLicenseProfile,
|
type RoleLicenseProfile,
|
||||||
@@ -358,6 +359,72 @@ export default function CompanyProfileForm({
|
|||||||
|
|
||||||
const hasDocuments = Boolean(uploadSetting?.fields?.length);
|
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
|
// 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
|
// 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:
|
// (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 () => {
|
const nextStep = async () => {
|
||||||
userNavigatedRef.current = true;
|
userNavigatedRef.current = true;
|
||||||
// The documents step auto-uploads whatever the user selected as they
|
// The documents step hard-blocks on required company documents and a
|
||||||
// continue (partial uploads are allowed — required-doc completeness is
|
// business license per operational profile before it auto-uploads and
|
||||||
// re-checked on resume). A failed upload holds them on the step.
|
// submits — no partial-completion path forward.
|
||||||
if (step === "documents") {
|
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) {
|
if (onUploadDocuments) {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
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);
|
setSaveError(null);
|
||||||
handleSubmit((data) => onSubmit(buildPayload(data, user)))();
|
handleSubmit((data) => onSubmit(buildPayload(data, user)))();
|
||||||
return;
|
return;
|
||||||
@@ -749,15 +813,17 @@ export default function CompanyProfileForm({
|
|||||||
file={uploadSetting}
|
file={uploadSetting}
|
||||||
value={documentFiles}
|
value={documentFiles}
|
||||||
uploadedKeys={uploadedDocumentKeys}
|
uploadedKeys={uploadedDocumentKeys}
|
||||||
|
errors={documentFieldErrors}
|
||||||
containerClassName="lg:grid grid-cols-2 items-stretch"
|
containerClassName="lg:grid grid-cols-2 items-stretch"
|
||||||
onChange={setDocumentFiles}
|
onChange={handleDocumentFilesChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<RoleLicenseStep
|
<RoleLicenseStep
|
||||||
profiles={roleProfiles ?? []}
|
profiles={roleProfiles ?? []}
|
||||||
value={licenseFiles ?? {}}
|
value={licenseFiles ?? {}}
|
||||||
onChange={onLicenseChange ?? (() => { })}
|
onChange={handleLicenseFilesChange}
|
||||||
|
errors={licenseFieldErrors}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user