mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 09:28:19 +00:00
enhance service filtering and dashboard functionality with company profile support
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user