mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +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 { 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} />
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
Reference in New Issue
Block a user