mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +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[] {
|
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":
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user