mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 08:25:43 +00:00
feat: update onboarding and company role handling to support multiple service types
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { Modal, ScrollArea, Stack, Text } from "@mantine/core";
|
import { Modal, ScrollArea, Stack, Text } from "@mantine/core";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useCallback, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import useAuth from "@/hooks/useAuth";
|
import useAuth from "@/hooks/useAuth";
|
||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
@@ -13,9 +13,7 @@ import { companiesService } from "@/services/companies.service";
|
|||||||
import type { UpdateProfilePayload } from "@/types/profile";
|
import type { UpdateProfilePayload } from "@/types/profile";
|
||||||
import { extractApiError } from "@/utils/result";
|
import { extractApiError } from "@/utils/result";
|
||||||
import CompanyProfileForm from "@/pages/accounts/CompanyProfileForm";
|
import CompanyProfileForm from "@/pages/accounts/CompanyProfileForm";
|
||||||
import ForwarderForm from "@/pages/accounts/ForwarderForm";
|
|
||||||
import type { RoleLicenseProfile } from "@/components/onboarding/RoleLicenseStep";
|
import type { RoleLicenseProfile } from "@/components/onboarding/RoleLicenseStep";
|
||||||
import { FREIGHT_FORWARDER } from "@/pages/settings/companyRoles";
|
|
||||||
import NationalitySelect from "@/pages/settings/NationalitySelect";
|
import NationalitySelect from "@/pages/settings/NationalitySelect";
|
||||||
import OnboardingRoleSelect from "@/pages/settings/OnboardingRoleSelect";
|
import OnboardingRoleSelect from "@/pages/settings/OnboardingRoleSelect";
|
||||||
|
|
||||||
@@ -42,9 +40,14 @@ interface OnboardingWizardDialogProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Map the chosen operational roles to the company type they belong to. */
|
/**
|
||||||
function companyTypeForRoles(roles: string[]): string {
|
* The company type for the onboarding selection. Importer / Exporter / Freight
|
||||||
return roles.includes(FREIGHT_FORWARDER.type) ? "forwarder" : "customer";
|
* Forwarder are all services a single "customer" company can hold (in any
|
||||||
|
* combination), each with its own business license — so the company is always
|
||||||
|
* registered as a "customer".
|
||||||
|
*/
|
||||||
|
function companyTypeForRoles(_roles: string[]): string {
|
||||||
|
return "customer";
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Document upload setting code per company nationality. */
|
/** Document upload setting code per company nationality. */
|
||||||
@@ -161,6 +164,23 @@ export default function OnboardingWizardDialog({
|
|||||||
api.companies.setOnboardingStep.call({ step }).catch(() => {});
|
api.companies.setOnboardingStep.call({ step }).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// The company query may resolve AFTER this dialog mounts (it's kept mounted by
|
||||||
|
// the gate), so the phase/roles/nationality initial state can be stale — a
|
||||||
|
// draft that already exists would otherwise leave us stuck on the first
|
||||||
|
// (nationality) phase. Once a draft loads, jump straight into the form with
|
||||||
|
// the persisted roles/nationality. Runs once per resumed draft.
|
||||||
|
const resumedRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!companyAlreadyStarted || resumedRef.current) return;
|
||||||
|
resumedRef.current = true;
|
||||||
|
setRoles(existingProfiles.map((p) => p.type));
|
||||||
|
setNationality(savedNationality);
|
||||||
|
setPhase("form");
|
||||||
|
const idx = FORM_STEPS.indexOf(resumeFormStep);
|
||||||
|
if (idx > furthestIdxRef.current) furthestIdxRef.current = idx;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [companyAlreadyStarted, resumeFormStep]);
|
||||||
|
|
||||||
const handleNationalityContinue = useCallback(() => {
|
const handleNationalityContinue = useCallback(() => {
|
||||||
if (nationality) setPhase("role");
|
if (nationality) setPhase("role");
|
||||||
}, [nationality]);
|
}, [nationality]);
|
||||||
@@ -205,8 +225,7 @@ export default function OnboardingWizardDialog({
|
|||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
const isForwarder = roles.includes(FREIGHT_FORWARDER.type);
|
// Any non-empty combination of importer/exporter/freight-forwarder is valid.
|
||||||
// Importer+Exporter (or either alone) is a valid customer selection.
|
|
||||||
const rolesValid = roles.length > 0;
|
const rolesValid = roles.length > 0;
|
||||||
// Documents depend on nationality; fall back to the saved one (resume) then ethiopian.
|
// Documents depend on nationality; fall back to the saved one (resume) then ethiopian.
|
||||||
const effectiveNationality: CompanyNationality =
|
const effectiveNationality: CompanyNationality =
|
||||||
@@ -293,8 +312,6 @@ export default function OnboardingWizardDialog({
|
|||||||
onClick={handleRolesContinue}
|
onClick={handleRolesContinue}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
) : isForwarder ? (
|
|
||||||
<ForwarderForm {...formProps} />
|
|
||||||
) : (
|
) : (
|
||||||
<CompanyProfileForm {...formProps} />
|
<CompanyProfileForm {...formProps} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -406,6 +406,10 @@ export default function CompanyProfileForm({
|
|||||||
const [contactIsPoa, setContactIsPoa] = useState(false);
|
const [contactIsPoa, setContactIsPoa] = useState(false);
|
||||||
|
|
||||||
const handleETradeDataLoaded = (data: CompanyRegistrationData) => {
|
const handleETradeDataLoaded = (data: CompanyRegistrationData) => {
|
||||||
|
// Company name comes from the eTrade manager/owner name on the license.
|
||||||
|
if (data.managerName) {
|
||||||
|
setValue("companyName", data.managerName, { shouldValidate: true });
|
||||||
|
}
|
||||||
setValue("licenceNumber", data.licenceNumber);
|
setValue("licenceNumber", data.licenceNumber);
|
||||||
setValue("statusDescription", data.statusDescription);
|
setValue("statusDescription", data.statusDescription);
|
||||||
setValue("dateRegistered", data.dateRegistered);
|
setValue("dateRegistered", data.dateRegistered);
|
||||||
@@ -673,11 +677,10 @@ export default function CompanyProfileForm({
|
|||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
{watch("licenceNumber") && (
|
<>
|
||||||
<>
|
|
||||||
<Divider my="sm" />
|
<Divider my="sm" />
|
||||||
<Text fw={600} size="sm" c="edr-text">
|
<Text fw={600} size="sm" c="edr-text">
|
||||||
Registration Details from eTrade
|
Registration Details
|
||||||
</Text>
|
</Text>
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
@@ -768,7 +771,6 @@ export default function CompanyProfileForm({
|
|||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
</>
|
</>
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export default function CompanyRolesCard({ profile }: CompanyRolesCardProps) {
|
|||||||
</Group>
|
</Group>
|
||||||
<Text c="edr-muted" size="sm" mb="lg">
|
<Text c="edr-muted" size="sm" mb="lg">
|
||||||
{profile.companyType === "customer"
|
{profile.companyType === "customer"
|
||||||
? "Select the role(s) your company operates as — importer, exporter, or both."
|
? "Select the service(s) your company operates as — importer, exporter and/or freight forwarder."
|
||||||
: "Your company's operational role."}
|
: "Your company's operational role."}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
|||||||
@@ -1,39 +1,34 @@
|
|||||||
import { Card, Divider, Group, SimpleGrid, Text, Title } from "@mantine/core";
|
import { Card, Group, SimpleGrid, Text, Title } from "@mantine/core";
|
||||||
import { Building2 } from "lucide-react";
|
import { Building2 } from "lucide-react";
|
||||||
import RoleCard from "./RoleCard";
|
import RoleCard from "./RoleCard";
|
||||||
import { CUSTOMER_ROLES, FREIGHT_FORWARDER } from "./companyRoles";
|
import { CUSTOMER_ROLES } from "./companyRoles";
|
||||||
|
|
||||||
interface OnboardingRoleSelectProps {
|
interface OnboardingRoleSelectProps {
|
||||||
/** Currently selected profile types (e.g. ["importer"], ["importer","exporter"], ["freight_forwarder"]). */
|
/** Currently selected profile types (e.g. ["importer"], ["importer","exporter","freight_forwarder"]). */
|
||||||
value: string[];
|
value: string[];
|
||||||
onChange: (next: string[]) => void;
|
onChange: (next: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* First (and only) thing shown in the Company Profile tab during onboarding.
|
* First (and only) thing shown in the Company Profile tab during onboarding.
|
||||||
* Importer / Exporter sit side by side and can both be picked; Freight
|
* Importer / Exporter / Freight Forwarder are independent services that can be
|
||||||
* Forwarder is a separate, mutually-exclusive choice below them. A valid
|
* picked in any combination — each becomes its own profile (with its own
|
||||||
* selection reveals the company-profile fields.
|
* business license) under the same company. A valid selection reveals the
|
||||||
|
* company-profile fields.
|
||||||
*/
|
*/
|
||||||
export default function OnboardingRoleSelect({
|
export default function OnboardingRoleSelect({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
}: OnboardingRoleSelectProps) {
|
}: OnboardingRoleSelectProps) {
|
||||||
const selected = new Set(value);
|
const selected = new Set(value);
|
||||||
const isForwarder = selected.has(FREIGHT_FORWARDER.type);
|
|
||||||
|
|
||||||
// Toggling a customer role drops any forwarder selection (mutually exclusive).
|
const toggleRole = (type: string) => {
|
||||||
const toggleCustomerRole = (type: string) => {
|
const next = new Set(value);
|
||||||
const next = new Set(value.filter((t) => t !== FREIGHT_FORWARDER.type));
|
|
||||||
if (next.has(type)) next.delete(type);
|
if (next.has(type)) next.delete(type);
|
||||||
else next.add(type);
|
else next.add(type);
|
||||||
onChange([...next]);
|
onChange([...next]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleForwarder = () => {
|
|
||||||
onChange(isForwarder ? [] : [FREIGHT_FORWARDER.type]);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<Group gap="sm" mb="xs">
|
<Group gap="sm" mb="xs">
|
||||||
@@ -41,7 +36,8 @@ export default function OnboardingRoleSelect({
|
|||||||
<Title order={3}>What does your company do?</Title>
|
<Title order={3}>What does your company do?</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Text c="edr-muted" size="sm" mb="lg">
|
<Text c="edr-muted" size="sm" mb="lg">
|
||||||
Pick Importer, Exporter, or both — or register as a Freight Forwarder.
|
Pick any combination of Importer, Exporter and Freight Forwarder — each
|
||||||
|
is set up with its own business license.
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||||
@@ -52,26 +48,10 @@ export default function OnboardingRoleSelect({
|
|||||||
description={role.description}
|
description={role.description}
|
||||||
icon={role.icon}
|
icon={role.icon}
|
||||||
selected={selected.has(role.type)}
|
selected={selected.has(role.type)}
|
||||||
onClick={() => toggleCustomerRole(role.type)}
|
onClick={() => toggleRole(role.type)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
|
||||||
<Divider
|
|
||||||
label="or"
|
|
||||||
labelPosition="center"
|
|
||||||
my="lg"
|
|
||||||
c="edr-muted"
|
|
||||||
styles={{ label: { textTransform: "uppercase", fontSize: 11 } }}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<RoleCard
|
|
||||||
label={FREIGHT_FORWARDER.label}
|
|
||||||
description={FREIGHT_FORWARDER.description}
|
|
||||||
icon={FREIGHT_FORWARDER.icon}
|
|
||||||
selected={isForwarder}
|
|
||||||
onClick={toggleForwarder}
|
|
||||||
/>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,12 @@ export const FREIGHT_FORWARDER: RoleMeta = {
|
|||||||
icon: <Building2 size={22} />,
|
icon: <Building2 size={22} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Importer / Exporter — the two roles a "customer" company can hold. */
|
/**
|
||||||
export const CUSTOMER_ROLES: RoleMeta[] = [IMPORTER, EXPORTER];
|
* Importer / Exporter / Freight Forwarder — the services a "customer" company
|
||||||
|
* can hold. A single company may register for any combination, each getting its
|
||||||
|
* own business license.
|
||||||
|
*/
|
||||||
|
export const CUSTOMER_ROLES: RoleMeta[] = [IMPORTER, EXPORTER, FREIGHT_FORWARDER];
|
||||||
|
|
||||||
// dj_freight_forwarder and transporter are intentionally not exposed yet.
|
// dj_freight_forwarder and transporter are intentionally not exposed yet.
|
||||||
export function rolesForCompanyType(companyType: string): RoleMeta[] {
|
export function rolesForCompanyType(companyType: string): RoleMeta[] {
|
||||||
|
|||||||
Reference in New Issue
Block a user