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 { 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} />
)} )}

View File

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

View File

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

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 { 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>
); );
} }

View File

@@ -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[] {