Merge pull request #1210 from Tria-plc/freight/nati-2

fixes
This commit is contained in:
Nathnael Wondisha
2026-08-10 10:07:13 +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 ? !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}

View File

@@ -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 && (
<TextInput
label="Name"
placeholder="Abebe Bikila"
error={errors.generalManagerName?.message}
{...register("generalManagerName")}
/>
)}
{(gaps.email || gaps.phone) && (
<SimpleGrid cols={gaps.email && gaps.phone ? 2 : 1} spacing="md">
{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 { 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 && (
<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
is never typed here — the verification owns it. */}
{gaps.name && (

View File

@@ -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 (
<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
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) && (
<Grid>
{gmGaps.email && (