fix(freight-portal): stop owner details vanishing between wizard steps

SourcedField rendered read-only whenever a value existed, so the input a
customer had just typed into turned into a badge as soon as the step saved and
they navigated back — and dropped out of requiredKeys at the same time. It now
locks on ownership instead: a Fayda verification owns what its claims filled,
everything else stays an editable, prefilled input.

The representation step follows the same rule and asks in the right order:

- The power-of-attorney question collapses to its answer once given, with a
  button back to it (none for a freight forwarder, whose answer is forced).
- A foreign company picks how to prove the person outright — Fayda or a
  passport — rather than being shown both at once.
- The representative's own fields appear only once the person is established,
  and only for what the verification did not supply; what it did supply is
  already on the panel above and is no longer repeated beneath it.
- Dropped the freight-forwarder lecture and the DARS blurb; the badge and the
  upload field's own help text already say both.

VAT numbers accept any non-blank value. A foreign tax authority's carries
letters and dashes and a co-operative's follows neither pattern, so the 10-11
digit rule only ever rejected numbers we had no business judging.
This commit is contained in:
Nathnael
2026-08-11 12:54:38 +00:00
parent 02d9debe1d
commit 72164b0b8e
6 changed files with 363 additions and 230 deletions

View File

@@ -5,7 +5,6 @@ import {
MaxLength,
IsEnum,
IsIn,
Matches,
} from "class-validator";
import { ETHIOPIAN_REGIONS, type EthiopianRegion } from "@edr/types";
import { CompanyNationality } from "../entities/company.entity";
@@ -36,13 +35,14 @@ export class UpdateProfileDto {
@IsTin({ message: "TIN must be exactly 10 digits" })
tin?: string;
// Ethiopian VAT registration numbers are 10 digits (the same shape as the
// TIN), but some are issued with an 11th. Both portal forms enforce the same
// range; without it here the API happily stored whatever a stale client sent,
// and the two layers disagreed about what the column may hold.
// No shape check. Ethiopian VAT numbers are usually 10 or 11 digits, but a
// foreign company's is whatever its own tax authority issues — letters,
// dashes and any length — and a co-operative's registration numbering does
// not follow the trade-licence pattern either. The field is required (the
// portal enforces non-blank) but its content is not ours to police.
@IsOptional()
@IsString()
@Matches(/^\d{10,11}$/, { message: "VAT number must be 10 or 11 digits" })
@MaxLength(64)
vatNumber?: string;
// `fanNumber` is deliberately absent: the FAN is a Fayda number, so it would

View File

@@ -1,7 +1,7 @@
import { Badge, Group, Stack, Text } from "@mantine/core";
import type { ReactNode } from "react";
/** Where a prefilled value came from, shown as a badge next to it. */
/** Where a locked value came from, shown as a badge next to it. */
export type FieldSource = "eTrade" | "Fayda";
const SOURCE_NOTE: Record<FieldSource, string> = {
@@ -10,35 +10,37 @@ const SOURCE_NOTE: Record<FieldSource, string> = {
};
/**
* One person-detail field that may already be answered for us.
* One person-detail field that a verification may have taken ownership of.
*
* The onboarding wizard fills what it can from the eTrade lookup and the Fayda
* verification, and asks the customer only for what neither supplied. Both
* sources are patchy in practice — eTrade returns no email at all and often no
* manager name; Fayda's email and phone claims are optional and routinely come
* back empty — so "what is missing" varies per company and cannot be decided
* once at build time.
* `locked` — not "does a value exist" — decides which side renders. That
* distinction is the whole point: `ownerName` holds a value the moment the
* eTrade lookup prefills it or the customer types it and the step saves, and
* keying off presence meant the input a customer had just filled in turned into
* a read-only badge as soon as they navigated away and back, with no way to
* correct it. Only a Fayda verification actually owns a field — the API refuses
* to overwrite those — so only those lock.
*
* This is the single place that decision is rendered: a supplied value shows
* read-only with its provenance, a gap shows the input. It pairs with
* `requiredKeys` in CompanyProfileForm, which requires exactly the fields that
* fall through to `children` — the invariant being that **a field is required
* if and only if there is an input on screen to satisfy it**.
* It pairs with `requiredKeys` in CompanyProfileForm, which requires exactly
* the fields that fall through to `children`: **a field is required if and only
* if there is an input on screen to satisfy it.**
*/
export default function SourcedField({
label,
value,
source,
locked,
children,
}: {
label: string;
/** The value a source supplied. Blank/absent means "ask the customer". */
/** The value to display when locked. */
value?: string | null;
source: FieldSource;
/** The input rendered when no source supplied a value. */
/** A verification owns this field: show it read-only instead of an input. */
locked: boolean;
/** The input rendered whenever the field is still the customer's to fill. */
children: ReactNode;
}) {
if (!value?.trim()) return <>{children}</>;
if (!locked || !value?.trim()) return <>{children}</>;
return (
<Stack gap={2}>

View File

@@ -14,12 +14,15 @@ export interface OwnerStepProps {
/** eTrade's registered manager, once a TIN lookup has succeeded. */
etradeOwner: { name: string; phone: string } | null;
/**
* Which of the owner's details neither eTrade nor Fayda supplied, and are
* therefore typed here. Computed by CompanyProfileForm, which requires
* exactly these in the schema — so every input below is one the customer is
* actually asked to fill, and nothing is required that has no input.
* Which of the owner's details a Fayda verification owns. A locked field is
* shown read-only; every other one is an editable input, prefilled from
* eTrade or from what was saved earlier. CompanyProfileForm computes this and
* requires exactly the unlocked fields, so every input on screen is one the
* customer is actually asked to fill and nothing is required that has none.
*/
gaps: { name: boolean; email: boolean; phone: boolean };
locked: { name: boolean; email: boolean; phone: boolean };
/** A co-operative union or farm: no licence, so no eTrade record to match. */
cooperative?: boolean;
}
/**
@@ -32,12 +35,17 @@ export interface OwnerStepProps {
* onboarding is often not the person on the licence, and stamping their name,
* email and phone onto the owner turned three required fields into a guess
* wearing the licence's authority.
*
* A co-operative union or farm has no licence, so there is nobody named on one
* — the owner is simply the person who runs it, typed in full and compared
* against nothing.
*/
export default function OwnerStep({
form,
identity,
etradeOwner,
gaps,
locked,
cooperative = false,
}: OwnerStepProps) {
const {
register,
@@ -45,16 +53,7 @@ export default function OwnerStep({
formState: { errors },
} = form;
// A verified owner's own claims outrank eTrade's record for display: the
// government IdP is the higher-trust source, and the API locks those fields
// to it. eTrade still supplies the name and phone when there is no
// verification — which is the case for every company represented by a PoA.
const ownerVerified = identity?.owner.verified ?? false;
const nameValue = identity?.owner.name || etradeOwner?.name || "";
const phoneValue = identity?.owner.phone || etradeOwner?.phone || "";
const emailValue = identity?.owner.email || "";
const nameSource = identity?.owner.name ? "Fayda" : "eTrade";
const phoneSource = identity?.owner.phone ? "Fayda" : "eTrade";
// A Fayda verification that names someone other than the person on the
// licence is the one thing this step exists to catch. Advisory here — the two
@@ -65,12 +64,12 @@ export default function OwnerStep({
return (
<Stack gap="md">
<Text size="sm" c="edr-muted">
These are the details of the person registered on your eTrade licence.
We fill in whatever eTrade and Fayda gave us; anything they left blank
we need from you.
{cooperative
? "The person who runs the co-operative union or farm. We have no licence record to fill these in from, so we need all of them from you."
: "These are the details of the person registered on your eTrade licence. We fill in whatever eTrade and Fayda gave us; anything they left blank we need from you."}
</Text>
{!etradeOwner && !ownerVerified && (
{!cooperative && !etradeOwner && !ownerVerified && (
<Alert color="blue" variant="light" icon={<Info size={18} />}>
Your eTrade licence didn't list a manager, so there's nothing for us
to prefill. Enter the details of the person registered on it.
@@ -84,15 +83,19 @@ export default function OwnerStep({
icon={<AlertTriangle size={18} />}
title="This doesn't match your eTrade licence"
>
Your licence lists{" "}
<strong>{identity?.etradeManagerName}</strong>, but the name here is{" "}
<strong>{nameValue}</strong>. You can continue, but our team will
check this before approving your account so make sure it's the
person the licence actually names.
Your licence lists <strong>{identity?.etradeManagerName}</strong>, but
the name here is <strong>{identity?.owner.name}</strong>. You can
continue, but our team will check this before approving your account
so make sure it's the person the licence actually names.
</Alert>
)}
<SourcedField label="Owner's Name" value={nameValue} source={nameSource}>
<SourcedField
label="Owner's Name"
value={identity?.owner.name}
source="Fayda"
locked={locked.name}
>
<TextInput
label="Owner's Name"
placeholder="Abebe Bikila"
@@ -101,11 +104,16 @@ export default function OwnerStep({
/>
</SourcedField>
<SimpleGrid cols={{ base: 1, sm: gaps.email && gaps.phone ? 2 : 1 }} spacing="md">
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
{/* eTrade never returns an email for the manager and Fayda's email
claim is optional, so this is the field most companies actually
type it is required either way (`REQUIRED_COMPANY_INFO`). */}
<SourcedField label="Owner's Email" value={emailValue} source="Fayda">
<SourcedField
label="Owner's Email"
value={identity?.owner.email}
source="Fayda"
locked={locked.email}
>
<TextInput
label="Owner's Email"
type="email"
@@ -117,8 +125,9 @@ export default function OwnerStep({
<SourcedField
label="Owner's Phone"
value={phoneValue}
source={phoneSource}
value={identity?.owner.phone}
source="Fayda"
locked={locked.phone}
>
<ControlledPhoneField
control={control}

View File

@@ -1,5 +1,6 @@
import {
Alert,
Button,
Card,
Divider,
Group,
Loader,
@@ -8,7 +9,7 @@ import {
Text,
TextInput,
} from "@mantine/core";
import { Info, UserCheck, UserX } from "lucide-react";
import { IdCard, ShieldCheck, UserCheck, UserX } from "lucide-react";
import type { UseFormReturn } from "react-hook-form";
import { SmartFileInput } from "@edr/ui-common";
@@ -24,6 +25,9 @@ import type {
import type { FormData } from "../schema";
import SourcedField from "../SourcedField";
/** How a foreign company chose to prove the person: Fayda, or a passport. */
export type IdentityMethod = "fayda" | "passport";
export interface RepresentationStepProps {
form: UseFormReturn<FormData>;
identity?: CompanyIdentityState;
@@ -32,11 +36,26 @@ export interface RepresentationStepProps {
/** A declaration change is in flight. */
declarePending: boolean;
/**
* Which of the representative's details the Fayda verification did not
* supply. Same contract as OwnerStep's `gaps`: an input is rendered for
* exactly these, and the schema requires exactly these.
* The answer is not the company's to change — it operates as a freight
* forwarder, which the API forces to "yes". No alert says so: the summary
* simply offers no way back, which is the same information without the
* lecture.
*/
gaps: { name: boolean; email: boolean; phone: boolean; address: boolean };
declarationLocked: boolean;
/**
* Which of the representative's details the Fayda verification owns. Same
* contract as OwnerStep's `locked`: a locked field is read-only, every other
* one is an input, and the schema requires exactly the unlocked ones.
*/
locked: { name: boolean; email: boolean; phone: boolean; address: boolean };
/**
* How a foreign company is proving the subject. Null until it picks — the
* either/or is a fork, not a fallback, so nothing below it renders until one
* side is chosen. Always "fayda" for an Ethiopian company, which has no
* choice to make.
*/
method: IdentityMethod | null;
onMethodChange: (method: IdentityMethod) => void;
/** Single-field upload setting carrying just the DARS delegation letter. */
poaDocumentSetting?: FileUploadSetting;
documentFiles: Record<string, File | File[] | null>;
@@ -64,7 +83,10 @@ export default function RepresentationStep({
identity,
onDeclare,
declarePending,
gaps,
declarationLocked,
locked,
method,
onMethodChange,
poaDocumentSetting,
documentFiles,
uploadedDocumentKeys,
@@ -86,184 +108,220 @@ export default function RepresentationStep({
}
const declared = identity.poaDeclared;
const locked = identity.poaDeclared === "yes" && identity.subject === "poa";
// Only the API knows whether the lock is the freight-forwarder rule; it
// reports the answer as "yes" for them no matter what is stored, so a company
// that cannot switch to "no" is one the API will refuse. Rather than
// duplicating the role check here, the "no" card simply reports the refusal.
const passportAccepted = identity.passportAccepted;
const subject = declared === "yes" ? identity.poa : identity.owner;
const verified = subject.verified;
// Nothing below the fork renders until the person is actually established:
// a Fayda claim that came back, or the passport path deliberately chosen.
// Asking for a name before the verification runs is asking for a value the
// verification is about to overwrite.
const established = verified || method === "passport";
const who = declared === "yes" ? "Representative" : "Owner";
const passportField =
declared === "yes" ? "poaPassportNumber" : "ownerPassportNumber";
return (
<Stack gap="lg">
<Stack gap="xs">
<Text fw={600} size="lg" c="edr-text">
Does anyone hold power of attorney for this company?
</Text>
<Text size="sm" c="edr-muted">
Your answer decides whose identity we verify the representative's,
or the owner's.
</Text>
</Stack>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<RoleCard
label="Yes, we have a representative"
description="Someone other than the owner is authorised to act for the company. We'll verify their identity and ask for the DARS delegation paper."
icon={<UserCheck size={20} />}
selected={declared === "yes"}
onClick={
declarePending || declared === "yes"
? undefined
: () => onDeclare("yes")
}
/>
<RoleCard
label="No, the owner acts for us"
description="Nobody holds power of attorney. We'll verify the owner instead, and no delegation paper is needed."
icon={<UserX size={20} />}
selected={declared === "no"}
onClick={
declarePending || declared === "no"
? undefined
: () => onDeclare("no")
}
/>
</SimpleGrid>
{locked && (
<Alert color="blue" variant="light" icon={<Info size={18} />}>
As a freight forwarder you act on other companies' behalf, so a Power
of Attorney is required — this can't be set to "no" while you hold the
freight forwarder role.
</Alert>
)}
{declared === null && (
<Text size="sm" c="edr-muted">
Pick one to continue.
</Text>
)}
{/* ---------------------------------------------------------------- */}
{/* No representative → the owner is the one who verifies. */}
{/* ---------------------------------------------------------------- */}
{declared === "no" && (
{/* ------------------------------------------------------------------ */}
{/* The question. Once answered it collapses to its answer, so the step */}
{/* is about the person rather than re-presenting a settled choice. */}
{/* ------------------------------------------------------------------ */}
{declared === null ? (
<>
<Divider />
<FaydaVerifyPanel
subject="owner"
title="Company owner"
state={identity.owner}
required={!passportAccepted}
/>
{/* Fayda is an Ethiopian national ID, so a foreign company's owner
may hold none — a passport number proves them instead. Offered
alongside, not after: either one satisfies the gate. */}
{passportAccepted && !identity.owner.verified && (
<TextInput
label="Owner's Passport Number"
description="If the owner has no Fayda ID, their passport number proves their identity instead."
placeholder="P1234567"
error={errors.ownerPassportNumber?.message}
{...register("ownerPassportNumber")}
<Stack gap="xs">
<Text fw={600} size="lg" c="edr-text">
Does anyone hold power of attorney for this company?
</Text>
<Text size="sm" c="edr-muted">
Your answer decides whose identity we verify the
representative's, or the owner's.
</Text>
</Stack>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<RoleCard
label="Yes, we have a representative"
description="Someone other than the owner is authorised to act for the company. We'll verify their identity and ask for the DARS delegation paper."
icon={<UserCheck size={20} />}
selected={false}
onClick={declarePending ? undefined : () => onDeclare("yes")}
/>
)}
</>
)}
{/* ---------------------------------------------------------------- */}
{/* A representative → they verify, and the delegation is evidenced. */}
{/* ---------------------------------------------------------------- */}
{declared === "yes" && (
<>
<Divider />
<FaydaVerifyPanel
subject="poa"
title="Power of Attorney"
state={identity.poa}
required={!passportAccepted}
/>
{passportAccepted && !identity.poa.verified && (
<TextInput
label="Representative's Passport Number"
description="If your representative has no Fayda ID, their passport number proves their identity instead."
placeholder="P1234567"
error={errors.poaPassportNumber?.message}
{...register("poaPassportNumber")}
<RoleCard
label="No, the owner acts for us"
description="Nobody holds power of attorney. We'll verify the owner instead, and no delegation paper is needed."
icon={<UserX size={20} />}
selected={false}
onClick={declarePending ? undefined : () => onDeclare("no")}
/>
)}
{/* Whatever the Fayda claim carried is shown on the panel above and
never typed here — the verification owns it. The rest is asked
for outright, because the API demands name/email/phone from any
declared representative (`REQUIRED_POA_FIELDS`). */}
<SourcedField
label="Representative's Name"
value={gaps.name ? "" : identity.poa.name}
source="Fayda"
>
<TextInput
label="Representative's Name"
placeholder="Abebe Bikila"
error={errors.poaName?.message}
{...register("poaName")}
/>
</SourcedField>
<SimpleGrid
cols={{ base: 1, sm: gaps.email && gaps.phone ? 2 : 1 }}
spacing="md"
>
<SourcedField
label="Representative's Email"
value={gaps.email ? "" : identity.poa.email}
source="Fayda"
>
<TextInput
label="Representative's Email"
type="email"
placeholder="representative@company.com"
error={errors.poaEmail?.message}
{...register("poaEmail")}
/>
</SourcedField>
<SourcedField
label="Representative's Phone"
value={gaps.phone ? "" : identity.poa.phone}
source="Fayda"
>
<ControlledPhoneField
control={control}
name="poaPhone"
label="Representative's Phone"
/>
</SourcedField>
</SimpleGrid>
</>
) : (
<ChoiceSummary
icon={declared === "yes" ? <UserCheck size={18} /> : <UserX size={18} />}
label={
declared === "yes"
? "A representative holds power of attorney"
: "The owner acts for the company"
}
detail={
declared === "yes"
? "We'll verify their identity and ask for the DARS delegation paper."
: "Nobody holds power of attorney, so we verify the owner."
}
onChange={
declarePending || declarationLocked
? undefined
: () => onDeclare(declared === "yes" ? "no" : "yes")
}
changeLabel={declared === "yes" ? "We have no representative" : "We have a representative"}
/>
)}
{gaps.address && (
<TextInput
label="Representative's Location"
placeholder="City, Country"
error={errors.poaLocation?.message}
{...register("poaLocation")}
/>
{declared !== null && (
<>
<Divider />
{/* -------------------------------------------------------------- */}
{/* How the person is proved. Ethiopian: Fayda, no choice. Foreign: */}
{/* Fayda or a passport — one or the other, picked outright. */}
{/* -------------------------------------------------------------- */}
{passportAccepted && !verified && method === null ? (
<Stack gap="xs">
<Text fw={600} c="edr-text">
How would you like to prove {who.toLowerCase()}'s identity?
</Text>
<Text size="sm" c="edr-muted">
Either one is enough — you don't need both.
</Text>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md" mt="xs">
<RoleCard
label="Verify with Fayda"
description="Their name, phone and address come straight from the national ID — nothing to type."
icon={<ShieldCheck size={20} />}
selected={false}
onClick={() => onMethodChange("fayda")}
/>
<RoleCard
label="Use a passport instead"
description="For someone who holds no Fayda ID. You'll enter their passport number and details yourself."
icon={<IdCard size={20} />}
selected={false}
onClick={() => onMethodChange("passport")}
/>
</SimpleGrid>
</Stack>
) : (
<>
{(method === "fayda" || !passportAccepted || verified) && (
<FaydaVerifyPanel
subject={declared === "yes" ? "poa" : "owner"}
title={declared === "yes" ? "Power of Attorney" : "Company owner"}
state={subject}
required={!passportAccepted}
/>
)}
{passportAccepted && !verified && method === "passport" && (
<TextInput
label={`${who}'s Passport Number`}
description="Fayda is an Ethiopian national ID, so a passport number proves this person instead."
placeholder="P1234567"
error={errors[passportField]?.message}
{...register(passportField)}
/>
)}
{passportAccepted && !verified && method !== null && (
<Button
variant="subtle"
size="compact-xs"
style={{ alignSelf: "flex-start" }}
onClick={() =>
onMethodChange(method === "fayda" ? "passport" : "fayda")
}
>
{method === "fayda"
? "Use a passport instead"
: "Verify with Fayda instead"}
</Button>
)}
</>
)}
{poaDocumentSetting && (
{/* -------------------------------------------------------------- */}
{/* The representative's own details — only once the person exists, */}
{/* and only the parts the verification did not already carry. What */}
{/* Fayda supplied is shown on the panel above and never repeated. */}
{/* -------------------------------------------------------------- */}
{declared === "yes" && established && (
<>
<Divider my="sm" />
<Text size="sm" c="edr-muted">
Upload the delegation paper authenticated by DARS. It is what
evidences that this person was actually delegated.
</Text>
<SmartFileInput
file={poaDocumentSetting}
value={documentFiles}
uploadedKeys={uploadedDocumentKeys}
errors={documentFieldErrors}
onChange={onDocumentFilesChange}
/>
<SourcedField
label="Representative's Name"
value={identity.poa.name}
source="Fayda"
locked={locked.name}
>
<TextInput
label="Representative's Name"
placeholder="Abebe Bikila"
error={errors.poaName?.message}
{...register("poaName")}
/>
</SourcedField>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<SourcedField
label="Representative's Email"
value={identity.poa.email}
source="Fayda"
locked={locked.email}
>
<TextInput
label="Representative's Email"
type="email"
placeholder="representative@company.com"
error={errors.poaEmail?.message}
{...register("poaEmail")}
/>
</SourcedField>
<SourcedField
label="Representative's Phone"
value={identity.poa.phone}
source="Fayda"
locked={locked.phone}
>
<ControlledPhoneField
control={control}
name="poaPhone"
label="Representative's Phone"
/>
</SourcedField>
</SimpleGrid>
{!locked.address && (
<TextInput
label="Representative's Location"
placeholder="City, Country"
error={errors.poaLocation?.message}
{...register("poaLocation")}
/>
)}
{poaDocumentSetting && (
<>
<Divider my="sm" />
<SmartFileInput
file={poaDocumentSetting}
value={documentFiles}
uploadedKeys={uploadedDocumentKeys}
errors={documentFieldErrors}
onChange={onDocumentFilesChange}
/>
</>
)}
</>
)}
</>
@@ -271,3 +329,41 @@ export default function RepresentationStep({
</Stack>
);
}
/** A settled choice, shown as its answer with a way back to the question. */
function ChoiceSummary({
icon,
label,
detail,
onChange,
changeLabel,
}: {
icon: React.ReactNode;
label: string;
detail: string;
onChange?: () => void;
changeLabel: string;
}) {
return (
<Card padding="md" radius="md" withBorder>
<Group justify="space-between" align="flex-start" wrap="nowrap">
<Group gap="sm" align="flex-start" wrap="nowrap">
<span style={{ display: "flex", marginTop: 2 }}>{icon}</span>
<Stack gap={2}>
<Text fw={600} size="sm" c="edr-text">
{label}
</Text>
<Text size="xs" c="edr-muted">
{detail}
</Text>
</Stack>
</Group>
{onChange && (
<Button variant="subtle" size="compact-xs" onClick={onChange}>
{changeLabel}
</Button>
)}
</Group>
</Card>
);
}

View File

@@ -69,6 +69,15 @@ export default function TabOwner({
// Only the person the declaration points at carries the verification, so the
// panel is offered here only when that person is the owner.
const ownerIsSubject = identity?.subject === "owner";
// A Fayda verification owns what its claims filled — the API refuses to
// overwrite those, so they show read-only. Anything it left blank stays
// editable here, whatever value is currently stored.
const ownerVerified = owner?.verified ?? false;
const ownerLocked = {
name: ownerVerified && Boolean(owner?.name?.trim()),
email: ownerVerified && Boolean(owner?.email?.trim()),
phone: ownerVerified && Boolean(owner?.phone?.trim()),
};
const {
register,
@@ -147,7 +156,12 @@ export default function TabOwner({
/>
)}
<SourcedField label="Name" value={owner?.name} source="Fayda">
<SourcedField
label="Name"
value={owner?.name}
source="Fayda"
locked={ownerLocked.name}
>
<TextInput
label="Name"
placeholder="Abebe Bikila"
@@ -158,7 +172,12 @@ export default function TabOwner({
<Grid>
<Grid.Col span={{ base: 12, sm: 6 }}>
<SourcedField label="Email" value={owner?.email} source="Fayda">
<SourcedField
label="Email"
value={owner?.email}
source="Fayda"
locked={ownerLocked.email}
>
<TextInput
label="Email"
type="email"
@@ -169,7 +188,12 @@ export default function TabOwner({
</SourcedField>
</Grid.Col>
<Grid.Col span={{ base: 12, sm: 6 }}>
<SourcedField label="Phone" value={owner?.phone} source="Fayda">
<SourcedField
label="Phone"
value={owner?.phone}
source="Fayda"
locked={ownerLocked.phone}
>
<ControlledPhoneField
control={control}
name="ownerPhone"

View File

@@ -288,9 +288,11 @@ export default function TabPowerOfAttorney({
)}
</Group>
<Text c="edr-muted" size="sm" mb="lg">
{requirePoa
? "As a freight forwarder you act on other companies' behalf, so a Power of Attorney is required."
: "Tell us whether anyone is authorised to act for the company — your answer decides whose identity we verify."}{" "}
{/* The "Required for freight forwarder" badge above already says why
a forwarder has no choice here; repeating it in prose was a
lecture, not information. */}
{!requirePoa &&
"Tell us whether anyone is authorised to act for the company — your answer decides whose identity we verify. "}
{declared === "no"
? "The owner acts for the company, so no delegation paper is needed."
: "A representative must be identified, and the DARS delegation paper authorising them uploaded."}