feat: update onboarding and company role handling to support multiple service types

This commit is contained in:
Marshal
2026-06-22 12:30:44 +00:00
parent ad0043b580
commit 30b022356c
5 changed files with 52 additions and 49 deletions

View File

@@ -1,6 +1,6 @@
import { Modal, ScrollArea, Stack, Text } from "@mantine/core";
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 { api } from "@/services/api";
@@ -13,9 +13,7 @@ import { companiesService } from "@/services/companies.service";
import type { UpdateProfilePayload } from "@/types/profile";
import { extractApiError } from "@/utils/result";
import CompanyProfileForm from "@/pages/accounts/CompanyProfileForm";
import ForwarderForm from "@/pages/accounts/ForwarderForm";
import type { RoleLicenseProfile } from "@/components/onboarding/RoleLicenseStep";
import { FREIGHT_FORWARDER } from "@/pages/settings/companyRoles";
import NationalitySelect from "@/pages/settings/NationalitySelect";
import OnboardingRoleSelect from "@/pages/settings/OnboardingRoleSelect";
@@ -42,9 +40,14 @@ interface OnboardingWizardDialogProps {
onClose: () => void;
}
/** Map the chosen operational roles to the company type they belong to. */
function companyTypeForRoles(roles: string[]): string {
return roles.includes(FREIGHT_FORWARDER.type) ? "forwarder" : "customer";
/**
* The company type for the onboarding selection. Importer / Exporter / Freight
* 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. */
@@ -161,6 +164,23 @@ export default function OnboardingWizardDialog({
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(() => {
if (nationality) setPhase("role");
}, [nationality]);
@@ -205,8 +225,7 @@ export default function OnboardingWizardDialog({
if (!user) return null;
const isForwarder = roles.includes(FREIGHT_FORWARDER.type);
// Importer+Exporter (or either alone) is a valid customer selection.
// Any non-empty combination of importer/exporter/freight-forwarder is valid.
const rolesValid = roles.length > 0;
// Documents depend on nationality; fall back to the saved one (resume) then ethiopian.
const effectiveNationality: CompanyNationality =
@@ -293,8 +312,6 @@ export default function OnboardingWizardDialog({
onClick={handleRolesContinue}
/>
</Stack>
) : isForwarder ? (
<ForwarderForm {...formProps} />
) : (
<CompanyProfileForm {...formProps} />
)}

View File

@@ -406,6 +406,10 @@ export default function CompanyProfileForm({
const [contactIsPoa, setContactIsPoa] = useState(false);
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("statusDescription", data.statusDescription);
setValue("dateRegistered", data.dateRegistered);
@@ -673,11 +677,10 @@ export default function CompanyProfileForm({
/>
</SimpleGrid>
{watch("licenceNumber") && (
<>
<>
<Divider my="sm" />
<Text fw={600} size="sm" c="edr-text">
Registration Details from eTrade
Registration Details
</Text>
<SimpleGrid cols={2} spacing="md">
<TextInput
@@ -768,7 +771,6 @@ export default function CompanyProfileForm({
/>
</SimpleGrid>
</>
)}
</>
)}

View File

@@ -72,7 +72,7 @@ export default function CompanyRolesCard({ profile }: CompanyRolesCardProps) {
</Group>
<Text c="edr-muted" size="sm" mb="lg">
{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."}
</Text>

View File

@@ -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 RoleCard from "./RoleCard";
import { CUSTOMER_ROLES, FREIGHT_FORWARDER } from "./companyRoles";
import { CUSTOMER_ROLES } from "./companyRoles";
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[];
onChange: (next: string[]) => void;
}
/**
* First (and only) thing shown in the Company Profile tab during onboarding.
* Importer / Exporter sit side by side and can both be picked; Freight
* Forwarder is a separate, mutually-exclusive choice below them. A valid
* selection reveals the company-profile fields.
* Importer / Exporter / Freight Forwarder are independent services that can be
* picked in any combination — each becomes its own profile (with its own
* business license) under the same company. A valid selection reveals the
* company-profile fields.
*/
export default function OnboardingRoleSelect({
value,
onChange,
}: OnboardingRoleSelectProps) {
const selected = new Set(value);
const isForwarder = selected.has(FREIGHT_FORWARDER.type);
// Toggling a customer role drops any forwarder selection (mutually exclusive).
const toggleCustomerRole = (type: string) => {
const next = new Set(value.filter((t) => t !== FREIGHT_FORWARDER.type));
const toggleRole = (type: string) => {
const next = new Set(value);
if (next.has(type)) next.delete(type);
else next.add(type);
onChange([...next]);
};
const toggleForwarder = () => {
onChange(isForwarder ? [] : [FREIGHT_FORWARDER.type]);
};
return (
<Card padding="lg">
<Group gap="sm" mb="xs">
@@ -41,7 +36,8 @@ export default function OnboardingRoleSelect({
<Title order={3}>What does your company do?</Title>
</Group>
<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>
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
@@ -52,26 +48,10 @@ export default function OnboardingRoleSelect({
description={role.description}
icon={role.icon}
selected={selected.has(role.type)}
onClick={() => toggleCustomerRole(role.type)}
onClick={() => toggleRole(role.type)}
/>
))}
</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>
);
}

View File

@@ -28,8 +28,12 @@ export const FREIGHT_FORWARDER: RoleMeta = {
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.
export function rolesForCompanyType(companyType: string): RoleMeta[] {