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 757c7508a..d15d88b92 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx @@ -491,6 +491,50 @@ export default function CompanyProfileForm({ (identity?.poa.verified ?? false) || (identity ? !identity.faydaRequired && poaTyped : false); + /** + * Drop an optional representative the company no longer wants. + * + * Verifying a PoA is one click on a step that calls itself optional, and it + * is not reversible from the form: the verification owns the fields (so + * blanking them is refused), and its mere existence makes the DARS paper due + * — which then blocks the submit AND clamps the resume back to this step. The + * settings page has the same escape hatch, but `/settings` is off-limits + * until onboarding finishes, so without this the customer is stuck. + * + * Not offered to a freight forwarder: the API refuses (they must have one). + */ + const [poaRemovePending, setPoaRemovePending] = useState(false); + const removePoa = async () => { + setSaveError(null); + setPoaRemovePending(true); + try { + await verifaydaService.removePoa(); + for (const key of [ + "poaName", + "poaEmail", + "poaPhone", + "poaAddress", + "poaLocation", + ] as const) { + setValue(key, "", { shouldDirty: false }); + } + // The paper is deleted server-side with the identity; a copy still + // sitting in the picker would be re-uploaded on the documents step. + setDocumentFiles({ ...documentFiles, [POA_DELEGATION_FILE_KEY]: null }); + onIdentityChange?.(); + } catch (err) { + setSaveError( + (err as { response?: { data?: { message?: string } } })?.response?.data + ?.message ?? + (err instanceof Error + ? err.message + : "Could not remove the Power of Attorney"), + ); + } finally { + setPoaRemovePending(false); + } + }; + // While linked, mirror the source values into the (disabled) target fields so // the copy stays current even if the user goes back and edits the source. useEffect(() => { @@ -699,10 +743,13 @@ export default function CompanyProfileForm({ }; // The GM's own verification never falls back to the signed-in account — that // account is the person onboarding, not necessarily the manager — so a GM - // verified with no email claim has nowhere else for one to come from. + // verified with no email claim has nowhere else for one to come from. The + // name claim is optional too, and `REQUIRED_COMPANY_INFO` demands it, so it + // gets the same treatment rather than dead-ending the submit. // "Same as owner" is exempt: the API copies the owner's (account-backed) // contact details across, so there is no gap and no input. const gmGaps = { + name: !gmSameAsOwner && gmVerified && !identity?.gm.name?.trim(), email: !gmSameAsOwner && gmVerified && !identity?.gm.email?.trim(), phone: !gmSameAsOwner && gmVerified && !identity?.gm.phone?.trim(), }; @@ -711,7 +758,9 @@ export default function CompanyProfileForm({ if (step === "personnel") { // The manager's email is offered but not demanded: the API dropped it from // `REQUIRED_COMPANY_INFO` once the notify resolver stopped depending on it. - // The phone is still required there, so it is still required here. + // The name and phone are still required there, so they are still required + // here. + if (gmGaps.name) requiredKeys.push("generalManagerName"); if (gmGaps.phone) requiredKeys.push("generalManagerPhone"); } else if (step === "poa" && delegationRequired) { // Only once a PoA is required or provided: an untouched optional PoA is @@ -976,6 +1025,8 @@ export default function CompanyProfileForm({ identity={identity} requirePoa={requirePoa} gaps={poaGaps} + onRemovePoa={removePoa} + removePending={poaRemovePending} delegationRequired={delegationRequired} poaDocumentSetting={poaDocumentSetting} documentFiles={documentFiles} diff --git a/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/steps/PersonnelStep.tsx b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/steps/PersonnelStep.tsx index 023cdb933..d3324e0a7 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/steps/PersonnelStep.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/steps/PersonnelStep.tsx @@ -23,7 +23,7 @@ export interface PersonnelStepProps { * supply, and are therefore typed here. Computed by CompanyProfileForm, which * requires exactly these in the schema. */ - gaps: { email: boolean; phone: boolean }; + gaps: { name: boolean; email: boolean; phone: boolean }; } export default function PersonnelStep({ @@ -81,12 +81,20 @@ export default function PersonnelStep({ /> )} - {/* Fayda's email and phone claims are optional, and the manager's own - verification has no account to fall back on the way the owner's does - — the person onboarding is not necessarily the manager. Whatever the - verification left empty is typed here, and required: without it the - submit fails on "Add your general manager email" with no field - anywhere to satisfy it. */} + {/* Fayda's name, email and phone claims are all optional, and the + manager's own verification has no account to fall back on the way the + owner's does — the person onboarding is not necessarily the manager. + Whatever the verification left empty is typed here, and required: + without it the submit fails on "Add your general manager name" with no + field anywhere to satisfy it. */} + {gaps.name && ( + + )} {(gaps.email || gaps.phone) && ( {gaps.email && ( diff --git a/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/steps/PoaStep.tsx b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/steps/PoaStep.tsx index d3782909d..6160b6690 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/steps/PoaStep.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/steps/PoaStep.tsx @@ -1,4 +1,5 @@ -import { Divider, SimpleGrid, Text, TextInput } from "@mantine/core"; +import { Button, Divider, Group, SimpleGrid, Text, TextInput } from "@mantine/core"; +import { Trash2 } from "lucide-react"; import type { UseFormReturn } from "react-hook-form"; import { SmartFileInput } from "@edr/ui-common"; @@ -21,6 +22,9 @@ export interface PoaStepProps { * the customer is actually asked to fill. */ gaps: { name: boolean; email: boolean; phone: boolean; address: boolean }; + /** Drop a verified representative the company decided against. */ + onRemovePoa: () => void; + removePending: boolean; /** The DARS delegation paper is owed (a PoA exists, or the company forwards). */ delegationRequired: boolean; /** Single-field upload setting carrying just the delegation letter. */ @@ -36,6 +40,8 @@ export default function PoaStep({ identity, requirePoa, gaps, + onRemovePoa, + removePending, delegationRequired, poaDocumentSetting, documentFiles, @@ -81,6 +87,26 @@ export default function PoaStep({ required={requirePoa} /> )} + {/* A verification cannot be undone by clearing the form — it owns those + fields — and its mere existence makes the delegation paper due, which + then blocks the submit. So an optional representative needs a way + back out, here rather than only in settings (unreachable until + onboarding finishes). */} + {identity?.poa.verified && !requirePoa && ( + + + + )} {/* Whatever the Fayda claim did carry is shown on the panel above and is never typed here — the verification owns it. */} {gaps.name && ( diff --git a/apps/edr-freight-web/portal/src/pages/settings/TabGeneralManager.tsx b/apps/edr-freight-web/portal/src/pages/settings/TabGeneralManager.tsx index e3d4c6698..ad680b010 100644 --- a/apps/edr-freight-web/portal/src/pages/settings/TabGeneralManager.tsx +++ b/apps/edr-freight-web/portal/src/pages/settings/TabGeneralManager.tsx @@ -148,15 +148,17 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue } // posting empty strings over a proven identity. const typedFieldsInUse = !gmSameAsOwner && !gm?.verified && !faydaRequired; - // Except for what the verification never supplied. Fayda's email and phone - // claims are optional, and a manager verified without them has no account to - // fall back on the way the owner does — so those stay typed, here as well as - // in onboarding, or a wrong address could never be corrected. + // Except for what the verification never supplied. Fayda's name, email and + // phone claims are all optional, and a manager verified without them has no + // account to fall back on the way the owner does — so those stay typed, here + // as well as in onboarding, or a wrong value could never be corrected. const gmGaps = { + name: !gmSameAsOwner && Boolean(gm?.verified) && !gm?.name?.trim(), email: !gmSameAsOwner && Boolean(gm?.verified) && !gm?.email?.trim(), phone: !gmSameAsOwner && Boolean(gm?.verified) && !gm?.phone?.trim(), }; - const savable = typedFieldsInUse || gmGaps.email || gmGaps.phone; + const savable = + typedFieldsInUse || gmGaps.name || gmGaps.email || gmGaps.phone; return ( @@ -203,6 +205,14 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue } {/* Whatever the verification did not supply is typed instead — the API keeps exactly those keys writable. */} + {gmGaps.name && ( + + )} {(gmGaps.email || gmGaps.phone) && ( {gmGaps.email && (