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[] {
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":

View File

@@ -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<SwitchResult> | void;
onSwitchMode?: (type: ServiceType) => Promise<SwitchResult> | void;
/** Create the profile of the given type (with business license) then switch. */
onCreateProfile?: (
type: ImporterExporter,
type: ServiceType,
licenseFiles: File[],
) => Promise<SwitchResult> | 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<ServiceType>("importer");
const [licenseFiles, setLicenseFiles] = useState<File[]>([]);
const [createError, setCreateError] = useState<string | null>(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 */}
<Group gap={10} wrap="nowrap" align="center">
{/* Importer/Exporter mode switch (customer companies only) */}
{/* Service selector (customer companies only) */}
{canSwitch && (
<Button
onClick={handleSwitchClick}
loading={switching}
variant="light"
color="edr-green"
radius={999}
size="sm"
leftSection={<ArrowLeftRight size={15} strokeWidth={1.8} />}
styles={{ root: { height: 36 } }}
visibleFrom="xs"
<Menu
width={220}
position="bottom-end"
withinPortal
shadow="md"
offset={8}
radius="md"
>
Switch to {modeLabel(targetMode)}
</Button>
<Menu.Target>
<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 */}
@@ -798,15 +852,15 @@ export function AppLayout({
<Modal
opened={createOpen}
onClose={() => (switching ? undefined : setCreateOpen(false))}
title={`Set up your ${modeLabel(targetMode)} profile`}
title={`Set up your ${serviceLabel(createTarget)} profile`}
centered
radius="lg"
>
<Stack gap="md">
<Text size="sm" c="dimmed">
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()}.
</Text>
<FileInput
label="Business license"