From ad0043b580fa9d4d0068cfb63c5818a50085985f Mon Sep 17 00:00:00 2001 From: Marshal Date: Mon, 22 Jun 2026 12:11:10 +0000 Subject: [PATCH] feat: enhance service profile handling for customer companies --- .../modules/companies/companies.service.ts | 8 +- .../portal/src/components/AppLayout.tsx | 116 +++++++++++++----- 2 files changed, 92 insertions(+), 32 deletions(-) diff --git a/apps/edr-freight-api/src/modules/companies/companies.service.ts b/apps/edr-freight-api/src/modules/companies/companies.service.ts index 78525d2c9..9c4bba5f4 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -585,7 +585,13 @@ export class CompaniesService { private getProfileTypeForCompanyType(companyType: string): ProfileType[] { switch (companyType) { case "customer": - return [ProfileType.importer, ProfileType.exporter]; + // A customer can operate as an importer and/or exporter, and may also + // add a freight-forwarder service profile under the same company. + return [ + ProfileType.importer, + ProfileType.exporter, + ProfileType.freightForwarder, + ]; case "freight_forwarder": return [ProfileType.freightForwarder]; case "dj_freight_forwarder": diff --git a/apps/edr-freight-web/portal/src/components/AppLayout.tsx b/apps/edr-freight-web/portal/src/components/AppLayout.tsx index 7720f6117..7f86ead43 100644 --- a/apps/edr-freight-web/portal/src/components/AppLayout.tsx +++ b/apps/edr-freight-web/portal/src/components/AppLayout.tsx @@ -66,16 +66,24 @@ export interface AppLayoutProps { /** The active operational mode (importer/exporter/...). */ activeProfileType?: string | null; /** Switch to an existing profile of the given type. */ - onSwitchMode?: (type: ImporterExporter) => Promise | void; + onSwitchMode?: (type: ServiceType) => Promise | void; /** Create the profile of the given type (with business license) then switch. */ onCreateProfile?: ( - type: ImporterExporter, + type: ServiceType, licenseFiles: File[], ) => Promise | void; children: ReactNode; } -type ImporterExporter = "importer" | "exporter"; +/** Service profiles a customer company can operate under and switch between. */ +type ServiceType = "importer" | "exporter" | "freight_forwarder"; + +/** Services a customer company can select in the header. */ +const CUSTOMER_SERVICES: ServiceType[] = [ + "importer", + "exporter", + "freight_forwarder", +]; type SwitchResult = | { success: true; data?: unknown } | { success: false; error?: { message?: string } }; @@ -166,29 +174,35 @@ export function AppLayout({ const initials = getInitials(userName); const activePage = getActivePage(sidebarItems, activePath); - // ── Importer/Exporter mode switching (customer companies only) ── + // ── Service selection (customer companies only) ── + // A customer can operate as importer, exporter and/or freight forwarder, + // and switch between whichever service profiles their company has. const isCustomer = companyType === "customer"; - const targetMode: ImporterExporter = - activeProfileType === "importer" ? "exporter" : "importer"; - const targetExists = companyProfiles.some((p) => p.type === targetMode); const canSwitch = isCustomer && - (activeProfileType === "importer" || activeProfileType === "exporter"); + CUSTOMER_SERVICES.includes(activeProfileType as ServiceType); + + const profileExists = (type: ServiceType) => + companyProfiles.some((p) => p.type === type); const [switching, setSwitching] = useState(false); const [createOpen, setCreateOpen] = useState(false); + const [createTarget, setCreateTarget] = useState("importer"); const [licenseFiles, setLicenseFiles] = useState([]); const [createError, setCreateError] = useState(null); - const handleSwitchClick = async () => { - if (targetExists) { + const handleSelectService = async (type: ServiceType) => { + if (type === activeProfileType) return; + if (profileExists(type)) { setSwitching(true); try { - await onSwitchMode?.(targetMode); + await onSwitchMode?.(type); } finally { setSwitching(false); } } else { + // No profile yet — collect a business license, then create + switch. + setCreateTarget(type); setLicenseFiles([]); setCreateError(null); setCreateOpen(true); @@ -203,7 +217,7 @@ export function AppLayout({ setSwitching(true); setCreateError(null); try { - const res = await onCreateProfile?.(targetMode, licenseFiles); + const res = await onCreateProfile?.(createTarget, licenseFiles); if (res && !res.success) { setCreateError(res.error?.message ?? "Failed to create profile"); return; @@ -214,8 +228,7 @@ export function AppLayout({ } }; - const modeLabel = (m: ImporterExporter) => - m === "importer" ? "Importer" : "Exporter"; + const serviceLabel = (m: ServiceType) => PROFILE_TYPE_LABELS[m] ?? m; const isItemActive = (item: SidebarItem) => activePath === item.href.toLowerCase() || @@ -289,21 +302,62 @@ export function AppLayout({ {/* Right: switch + search + bell + avatar */} - {/* Importer/Exporter mode switch (customer companies only) */} + {/* Service selector (customer companies only) */} {canSwitch && ( - + + + + + Select service + {CUSTOMER_SERVICES.map((type) => { + const isActive = type === activeProfileType; + const exists = profileExists(type); + return ( + handleSelectService(type)} + leftSection={ + isActive ? ( + + ) : exists ? ( + + ) : ( + + ) + } + disabled={isActive} + > + {serviceLabel(type)} + {!exists && ( + + (set up) + + )} + + ); + })} + + )} {/* Search pill */} @@ -798,15 +852,15 @@ export function AppLayout({ (switching ? undefined : setCreateOpen(false))} - title={`Set up your ${modeLabel(targetMode)} profile`} + title={`Set up your ${serviceLabel(createTarget)} profile`} centered radius="lg" > - You don't have an {modeLabel(targetMode).toLowerCase()} profile yet. - Add your business license to create one and switch to{" "} - {modeLabel(targetMode).toLowerCase()} mode. + You don't have a {serviceLabel(createTarget).toLowerCase()} profile + yet. Add your business license to create one and switch to{" "} + {serviceLabel(createTarget).toLowerCase()}.