Merge pull request #1211 from Tria-plc/dev

to staging
This commit is contained in:
Nathnael Wondisha
2026-08-10 10:08:38 +03:00
committed by GitHub
4 changed files with 110 additions and 15 deletions

View File

@@ -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}

View File

@@ -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 && (

View File

@@ -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 && (

View File

@@ -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 && (