mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
feat: enhance service profile handling for customer companies
This commit is contained in:
@@ -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":
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user