feat: add the company profile support to the ui

This commit is contained in:
Nathnael
2026-06-19 12:25:42 +00:00
parent 7ce5f23005
commit 653f39ec65
3 changed files with 241 additions and 46 deletions

View File

@@ -146,10 +146,11 @@ const sidebarItems: SidebarItem[] = [
const App = () => { const App = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const { user } = useAuth(); const { user, company } = useAuth();
const displayName = user?.name?.en || user?.username || user?.email || "User"; const displayName = user?.name?.en || user?.username || user?.email || "User";
const userEmail = user?.email; const userEmail = user?.email;
const companyProfiles = company?.company?.companyProfiles ?? [];
return ( return (
<Routes> <Routes>
@@ -190,6 +191,7 @@ const App = () => {
enableThemeToggle enableThemeToggle
userName={displayName} userName={displayName}
userEmail={userEmail} userEmail={userEmail}
companyProfiles={companyProfiles}
> >
<Outlet /> <Outlet />
</AppLayout> </AppLayout>

View File

@@ -47,9 +47,19 @@ export interface AppLayoutProps {
enableThemeToggle?: boolean; enableThemeToggle?: boolean;
userName?: string; userName?: string;
userEmail?: string; userEmail?: string;
/** Operational profiles for the company — surfaced as reference chips in the account menu. */
companyProfiles?: { type: string; reference: string; status?: string }[];
children: ReactNode; children: ReactNode;
} }
const PROFILE_TYPE_LABELS: Record<string, string> = {
importer: "Importer",
exporter: "Exporter",
freight_forwarder: "Freight Forwarder",
dj_freight_forwarder: "DJ Freight Forwarder",
transporter: "Transporter",
};
function getInitials(name: string): string { function getInitials(name: string): string {
return name return name
.split(" ") .split(" ")
@@ -106,6 +116,7 @@ export function AppLayout({
enableThemeToggle = false, enableThemeToggle = false,
userName = "User", userName = "User",
userEmail, userEmail,
companyProfiles = [],
children, children,
}: AppLayoutProps) { }: AppLayoutProps) {
const [mobileOpen, { toggle: toggleMobile }] = useDisclosure(); const [mobileOpen, { toggle: toggleMobile }] = useDisclosure();
@@ -306,6 +317,34 @@ export function AppLayout({
</Text> </Text>
)} )}
</Box> </Box>
{companyProfiles.length > 0 && (
<>
<Divider />
<Box px="sm" py="xs">
<Stack gap={6}>
{companyProfiles.map((p) => (
<Group
key={p.reference}
justify="space-between"
gap="sm"
wrap="nowrap"
>
<Text
size="xs"
fw={600}
style={{ color: textColor }}
>
{PROFILE_TYPE_LABELS[p.type] ?? p.type}
</Text>
<Text size="xs" ff="monospace" c="dimmed">
{p.reference}
</Text>
</Group>
))}
</Stack>
</Box>
</>
)}
<Divider /> <Divider />
<Menu.Item <Menu.Item
leftSection={<User size={15} />} leftSection={<User size={15} />}

View File

@@ -18,6 +18,7 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { import {
BadgeCheck,
Briefcase, Briefcase,
Building, Building,
Building2, Building2,
@@ -26,11 +27,13 @@ import {
Mail, Mail,
MapPin, MapPin,
Phone, Phone,
Plus,
ShieldCheck, ShieldCheck,
User, User,
UserCheck, UserCheck,
} from "lucide-react"; } from "lucide-react";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { rolesForCompanyType } from "./settings/companyRoles";
function InfoItem({ function InfoItem({
icon, icon,
@@ -129,6 +132,12 @@ export default function ProfilePage() {
); );
} }
// Registered operational profiles keyed by type, plus the roles this company
// type may hold (importer/exporter for a customer). Mirrors CompanyRolesCard.
const refByType = new Map(profile.companyProfiles.map((p) => [p.type, p]));
const roleOptions = rolesForCompanyType(profile.companyType);
const activeOptions = roleOptions.filter((o) => refByType.has(o.type));
return ( return (
<Container size="xl" px="lg" py="xl"> <Container size="xl" px="lg" py="xl">
{/* Header */} {/* Header */}
@@ -145,12 +154,28 @@ export default function ProfilePage() {
Verified Verified
</Badge> </Badge>
</Group> </Group>
<Group gap={6} c="edr-muted"> {activeOptions.length > 0 ? (
<Building size={16} /> <Group gap="xs">
<Text c="edr-muted" fw={500}> {activeOptions.map((opt) => (
{profile.companyType} <Badge
</Text> key={opt.type}
</Group> variant="light"
color="edr-green"
size="lg"
radius="sm"
>
{opt.label} · {refByType.get(opt.type)!.reference}
</Badge>
))}
</Group>
) : (
<Group gap={6} c="edr-muted">
<Building size={16} />
<Text c="edr-muted" fw={500}>
{profile.companyType}
</Text>
</Group>
)}
</Stack> </Stack>
</Group> </Group>
@@ -163,24 +188,58 @@ export default function ProfilePage() {
{/* Company Details */} {/* Company Details */}
<Card> <Card>
<CardHeading <CardHeading
icon={<Building2 size={20} color="var(--mantine-color-edr-green-6)" />} icon={
<Building2
size={20}
color="var(--mantine-color-edr-green-6)"
/>
}
title="Company Details" title="Company Details"
description="Business registration information" description="Business registration information"
/> />
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md"> <SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
<InfoItem icon={<Globe size={16} />} label="Location" value={profile.companyLocation} /> <InfoItem
<InfoItem icon={<MapPin size={16} />} label="Address" value={profile.companyAddress} /> icon={<Globe size={16} />}
<InfoItem icon={<FileCheck size={16} />} label="TIN Number" value={profile.tinNumber} /> label="Location"
<InfoItem icon={<ShieldCheck size={16} />} label="FAN Number" value={profile.fanNumber} /> value={profile.companyLocation}
<InfoItem icon={<Mail size={16} />} label="Email" value={profile.companyEmail} /> />
<InfoItem icon={<Phone size={16} />} label="Phone" value={profile.companyPhone} /> <InfoItem
icon={<MapPin size={16} />}
label="Address"
value={profile.companyAddress}
/>
<InfoItem
icon={<FileCheck size={16} />}
label="TIN Number"
value={profile.tinNumber}
/>
<InfoItem
icon={<ShieldCheck size={16} />}
label="FAN Number"
value={profile.fanNumber}
/>
<InfoItem
icon={<Mail size={16} />}
label="Email"
value={profile.companyEmail}
/>
<InfoItem
icon={<Phone size={16} />}
label="Phone"
value={profile.companyPhone}
/>
</SimpleGrid> </SimpleGrid>
</Card> </Card>
{/* Key Personnel */} {/* Key Personnel */}
<Card> <Card>
<CardHeading <CardHeading
icon={<Briefcase size={20} color="var(--mantine-color-edr-green-6)" />} icon={
<Briefcase
size={20}
color="var(--mantine-color-edr-green-6)"
/>
}
title="Key Personnel" title="Key Personnel"
description="Management and contact persons" description="Management and contact persons"
/> />
@@ -201,7 +260,12 @@ export default function ProfilePage() {
{profile.poaName && ( {profile.poaName && (
<Card style={{ borderStyle: "dashed" }}> <Card style={{ borderStyle: "dashed" }}>
<CardHeading <CardHeading
icon={<UserCheck size={20} color="var(--mantine-color-edr-accent-6)" />} icon={
<UserCheck
size={20}
color="var(--mantine-color-edr-accent-6)"
/>
}
title="Power of Attorney" title="Power of Attorney"
description="Authorized representative details" description="Authorized representative details"
/> />
@@ -218,37 +282,127 @@ export default function ProfilePage() {
{/* Right Column */} {/* Right Column */}
<Grid.Col span={{ base: 12, lg: 4 }}> <Grid.Col span={{ base: 12, lg: 4 }}>
<Card <Stack gap="lg">
padding="xl" {/* Operating Roles */}
style={{ <Card>
background: "var(--mantine-color-edr-ink-6)", <CardHeading
position: "relative", icon={
overflow: "hidden", <BadgeCheck
}} size={20}
> color="var(--mantine-color-edr-green-6)"
<Box style={{ position: "absolute", top: 16, right: 16, opacity: 0.1 }}> />
<ShieldCheck size={128} color="white" /> }
</Box> title="Operating Roles"
<Stack gap="md" style={{ position: "relative", zIndex: 1 }}> description="Your registered freight roles and reference numbers"
<Title order={3} size="h4" c="white"> />
Secure Account {roleOptions.length === 0 ? (
</Title> <Text size="sm" c="edr-muted">
<Text size="sm" c="gray.4"> Role management for this company type is coming soon.
Your information is protected by enterprise-grade security. Contact </Text>
support for verified information updates. ) : (
</Text> <Stack gap="md">
<Button {roleOptions.map((opt) => {
component={Link} const active = refByType.get(opt.type);
to="/settings" return (
variant="white" <Group
color="dark" key={opt.type}
mt="xs" justify="space-between"
w="fit-content" wrap="nowrap"
align="center"
>
<Group gap="sm" wrap="nowrap">
<ThemeIcon
variant="light"
color="edr-green"
size="lg"
radius="md"
>
{opt.icon}
</ThemeIcon>
<Stack gap={2}>
<Text size="sm" fw={600} c="edr-text">
{opt.label}
</Text>
<Text
size="xs"
c="edr-muted"
ff={active ? "monospace" : undefined}
>
{active ? active.reference : "Not registered"}
</Text>
</Stack>
</Group>
{active ? (
<Badge
variant="light"
color={
active.status === "active"
? "edr-green"
: "edr-accent"
}
tt="capitalize"
>
{active.status}
</Badge>
) : (
<Button
component={Link}
to="/settings?tab=company"
size="xs"
variant="light"
color="edr-green"
leftSection={<Plus size={14} />}
>
Add {opt.label}
</Button>
)}
</Group>
);
})}
</Stack>
)}
</Card>
{/* Secure Account */}
<Card
padding="xl"
style={{
background: "var(--mantine-color-edr-ink-6)",
position: "relative",
overflow: "hidden",
}}
>
<Box
style={{
position: "absolute",
top: 16,
right: 16,
opacity: 0.1,
}}
> >
Edit Settings <ShieldCheck size={128} color="white" />
</Button> </Box>
</Stack> <Stack gap="md" style={{ position: "relative", zIndex: 1 }}>
</Card> <Title order={3} size="h4" c="white">
Secure Account
</Title>
<Text size="sm" c="gray.4">
Your information is protected by enterprise-grade security.
Contact support for verified information updates.
</Text>
<Button
component={Link}
to="/settings"
variant="white"
color="dark"
mt="xs"
w="fit-content"
>
Edit Settings
</Button>
</Stack>
</Card>
</Stack>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</Container> </Container>