feat: enhance service profile handling for customer companies

This commit is contained in:
Marshal
2026-06-22 12:11:10 +00:00
parent 3e45d694da
commit ad0043b580
2 changed files with 92 additions and 32 deletions

View File

@@ -585,7 +585,13 @@ export class CompaniesService {
private getProfileTypeForCompanyType(companyType: string): ProfileType[] { private getProfileTypeForCompanyType(companyType: string): ProfileType[] {
switch (companyType) { switch (companyType) {
case "customer": 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": case "freight_forwarder":
return [ProfileType.freightForwarder]; return [ProfileType.freightForwarder];
case "dj_freight_forwarder": case "dj_freight_forwarder":

View File

@@ -66,16 +66,24 @@ export interface AppLayoutProps {
/** The active operational mode (importer/exporter/...). */ /** The active operational mode (importer/exporter/...). */
activeProfileType?: string | null; activeProfileType?: string | null;
/** Switch to an existing profile of the given type. */ /** Switch to an existing profile of the given type. */
onSwitchMode?: (type: ImporterExporter) => Promise<SwitchResult> | void; onSwitchMode?: (type: ServiceType) => Promise<SwitchResult> | void;
/** Create the profile of the given type (with business license) then switch. */ /** Create the profile of the given type (with business license) then switch. */
onCreateProfile?: ( onCreateProfile?: (
type: ImporterExporter, type: ServiceType,
licenseFiles: File[], licenseFiles: File[],
) => Promise<SwitchResult> | void; ) => Promise<SwitchResult> | void;
children: ReactNode; 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 = type SwitchResult =
| { success: true; data?: unknown } | { success: true; data?: unknown }
| { success: false; error?: { message?: string } }; | { success: false; error?: { message?: string } };
@@ -166,29 +174,35 @@ export function AppLayout({
const initials = getInitials(userName); const initials = getInitials(userName);
const activePage = getActivePage(sidebarItems, activePath); 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 isCustomer = companyType === "customer";
const targetMode: ImporterExporter =
activeProfileType === "importer" ? "exporter" : "importer";
const targetExists = companyProfiles.some((p) => p.type === targetMode);
const canSwitch = const canSwitch =
isCustomer && 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 [switching, setSwitching] = useState(false);
const [createOpen, setCreateOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false);
const [createTarget, setCreateTarget] = useState<ServiceType>("importer");
const [licenseFiles, setLicenseFiles] = useState<File[]>([]); const [licenseFiles, setLicenseFiles] = useState<File[]>([]);
const [createError, setCreateError] = useState<string | null>(null); const [createError, setCreateError] = useState<string | null>(null);
const handleSwitchClick = async () => { const handleSelectService = async (type: ServiceType) => {
if (targetExists) { if (type === activeProfileType) return;
if (profileExists(type)) {
setSwitching(true); setSwitching(true);
try { try {
await onSwitchMode?.(targetMode); await onSwitchMode?.(type);
} finally { } finally {
setSwitching(false); setSwitching(false);
} }
} else { } else {
// No profile yet — collect a business license, then create + switch.
setCreateTarget(type);
setLicenseFiles([]); setLicenseFiles([]);
setCreateError(null); setCreateError(null);
setCreateOpen(true); setCreateOpen(true);
@@ -203,7 +217,7 @@ export function AppLayout({
setSwitching(true); setSwitching(true);
setCreateError(null); setCreateError(null);
try { try {
const res = await onCreateProfile?.(targetMode, licenseFiles); const res = await onCreateProfile?.(createTarget, licenseFiles);
if (res && !res.success) { if (res && !res.success) {
setCreateError(res.error?.message ?? "Failed to create profile"); setCreateError(res.error?.message ?? "Failed to create profile");
return; return;
@@ -214,8 +228,7 @@ export function AppLayout({
} }
}; };
const modeLabel = (m: ImporterExporter) => const serviceLabel = (m: ServiceType) => PROFILE_TYPE_LABELS[m] ?? m;
m === "importer" ? "Importer" : "Exporter";
const isItemActive = (item: SidebarItem) => const isItemActive = (item: SidebarItem) =>
activePath === item.href.toLowerCase() || activePath === item.href.toLowerCase() ||
@@ -289,21 +302,62 @@ export function AppLayout({
{/* Right: switch + search + bell + avatar */} {/* Right: switch + search + bell + avatar */}
<Group gap={10} wrap="nowrap" align="center"> <Group gap={10} wrap="nowrap" align="center">
{/* Importer/Exporter mode switch (customer companies only) */} {/* Service selector (customer companies only) */}
{canSwitch && ( {canSwitch && (
<Button <Menu
onClick={handleSwitchClick} width={220}
loading={switching} position="bottom-end"
variant="light" withinPortal
color="edr-green" shadow="md"
radius={999} offset={8}
size="sm" radius="md"
leftSection={<ArrowLeftRight size={15} strokeWidth={1.8} />}
styles={{ root: { height: 36 } }}
visibleFrom="xs"
> >
Switch to {modeLabel(targetMode)} <Menu.Target>
</Button> <Button
loading={switching}
variant="light"
color="edr-green"
radius={999}
size="sm"
leftSection={<ArrowLeftRight size={15} strokeWidth={1.8} />}
rightSection={<ChevronDown size={14} strokeWidth={1.8} />}
styles={{ root: { height: 36 } }}
visibleFrom="xs"
>
{serviceLabel(activeProfileType as ServiceType)}
</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Select service</Menu.Label>
{CUSTOMER_SERVICES.map((type) => {
const isActive = type === activeProfileType;
const exists = profileExists(type);
return (
<Menu.Item
key={type}
onClick={() => handleSelectService(type)}
leftSection={
isActive ? (
<Check size={15} strokeWidth={2} />
) : exists ? (
<ArrowLeftRight size={15} strokeWidth={1.8} />
) : (
<Plus size={15} strokeWidth={1.8} />
)
}
disabled={isActive}
>
{serviceLabel(type)}
{!exists && (
<Text span size="xs" c="dimmed" ml={6}>
(set up)
</Text>
)}
</Menu.Item>
);
})}
</Menu.Dropdown>
</Menu>
)} )}
{/* Search pill */} {/* Search pill */}
@@ -798,15 +852,15 @@ export function AppLayout({
<Modal <Modal
opened={createOpen} opened={createOpen}
onClose={() => (switching ? undefined : setCreateOpen(false))} onClose={() => (switching ? undefined : setCreateOpen(false))}
title={`Set up your ${modeLabel(targetMode)} profile`} title={`Set up your ${serviceLabel(createTarget)} profile`}
centered centered
radius="lg" radius="lg"
> >
<Stack gap="md"> <Stack gap="md">
<Text size="sm" c="dimmed"> <Text size="sm" c="dimmed">
You don't have an {modeLabel(targetMode).toLowerCase()} profile yet. You don't have a {serviceLabel(createTarget).toLowerCase()} profile
Add your business license to create one and switch to{" "} yet. Add your business license to create one and switch to{" "}
{modeLabel(targetMode).toLowerCase()} mode. {serviceLabel(createTarget).toLowerCase()}.
</Text> </Text>
<FileInput <FileInput
label="Business license" label="Business license"