enhance service filtering and dashboard functionality with company profile support

This commit is contained in:
Marshal
2026-06-23 07:54:36 +00:00
parent fd1fc1ca69
commit 1d77f377f3
19 changed files with 206 additions and 235 deletions

View File

@@ -19,9 +19,7 @@ import {
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import {
ArrowLeftRight,
Bell,
Check,
ChevronDown,
FileSignature,
LogOut,
@@ -61,13 +59,9 @@ export interface AppLayoutProps {
userEmail?: string;
/** Operational profiles for the company — surfaced as reference chips in the account menu. */
companyProfiles?: { type: string; reference: string; status?: string }[];
/** Company type (e.g. "customer", "forwarder") — gates the importer/exporter switch. */
/** Company type (e.g. "customer", "forwarder") — gates the "Add service" control. */
companyType?: string | null;
/** The active operational mode (importer/exporter/...). */
activeProfileType?: string | null;
/** Switch to an existing profile of the given type. */
onSwitchMode?: (type: ServiceType) => Promise<SwitchResult> | void;
/** Create the profile of the given type (with business license) then switch. */
/** Create a new service profile of the given type (with business license). */
onCreateProfile?: (
type: ServiceType,
licenseFiles: File[],
@@ -146,8 +140,6 @@ export function AppLayout({
userEmail,
companyProfiles = [],
companyType,
activeProfileType,
onSwitchMode,
onCreateProfile,
children,
}: AppLayoutProps) {
@@ -174,16 +166,16 @@ export function AppLayout({
const initials = getInitials(userName);
const activePage = getActivePage(sidebarItems, activePath);
// ── 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.
// ── Add a service (customer companies only) ──
// A customer can operate as importer, exporter and/or freight forwarder. The
// header lets them ADD a service they don't have yet (creating a profile with
// its business license). Data is no longer scoped by an "active" service —
// every page shows all the company's data, with an optional per-page filter.
const isCustomer = companyType === "customer";
const canSwitch =
isCustomer &&
CUSTOMER_SERVICES.includes(activeProfileType as ServiceType);
const profileExists = (type: ServiceType) =>
companyProfiles.some((p) => p.type === type);
const addableServices = CUSTOMER_SERVICES.filter((t) => !profileExists(t));
const canAddService = isCustomer && addableServices.length > 0;
const [switching, setSwitching] = useState(false);
const [createOpen, setCreateOpen] = useState(false);
@@ -191,22 +183,12 @@ export function AppLayout({
const [licenseFiles, setLicenseFiles] = useState<File[]>([]);
const [createError, setCreateError] = useState<string | null>(null);
const handleSelectService = async (type: ServiceType) => {
if (type === activeProfileType) return;
if (profileExists(type)) {
setSwitching(true);
try {
await onSwitchMode?.(type);
} finally {
setSwitching(false);
}
} else {
// No profile yet — collect a business license, then create + switch.
setCreateTarget(type);
setLicenseFiles([]);
setCreateError(null);
setCreateOpen(true);
}
const handleAddService = (type: ServiceType) => {
// Collect a business license, then create the profile.
setCreateTarget(type);
setLicenseFiles([]);
setCreateError(null);
setCreateOpen(true);
};
const handleCreateConfirm = async () => {
@@ -302,8 +284,8 @@ export function AppLayout({
{/* Right: switch + search + bell + avatar */}
<Group gap={10} wrap="nowrap" align="center">
{/* Service selector (customer companies only) */}
{canSwitch && (
{/* Add a service (customer companies that don't yet have all three) */}
{canAddService && (
<Menu
width={220}
position="bottom-end"
@@ -319,43 +301,25 @@ export function AppLayout({
color="edr-green"
radius={999}
size="sm"
leftSection={<ArrowLeftRight size={15} strokeWidth={1.8} />}
leftSection={<Plus size={15} strokeWidth={1.8} />}
rightSection={<ChevronDown size={14} strokeWidth={1.8} />}
styles={{ root: { height: 36 } }}
visibleFrom="xs"
>
{serviceLabel(activeProfileType as ServiceType)}
Add service
</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.Label>Add a service</Menu.Label>
{addableServices.map((type) => (
<Menu.Item
key={type}
onClick={() => handleAddService(type)}
leftSection={<Plus size={15} strokeWidth={1.8} />}
>
{serviceLabel(type)}
</Menu.Item>
))}
</Menu.Dropdown>
</Menu>
)}
@@ -468,39 +432,25 @@ export function AppLayout({
<Divider />
<Box px="sm" py="xs">
<Stack gap={6}>
{companyProfiles.map((p) => {
const isActive = p.type === activeProfileType;
return (
<Group
key={p.reference}
justify="space-between"
gap="sm"
wrap="nowrap"
{companyProfiles.map((p) => (
<Group
key={p.reference}
justify="space-between"
gap="sm"
wrap="nowrap"
>
<Text
size="xs"
fw={600}
style={{ color: textColor }}
>
<Group gap={6} wrap="nowrap">
{isActive && (
<Check
size={13}
color={primaryDarkColor}
strokeWidth={2.5}
/>
)}
<Text
size="xs"
fw={isActive ? 700 : 600}
style={{
color: isActive ? primaryDarkColor : textColor,
}}
>
{PROFILE_TYPE_LABELS[p.type] ?? p.type}
</Text>
</Group>
<Text size="xs" ff="monospace" c="dimmed">
{p.reference}
</Text>
</Group>
);
})}
{PROFILE_TYPE_LABELS[p.type] ?? p.type}
</Text>
<Text size="xs" ff="monospace" c="dimmed">
{p.reference}
</Text>
</Group>
))}
</Stack>
</Box>
</>