mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
@@ -491,6 +491,50 @@ export default function CompanyProfileForm({
|
|||||||
(identity?.poa.verified ?? false) ||
|
(identity?.poa.verified ?? false) ||
|
||||||
(identity ? !identity.faydaRequired && poaTyped : 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
|
// 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.
|
// the copy stays current even if the user goes back and edits the source.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -699,10 +743,13 @@ export default function CompanyProfileForm({
|
|||||||
};
|
};
|
||||||
// The GM's own verification never falls back to the signed-in account — that
|
// 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
|
// 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)
|
// "Same as owner" is exempt: the API copies the owner's (account-backed)
|
||||||
// contact details across, so there is no gap and no input.
|
// contact details across, so there is no gap and no input.
|
||||||
const gmGaps = {
|
const gmGaps = {
|
||||||
|
name: !gmSameAsOwner && gmVerified && !identity?.gm.name?.trim(),
|
||||||
email: !gmSameAsOwner && gmVerified && !identity?.gm.email?.trim(),
|
email: !gmSameAsOwner && gmVerified && !identity?.gm.email?.trim(),
|
||||||
phone: !gmSameAsOwner && gmVerified && !identity?.gm.phone?.trim(),
|
phone: !gmSameAsOwner && gmVerified && !identity?.gm.phone?.trim(),
|
||||||
};
|
};
|
||||||
@@ -711,7 +758,9 @@ export default function CompanyProfileForm({
|
|||||||
if (step === "personnel") {
|
if (step === "personnel") {
|
||||||
// The manager's email is offered but not demanded: the API dropped it from
|
// 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.
|
// `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");
|
if (gmGaps.phone) requiredKeys.push("generalManagerPhone");
|
||||||
} else if (step === "poa" && delegationRequired) {
|
} else if (step === "poa" && delegationRequired) {
|
||||||
// Only once a PoA is required or provided: an untouched optional PoA is
|
// Only once a PoA is required or provided: an untouched optional PoA is
|
||||||
@@ -976,6 +1025,8 @@ export default function CompanyProfileForm({
|
|||||||
identity={identity}
|
identity={identity}
|
||||||
requirePoa={requirePoa}
|
requirePoa={requirePoa}
|
||||||
gaps={poaGaps}
|
gaps={poaGaps}
|
||||||
|
onRemovePoa={removePoa}
|
||||||
|
removePending={poaRemovePending}
|
||||||
delegationRequired={delegationRequired}
|
delegationRequired={delegationRequired}
|
||||||
poaDocumentSetting={poaDocumentSetting}
|
poaDocumentSetting={poaDocumentSetting}
|
||||||
documentFiles={documentFiles}
|
documentFiles={documentFiles}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export interface PersonnelStepProps {
|
|||||||
* supply, and are therefore typed here. Computed by CompanyProfileForm, which
|
* supply, and are therefore typed here. Computed by CompanyProfileForm, which
|
||||||
* requires exactly these in the schema.
|
* requires exactly these in the schema.
|
||||||
*/
|
*/
|
||||||
gaps: { email: boolean; phone: boolean };
|
gaps: { name: boolean; email: boolean; phone: boolean };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PersonnelStep({
|
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
|
{/* Fayda's name, email and phone claims are all optional, and the
|
||||||
verification has no account to fall back on the way the owner's does
|
manager's own verification has no account to fall back on the way the
|
||||||
— the person onboarding is not necessarily the manager. Whatever the
|
owner's does — the person onboarding is not necessarily the manager.
|
||||||
verification left empty is typed here, and required: without it the
|
Whatever the verification left empty is typed here, and required:
|
||||||
submit fails on "Add your general manager email" with no field
|
without it the submit fails on "Add your general manager name" with no
|
||||||
anywhere to satisfy it. */}
|
field anywhere to satisfy it. */}
|
||||||
|
{gaps.name && (
|
||||||
|
<TextInput
|
||||||
|
label="Name"
|
||||||
|
placeholder="Abebe Bikila"
|
||||||
|
error={errors.generalManagerName?.message}
|
||||||
|
{...register("generalManagerName")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{(gaps.email || gaps.phone) && (
|
{(gaps.email || gaps.phone) && (
|
||||||
<SimpleGrid cols={gaps.email && gaps.phone ? 2 : 1} spacing="md">
|
<SimpleGrid cols={gaps.email && gaps.phone ? 2 : 1} spacing="md">
|
||||||
{gaps.email && (
|
{gaps.email && (
|
||||||
|
|||||||
@@ -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 type { UseFormReturn } from "react-hook-form";
|
||||||
|
|
||||||
import { SmartFileInput } from "@edr/ui-common";
|
import { SmartFileInput } from "@edr/ui-common";
|
||||||
@@ -21,6 +22,9 @@ export interface PoaStepProps {
|
|||||||
* the customer is actually asked to fill.
|
* the customer is actually asked to fill.
|
||||||
*/
|
*/
|
||||||
gaps: { name: boolean; email: boolean; phone: boolean; address: boolean };
|
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). */
|
/** The DARS delegation paper is owed (a PoA exists, or the company forwards). */
|
||||||
delegationRequired: boolean;
|
delegationRequired: boolean;
|
||||||
/** Single-field upload setting carrying just the delegation letter. */
|
/** Single-field upload setting carrying just the delegation letter. */
|
||||||
@@ -36,6 +40,8 @@ export default function PoaStep({
|
|||||||
identity,
|
identity,
|
||||||
requirePoa,
|
requirePoa,
|
||||||
gaps,
|
gaps,
|
||||||
|
onRemovePoa,
|
||||||
|
removePending,
|
||||||
delegationRequired,
|
delegationRequired,
|
||||||
poaDocumentSetting,
|
poaDocumentSetting,
|
||||||
documentFiles,
|
documentFiles,
|
||||||
@@ -81,6 +87,26 @@ export default function PoaStep({
|
|||||||
required={requirePoa}
|
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 && (
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="subtle"
|
||||||
|
color="red"
|
||||||
|
size="xs"
|
||||||
|
loading={removePending}
|
||||||
|
leftSection={<Trash2 size={14} />}
|
||||||
|
onClick={onRemovePoa}
|
||||||
|
>
|
||||||
|
Remove this representative
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
{/* Whatever the Fayda claim did carry is shown on the panel above and
|
{/* Whatever the Fayda claim did carry is shown on the panel above and
|
||||||
is never typed here — the verification owns it. */}
|
is never typed here — the verification owns it. */}
|
||||||
{gaps.name && (
|
{gaps.name && (
|
||||||
|
|||||||
@@ -148,15 +148,17 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue }
|
|||||||
// posting empty strings over a proven identity.
|
// posting empty strings over a proven identity.
|
||||||
const typedFieldsInUse = !gmSameAsOwner && !gm?.verified && !faydaRequired;
|
const typedFieldsInUse = !gmSameAsOwner && !gm?.verified && !faydaRequired;
|
||||||
|
|
||||||
// Except for what the verification never supplied. Fayda's email and phone
|
// Except for what the verification never supplied. Fayda's name, email and
|
||||||
// claims are optional, and a manager verified without them has no account to
|
// phone claims are all optional, and a manager verified without them has no
|
||||||
// fall back on the way the owner does — so those stay typed, here as well as
|
// account to fall back on the way the owner does — so those stay typed, here
|
||||||
// in onboarding, or a wrong address could never be corrected.
|
// as well as in onboarding, or a wrong value could never be corrected.
|
||||||
const gmGaps = {
|
const gmGaps = {
|
||||||
|
name: !gmSameAsOwner && Boolean(gm?.verified) && !gm?.name?.trim(),
|
||||||
email: !gmSameAsOwner && Boolean(gm?.verified) && !gm?.email?.trim(),
|
email: !gmSameAsOwner && Boolean(gm?.verified) && !gm?.email?.trim(),
|
||||||
phone: !gmSameAsOwner && Boolean(gm?.verified) && !gm?.phone?.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 (
|
return (
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
@@ -203,6 +205,14 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue }
|
|||||||
|
|
||||||
{/* Whatever the verification did not supply is typed instead — the
|
{/* Whatever the verification did not supply is typed instead — the
|
||||||
API keeps exactly those keys writable. */}
|
API keeps exactly those keys writable. */}
|
||||||
|
{gmGaps.name && (
|
||||||
|
<TextInput
|
||||||
|
label="Full Name"
|
||||||
|
placeholder="Abebe Bikila"
|
||||||
|
error={errors.generalManagerName?.message}
|
||||||
|
{...register("generalManagerName")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{(gmGaps.email || gmGaps.phone) && (
|
{(gmGaps.email || gmGaps.phone) && (
|
||||||
<Grid>
|
<Grid>
|
||||||
{gmGaps.email && (
|
{gmGaps.email && (
|
||||||
|
|||||||
Reference in New Issue
Block a user