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 && (
+
+ }
+ onClick={onRemovePoa}
+ >
+ Remove this representative
+
+
+ )}
{/* 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 && (