mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 04:08:11 +00:00
feat: remove ProfilePage and update routing to redirect to Settings; enhance SettingsPage with ProfileHeader and improved tab handling
This commit is contained in:
@@ -7,7 +7,6 @@ import {
|
|||||||
Receipt,
|
Receipt,
|
||||||
Settings,
|
Settings,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
User,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useDisclosure } from "@mantine/hooks";
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
@@ -24,7 +23,6 @@ import useAuth from "./hooks/useAuth";
|
|||||||
import OnboardingWizardDialog from "./components/onboarding/OnboardingWizardDialog";
|
import OnboardingWizardDialog from "./components/onboarding/OnboardingWizardDialog";
|
||||||
import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
|
import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
|
||||||
import MyPortalPage from "./pages/MyPortalPage";
|
import MyPortalPage from "./pages/MyPortalPage";
|
||||||
import ProfilePage from "./pages/ProfilePage";
|
|
||||||
import MySignaturePage from "./pages/MySignaturePage";
|
import MySignaturePage from "./pages/MySignaturePage";
|
||||||
import SettingsPage from "./pages/SettingsPage";
|
import SettingsPage from "./pages/SettingsPage";
|
||||||
import LoginPage from "./pages/accounts/LoginPage";
|
import LoginPage from "./pages/accounts/LoginPage";
|
||||||
@@ -211,12 +209,6 @@ const sidebarItems: SidebarItem[] = [
|
|||||||
href: "/billing",
|
href: "/billing",
|
||||||
icon: <Receipt size={18} />,
|
icon: <Receipt size={18} />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
section: "Account",
|
|
||||||
label: "Profile",
|
|
||||||
href: "/profile",
|
|
||||||
icon: <User size={18} />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
section: "Account",
|
section: "Account",
|
||||||
label: "Settings",
|
label: "Settings",
|
||||||
@@ -297,7 +289,8 @@ const App = () => {
|
|||||||
/>
|
/>
|
||||||
<Route path="/tracking" element={<TrackingPage />} />
|
<Route path="/tracking" element={<TrackingPage />} />
|
||||||
<Route path="/billing" element={<BillingPage />} />
|
<Route path="/billing" element={<BillingPage />} />
|
||||||
<Route path="/profile" element={<ProfilePage />} />
|
{/* Profile was merged into Settings — keep old links working. */}
|
||||||
|
<Route path="/profile" element={<Navigate to="/settings" replace />} />
|
||||||
<Route path="/signature" element={<MySignaturePage />} />
|
<Route path="/signature" element={<MySignaturePage />} />
|
||||||
<Route path="/settings" element={<SettingsPage />} />
|
<Route path="/settings" element={<SettingsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|||||||
@@ -1,410 +0,0 @@
|
|||||||
import { api } from "@/services/api";
|
|
||||||
import {
|
|
||||||
Badge,
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Center,
|
|
||||||
Container,
|
|
||||||
Divider,
|
|
||||||
Grid,
|
|
||||||
Group,
|
|
||||||
Loader,
|
|
||||||
SimpleGrid,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
ThemeIcon,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import {
|
|
||||||
BadgeCheck,
|
|
||||||
Briefcase,
|
|
||||||
Building,
|
|
||||||
Building2,
|
|
||||||
FileCheck,
|
|
||||||
Globe,
|
|
||||||
Mail,
|
|
||||||
MapPin,
|
|
||||||
Phone,
|
|
||||||
Plus,
|
|
||||||
ShieldCheck,
|
|
||||||
User,
|
|
||||||
UserCheck,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { rolesForCompanyType } from "./settings/companyRoles";
|
|
||||||
|
|
||||||
function InfoItem({
|
|
||||||
icon,
|
|
||||||
label,
|
|
||||||
value,
|
|
||||||
}: {
|
|
||||||
icon?: React.ReactNode;
|
|
||||||
label: string;
|
|
||||||
value?: string | null;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Group gap="sm" align="flex-start" wrap="nowrap">
|
|
||||||
{icon && (
|
|
||||||
<ThemeIcon variant="light" color="edr-green" size="md" radius="md">
|
|
||||||
{icon}
|
|
||||||
</ThemeIcon>
|
|
||||||
)}
|
|
||||||
<Stack gap={2}>
|
|
||||||
<Text size="xs" fw={700} tt="uppercase" c="edr-muted">
|
|
||||||
{label}
|
|
||||||
</Text>
|
|
||||||
<Text size="sm" fw={600} c="edr-text">
|
|
||||||
{value || "—"}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CardHeading({
|
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
}: {
|
|
||||||
icon: React.ReactNode;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Stack gap={2} mb="md">
|
|
||||||
<Group gap="sm">
|
|
||||||
{icon}
|
|
||||||
<Title order={4} size="h5">
|
|
||||||
{title}
|
|
||||||
</Title>
|
|
||||||
</Group>
|
|
||||||
<Text size="sm" c="edr-muted">
|
|
||||||
{description}
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function PersonnelGroup({
|
|
||||||
color,
|
|
||||||
title,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
color: string;
|
|
||||||
title: string;
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Stack gap="sm">
|
|
||||||
<Group gap="xs">
|
|
||||||
<Box w={4} h={16} bg={color} style={{ borderRadius: 2 }} />
|
|
||||||
<Text size="sm" fw={700} tt="uppercase" c="edr-text">
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
<Stack gap="sm" pl="lg">
|
|
||||||
{children}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ProfilePage() {
|
|
||||||
const { data: profile, isPending } = useQuery(
|
|
||||||
api.companies.getProfile.queryOptions(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isPending) {
|
|
||||||
return (
|
|
||||||
<Center h="100%">
|
|
||||||
<Loader color="edr-green" size="lg" />
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!profile) {
|
|
||||||
return (
|
|
||||||
<Center h="100%">
|
|
||||||
<Text c="edr-muted">No company profile found.</Text>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 (
|
|
||||||
<Container size="xl" px="lg" py="xl">
|
|
||||||
{/* Header */}
|
|
||||||
<Group gap="lg" align="center" mb="lg">
|
|
||||||
<ThemeIcon variant="light" color="edr-green" size={88} radius="lg">
|
|
||||||
<User size={44} />
|
|
||||||
</ThemeIcon>
|
|
||||||
<Stack gap={6}>
|
|
||||||
<Group gap="sm" align="center">
|
|
||||||
<Title order={1} size="h2">
|
|
||||||
{profile.companyName}
|
|
||||||
</Title>
|
|
||||||
<Badge color="edr-green" variant="light">
|
|
||||||
Verified
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
{activeOptions.length > 0 ? (
|
|
||||||
<Group gap="xs">
|
|
||||||
{activeOptions.map((opt) => (
|
|
||||||
<Badge
|
|
||||||
key={opt.type}
|
|
||||||
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>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Divider mb="lg" />
|
|
||||||
|
|
||||||
<Grid gap="lg">
|
|
||||||
{/* Left Column */}
|
|
||||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
|
||||||
<Stack gap="lg">
|
|
||||||
{/* Company Details */}
|
|
||||||
<Card>
|
|
||||||
<CardHeading
|
|
||||||
icon={
|
|
||||||
<Building2
|
|
||||||
size={20}
|
|
||||||
color="var(--mantine-color-edr-green-6)"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="Company Details"
|
|
||||||
description="Business registration information"
|
|
||||||
/>
|
|
||||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
|
||||||
<InfoItem
|
|
||||||
icon={<Globe size={16} />}
|
|
||||||
label="Location"
|
|
||||||
value={profile.companyLocation}
|
|
||||||
/>
|
|
||||||
<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>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Key Personnel */}
|
|
||||||
<Card>
|
|
||||||
<CardHeading
|
|
||||||
icon={
|
|
||||||
<Briefcase
|
|
||||||
size={20}
|
|
||||||
color="var(--mantine-color-edr-green-6)"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="Key Personnel"
|
|
||||||
description="Management and contact persons"
|
|
||||||
/>
|
|
||||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
|
|
||||||
<PersonnelGroup color="edr-green" title="Contact Person">
|
|
||||||
<InfoItem label="Name" value={profile.contactPersonName} />
|
|
||||||
<InfoItem label="Phone" value={profile.contactPersonPhone} />
|
|
||||||
</PersonnelGroup>
|
|
||||||
<PersonnelGroup color="edr-accent" title="General Manager">
|
|
||||||
<InfoItem label="Name" value={profile.generalManagerName} />
|
|
||||||
<InfoItem label="Email" value={profile.generalManagerEmail} />
|
|
||||||
<InfoItem label="Phone" value={profile.generalManagerPhone} />
|
|
||||||
</PersonnelGroup>
|
|
||||||
</SimpleGrid>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* Power of Attorney */}
|
|
||||||
{profile.poaName && (
|
|
||||||
<Card style={{ borderStyle: "dashed" }}>
|
|
||||||
<CardHeading
|
|
||||||
icon={
|
|
||||||
<UserCheck
|
|
||||||
size={20}
|
|
||||||
color="var(--mantine-color-edr-accent-6)"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="Power of Attorney"
|
|
||||||
description="Authorized representative details"
|
|
||||||
/>
|
|
||||||
<SimpleGrid cols={{ base: 1, md: 2 }} spacing="md">
|
|
||||||
<InfoItem label="PoA Name" value={profile.poaName} />
|
|
||||||
<InfoItem label="PoA Email" value={profile.poaEmail} />
|
|
||||||
<InfoItem label="PoA Phone" value={profile.poaPhone} />
|
|
||||||
<InfoItem label="PoA Location" value={profile.poaLocation} />
|
|
||||||
</SimpleGrid>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</Grid.Col>
|
|
||||||
|
|
||||||
{/* Right Column */}
|
|
||||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
|
||||||
<Stack gap="lg">
|
|
||||||
{/* Operating Roles */}
|
|
||||||
<Card>
|
|
||||||
<CardHeading
|
|
||||||
icon={
|
|
||||||
<BadgeCheck
|
|
||||||
size={20}
|
|
||||||
color="var(--mantine-color-edr-green-6)"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="Operating Roles"
|
|
||||||
description="Your registered freight roles and reference numbers"
|
|
||||||
/>
|
|
||||||
{roleOptions.length === 0 ? (
|
|
||||||
<Text size="sm" c="edr-muted">
|
|
||||||
Role management for this company type is coming soon.
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Stack gap="md">
|
|
||||||
{roleOptions.map((opt) => {
|
|
||||||
const active = refByType.get(opt.type);
|
|
||||||
return (
|
|
||||||
<Group
|
|
||||||
key={opt.type}
|
|
||||||
justify="space-between"
|
|
||||||
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,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ShieldCheck size={128} color="white" />
|
|
||||||
</Box>
|
|
||||||
<Stack gap="md" style={{ position: "relative", zIndex: 1 }}>
|
|
||||||
<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>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,27 +1,34 @@
|
|||||||
import { api } from "@/services/api";
|
import { api } from "@/services/api";
|
||||||
import type { ProfileResponse } from "@/types/profile";
|
import type { ProfileResponse } from "@/types/profile";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Badge,
|
||||||
|
Box,
|
||||||
Card,
|
Card,
|
||||||
Center,
|
Center,
|
||||||
Container,
|
Container,
|
||||||
Group,
|
Group,
|
||||||
Loader,
|
Loader,
|
||||||
|
Stack,
|
||||||
Tabs,
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
|
ThemeIcon,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
BadgeCheck,
|
||||||
Briefcase,
|
Briefcase,
|
||||||
Building2,
|
Building2,
|
||||||
FileCheck,
|
FileCheck,
|
||||||
|
Globe,
|
||||||
|
ShieldCheck,
|
||||||
User,
|
User,
|
||||||
UserCheck,
|
UserCheck,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { rolesForCompanyType } from "./settings/companyRoles";
|
||||||
import TabCompanyProfile from "./settings/TabCompanyProfile";
|
import TabCompanyProfile from "./settings/TabCompanyProfile";
|
||||||
import TabContactPerson from "./settings/TabContactPerson";
|
import TabContactPerson from "./settings/TabContactPerson";
|
||||||
import TabDocuments from "./settings/TabDocuments";
|
import TabDocuments from "./settings/TabDocuments";
|
||||||
@@ -30,35 +37,139 @@ import TabPowerOfAttorney from "./settings/TabPowerOfAttorney";
|
|||||||
|
|
||||||
type SettingsTab = "company" | "contact" | "gm" | "poa" | "documents";
|
type SettingsTab = "company" | "contact" | "gm" | "poa" | "documents";
|
||||||
|
|
||||||
function tabIncomplete(tabId: SettingsTab, profile?: ProfileResponse | null): boolean {
|
/** A section is "incomplete" when its required fields aren't filled in yet. */
|
||||||
if (!profile) return false;
|
function tabIncomplete(
|
||||||
|
tabId: SettingsTab,
|
||||||
|
profile: ProfileResponse,
|
||||||
|
): boolean {
|
||||||
switch (tabId) {
|
switch (tabId) {
|
||||||
case "company":
|
case "company":
|
||||||
return !profile.companyEmail || !profile.companyPhone || !profile.companyAddress || !profile.fanNumber;
|
return (
|
||||||
|
!profile.companyEmail ||
|
||||||
|
!profile.companyPhone ||
|
||||||
|
!profile.companyAddress ||
|
||||||
|
!profile.fanNumber
|
||||||
|
);
|
||||||
case "contact":
|
case "contact":
|
||||||
return !profile.contactPersonName || !profile.contactPersonPhone;
|
return !profile.contactPersonName || !profile.contactPersonPhone;
|
||||||
case "gm":
|
case "gm":
|
||||||
return !profile.generalManagerName || !profile.generalManagerEmail || !profile.generalManagerPhone;
|
return (
|
||||||
|
!profile.generalManagerName ||
|
||||||
|
!profile.generalManagerEmail ||
|
||||||
|
!profile.generalManagerPhone
|
||||||
|
);
|
||||||
case "poa":
|
case "poa":
|
||||||
return false;
|
|
||||||
case "documents":
|
case "documents":
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
|
const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
|
||||||
{ id: "company", label: "Company Profile", icon: <Building2 size={16} /> },
|
{ id: "company", label: "Company", icon: <Building2 size={16} /> },
|
||||||
{ id: "contact", label: "Contact Person", icon: <User size={16} /> },
|
{ id: "contact", label: "Contact Person", icon: <User size={16} /> },
|
||||||
{ id: "gm", label: "General Manager", icon: <Briefcase size={16} /> },
|
{ id: "gm", label: "General Manager", icon: <Briefcase size={16} /> },
|
||||||
{ id: "poa", label: "Power of Attorney", icon: <UserCheck size={16} /> },
|
{ id: "poa", label: "Power of Attorney", icon: <UserCheck size={16} /> },
|
||||||
{ id: "documents", label: "Documents", icon: <FileCheck size={16} /> },
|
{ id: "documents", label: "Documents", icon: <FileCheck size={16} /> },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polished identity banner shown above the editor tabs — company name, its
|
||||||
|
* registered operating roles, location and verification status at a glance.
|
||||||
|
*/
|
||||||
|
function ProfileHeader({ profile }: { profile: ProfileResponse }) {
|
||||||
|
const refByType = new Map(profile.companyProfiles.map((p) => [p.type, p]));
|
||||||
|
const roleOptions = rolesForCompanyType(profile.companyType);
|
||||||
|
const activeRoles = roleOptions.filter((o) => refByType.has(o.type));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
padding="xl"
|
||||||
|
radius="lg"
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
"linear-gradient(135deg, var(--mantine-color-edr-ink-6) 0%, var(--mantine-color-edr-ink-8, var(--mantine-color-edr-ink-6)) 100%)",
|
||||||
|
position: "relative",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
style={{ position: "absolute", top: -24, right: -16, opacity: 0.08 }}
|
||||||
|
>
|
||||||
|
<Building2 size={180} color="white" />
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Group
|
||||||
|
justify="space-between"
|
||||||
|
align="flex-start"
|
||||||
|
wrap="nowrap"
|
||||||
|
style={{ position: "relative", zIndex: 1 }}
|
||||||
|
>
|
||||||
|
<Group gap="lg" align="center" wrap="nowrap">
|
||||||
|
<ThemeIcon variant="white" color="edr-green" size={72} radius="lg">
|
||||||
|
<Building2 size={36} />
|
||||||
|
</ThemeIcon>
|
||||||
|
<Stack gap={8}>
|
||||||
|
<Group gap="sm" align="center">
|
||||||
|
<Title order={1} size="h2" c="white">
|
||||||
|
{profile.companyName}
|
||||||
|
</Title>
|
||||||
|
<Badge
|
||||||
|
color="edr-green"
|
||||||
|
variant="filled"
|
||||||
|
leftSection={<BadgeCheck size={13} />}
|
||||||
|
>
|
||||||
|
Verified
|
||||||
|
</Badge>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
{activeRoles.length > 0 ? (
|
||||||
|
<Group gap="xs">
|
||||||
|
{activeRoles.map((opt) => (
|
||||||
|
<Badge
|
||||||
|
key={opt.type}
|
||||||
|
variant="white"
|
||||||
|
color="edr-ink"
|
||||||
|
radius="sm"
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
{opt.label} · {refByType.get(opt.type)!.reference}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</Group>
|
||||||
|
) : (
|
||||||
|
<Text c="gray.4" fw={500} tt="capitalize">
|
||||||
|
{profile.companyType.replace(/_/g, " ")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Group gap="lg" mt={4}>
|
||||||
|
{profile.companyLocation && (
|
||||||
|
<Group gap={6} c="gray.4">
|
||||||
|
<Globe size={15} />
|
||||||
|
<Text size="sm">{profile.companyLocation}</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
{profile.tinNumber && (
|
||||||
|
<Group gap={6} c="gray.4">
|
||||||
|
<ShieldCheck size={15} />
|
||||||
|
<Text size="sm" ff="monospace">
|
||||||
|
TIN {profile.tinNumber}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const tab = (searchParams.get("tab") as SettingsTab) || "company";
|
const tab = (searchParams.get("tab") as SettingsTab) || "company";
|
||||||
|
|
||||||
const setTab = useCallback(
|
const setTab = useCallback(
|
||||||
(t: SettingsTab) => {
|
(t: SettingsTab) => {
|
||||||
setSearchParams(
|
setSearchParams(
|
||||||
@@ -79,9 +190,10 @@ export default function SettingsPage() {
|
|||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const profile = profileQuery.data;
|
const profile = profileQuery.data;
|
||||||
|
|
||||||
|
// Keep the cached company info in sync whenever the profile changes, so the
|
||||||
|
// header (and the rest of the app) reflect edits immediately.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (profileQuery.dataUpdatedAt > 0) {
|
if (profileQuery.dataUpdatedAt > 0) {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
@@ -90,34 +202,6 @@ export default function SettingsPage() {
|
|||||||
}
|
}
|
||||||
}, [profileQuery.dataUpdatedAt, queryClient]);
|
}, [profileQuery.dataUpdatedAt, queryClient]);
|
||||||
|
|
||||||
const [isOnboarding, setIsOnboarding] = useState<boolean | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (profileQuery.isFetched && isOnboarding === null) {
|
|
||||||
setIsOnboarding(!profileQuery.data);
|
|
||||||
}
|
|
||||||
}, [profileQuery.isFetched, profileQuery.data, isOnboarding]);
|
|
||||||
|
|
||||||
const handleOnboardingSuccess = useCallback(() => {
|
|
||||||
setTab("contact");
|
|
||||||
}, [setTab]);
|
|
||||||
|
|
||||||
const handleContactContinue = useCallback(() => {
|
|
||||||
setTab("gm");
|
|
||||||
}, [setTab]);
|
|
||||||
|
|
||||||
const handleGMContinue = useCallback(() => {
|
|
||||||
setTab("poa");
|
|
||||||
}, [setTab]);
|
|
||||||
|
|
||||||
const handlePOAContinue = useCallback(() => {
|
|
||||||
setTab("documents");
|
|
||||||
}, [setTab]);
|
|
||||||
|
|
||||||
const handleDocumentsContinue = useCallback(() => {
|
|
||||||
navigate("/portal");
|
|
||||||
}, [navigate]);
|
|
||||||
|
|
||||||
if (profileQuery.isPending) {
|
if (profileQuery.isPending) {
|
||||||
return (
|
return (
|
||||||
<Center h="100%">
|
<Center h="100%">
|
||||||
@@ -126,126 +210,82 @@ export default function SettingsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const onboarding = isOnboarding === true;
|
if (!profile) {
|
||||||
|
return (
|
||||||
const renderProfileContent = (children: React.ReactNode) => {
|
<Container size="xl" px="lg" py="xl">
|
||||||
if (onboarding && tab !== "company" && !profile) {
|
<Card padding="xl" radius="lg">
|
||||||
return (
|
|
||||||
<Center h={200}>
|
|
||||||
<Loader color="edr-green" />
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (!profile) {
|
|
||||||
return (
|
|
||||||
<Card padding="xl">
|
|
||||||
<Center>
|
<Center>
|
||||||
<Alert
|
<Group gap="sm" c="edr-muted">
|
||||||
icon={<AlertCircle size={24} />}
|
<AlertCircle size={20} />
|
||||||
color="gray"
|
<Text>No company profile found.</Text>
|
||||||
variant="light"
|
</Group>
|
||||||
>
|
|
||||||
Please complete the company profile first.
|
|
||||||
</Alert>
|
|
||||||
</Center>
|
</Center>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
</Container>
|
||||||
}
|
);
|
||||||
return children;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="xl" px="lg">
|
<Container size="xl" px="lg" py="xl">
|
||||||
<Group justify="space-between" mb="xl">
|
<Stack gap="xl">
|
||||||
|
<ProfileHeader profile={profile} />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Title order={1} size="h2">
|
<Title order={2} size="h3">
|
||||||
{onboarding ? "Complete Your Profile" : "Account Settings"}
|
Account Settings
|
||||||
</Title>
|
</Title>
|
||||||
<Text c="edr-muted" size="sm" mt={4}>
|
<Text c="edr-muted" size="sm" mt={4}>
|
||||||
{onboarding
|
Manage your company profile, personnel, and documents.
|
||||||
? "Set up your company profile, personnel, and documents to get started"
|
|
||||||
: "Manage your company profile, personnel, and documents"}
|
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
value={tab}
|
value={tab}
|
||||||
onChange={(value) => {
|
onChange={(value) => value && setTab(value as SettingsTab)}
|
||||||
if (!value) return;
|
variant="pills"
|
||||||
// if (onboarding) return;
|
radius="md"
|
||||||
setTab(value as SettingsTab);
|
>
|
||||||
}}
|
<Tabs.List mb="lg">
|
||||||
>
|
{TABS.map((t) => (
|
||||||
<Tabs.List mb="md">
|
<Tabs.Tab
|
||||||
{TABS.map((t) => (
|
key={t.id}
|
||||||
<Tabs.Tab
|
value={t.id}
|
||||||
key={t.id}
|
leftSection={t.icon}
|
||||||
value={t.id}
|
rightSection={
|
||||||
leftSection={t.icon}
|
tabIncomplete(t.id, profile) ? (
|
||||||
disabled={!onboarding && !profile && t.id !== "company"}
|
<Box
|
||||||
rightSection={
|
w={7}
|
||||||
!onboarding && profile && tabIncomplete(t.id, profile) ? (
|
h={7}
|
||||||
<AlertCircle size={14} color="red" />
|
style={{
|
||||||
) : undefined
|
borderRadius: "50%",
|
||||||
}
|
background: "var(--mantine-color-red-6)",
|
||||||
>
|
}}
|
||||||
{t.label}
|
/>
|
||||||
</Tabs.Tab>
|
) : undefined
|
||||||
))}
|
}
|
||||||
</Tabs.List>
|
>
|
||||||
|
{t.label}
|
||||||
|
</Tabs.Tab>
|
||||||
|
))}
|
||||||
|
</Tabs.List>
|
||||||
|
|
||||||
<Tabs.Panel value="company">
|
<Tabs.Panel value="company">
|
||||||
{!profile ? (
|
|
||||||
<TabCompanyProfile
|
|
||||||
mode="create"
|
|
||||||
onCreateSuccess={handleOnboardingSuccess}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TabCompanyProfile mode="edit" profile={profile} />
|
<TabCompanyProfile mode="edit" profile={profile} />
|
||||||
)}
|
</Tabs.Panel>
|
||||||
</Tabs.Panel>
|
<Tabs.Panel value="contact">
|
||||||
|
<TabContactPerson profile={profile} mode="edit" />
|
||||||
<Tabs.Panel value="contact">
|
</Tabs.Panel>
|
||||||
{renderProfileContent(
|
<Tabs.Panel value="gm">
|
||||||
<TabContactPerson
|
<TabGeneralManager profile={profile} mode="edit" />
|
||||||
profile={profile!}
|
</Tabs.Panel>
|
||||||
mode={onboarding ? "onboarding" : "edit"}
|
<Tabs.Panel value="poa">
|
||||||
onContinue={handleContactContinue}
|
<TabPowerOfAttorney profile={profile} mode="edit" />
|
||||||
/>,
|
</Tabs.Panel>
|
||||||
)}
|
<Tabs.Panel value="documents">
|
||||||
</Tabs.Panel>
|
<TabDocuments profile={profile} mode="edit" />
|
||||||
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value="gm">
|
</Tabs>
|
||||||
{renderProfileContent(
|
</Stack>
|
||||||
<TabGeneralManager
|
|
||||||
profile={profile!}
|
|
||||||
mode={onboarding ? "onboarding" : "edit"}
|
|
||||||
onContinue={handleGMContinue}
|
|
||||||
/>,
|
|
||||||
)}
|
|
||||||
</Tabs.Panel>
|
|
||||||
|
|
||||||
<Tabs.Panel value="poa">
|
|
||||||
{renderProfileContent(
|
|
||||||
<TabPowerOfAttorney
|
|
||||||
profile={profile!}
|
|
||||||
mode={onboarding ? "onboarding" : "edit"}
|
|
||||||
onContinue={handlePOAContinue}
|
|
||||||
/>,
|
|
||||||
)}
|
|
||||||
</Tabs.Panel>
|
|
||||||
|
|
||||||
<Tabs.Panel value="documents">
|
|
||||||
{renderProfileContent(
|
|
||||||
<TabDocuments
|
|
||||||
profile={profile!}
|
|
||||||
mode={onboarding ? "onboarding" : "edit"}
|
|
||||||
onContinue={handleDocumentsContinue}
|
|
||||||
/>,
|
|
||||||
)}
|
|
||||||
</Tabs.Panel>
|
|
||||||
</Tabs>
|
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ const onboardingSchema = z.object({
|
|||||||
companyPhone: z.string().min(1, "Company phone is required"),
|
companyPhone: z.string().min(1, "Company phone is required"),
|
||||||
companyPhoneCountryCode: z.string().min(1, "Country code is required"),
|
companyPhoneCountryCode: z.string().min(1, "Country code is required"),
|
||||||
companyLocation: z.string().min(1, "Location is required"),
|
companyLocation: z.string().min(1, "Location is required"),
|
||||||
companyAddress: z.string().min(1, "Address is required"),
|
// Derived from the eTrade address parts (kebele/woreda/zone/region); no
|
||||||
|
// standalone input — the granular fields live in the registration section.
|
||||||
|
companyAddress: z.string().optional(),
|
||||||
tinNumber: z.string().length(10, "TIN must be exactly 10 digits"),
|
tinNumber: z.string().length(10, "TIN must be exactly 10 digits"),
|
||||||
vatNumber: z
|
vatNumber: z
|
||||||
.string()
|
.string()
|
||||||
@@ -429,6 +431,27 @@ export default function CompanyProfileForm({
|
|||||||
setValue("kebele", data.kebele);
|
setValue("kebele", data.kebele);
|
||||||
setValue("houseNo", data.houseNo);
|
setValue("houseNo", data.houseNo);
|
||||||
setValue("etradePhone", data.regularPhone || data.mobilePhone);
|
setValue("etradePhone", data.regularPhone || data.mobilePhone);
|
||||||
|
|
||||||
|
// Compose a readable company address from the granular eTrade parts.
|
||||||
|
const addressParts = [
|
||||||
|
data.houseNo,
|
||||||
|
data.kebele,
|
||||||
|
data.woreda,
|
||||||
|
data.zone,
|
||||||
|
data.region,
|
||||||
|
].filter((part) => part && part.trim());
|
||||||
|
if (addressParts.length) {
|
||||||
|
setValue("companyAddress", addressParts.join(", "));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-fill the company contact phone from eTrade's mobile number.
|
||||||
|
const mobile = data.mobilePhone || data.regularPhone;
|
||||||
|
if (mobile) {
|
||||||
|
const { number, countryCode } = splitPhone(mobile);
|
||||||
|
setValue("companyPhone", number);
|
||||||
|
setValue("companyPhoneCountryCode", countryCode);
|
||||||
|
}
|
||||||
|
|
||||||
setEtradeOwner({
|
setEtradeOwner({
|
||||||
name: data.managerName,
|
name: data.managerName,
|
||||||
phone: data.managerPhone || data.regularPhone || data.mobilePhone,
|
phone: data.managerPhone || data.regularPhone || data.mobilePhone,
|
||||||
@@ -651,20 +674,12 @@ export default function CompanyProfileForm({
|
|||||||
label="Company Phone"
|
label="Company Phone"
|
||||||
/>
|
/>
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<TextInput
|
||||||
<TextInput
|
label="Location"
|
||||||
label="Location"
|
placeholder="Addis Ababa, Ethiopia"
|
||||||
placeholder="Addis Ababa, Ethiopia"
|
error={errors.companyLocation?.message}
|
||||||
error={errors.companyLocation?.message}
|
{...register("companyLocation")}
|
||||||
{...register("companyLocation")}
|
/>
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
label="Address"
|
|
||||||
placeholder="Bole Subcity, Woreda 03"
|
|
||||||
error={errors.companyAddress?.message}
|
|
||||||
{...register("companyAddress")}
|
|
||||||
/>
|
|
||||||
</SimpleGrid>
|
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="md">
|
||||||
<TextInput
|
<TextInput
|
||||||
label="VAT Number"
|
label="VAT Number"
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default function CompanyRolesCard({ profile }: CompanyRolesCardProps) {
|
|||||||
<Card padding="lg">
|
<Card padding="lg">
|
||||||
<Group gap="sm" mb="xs">
|
<Group gap="sm" mb="xs">
|
||||||
<Building2 size={20} />
|
<Building2 size={20} />
|
||||||
<Title order={3}>Business Profile</Title>
|
<Title order={3}>Operating Roles</Title>
|
||||||
</Group>
|
</Group>
|
||||||
<Text c="edr-muted" size="sm" mb="lg">
|
<Text c="edr-muted" size="sm" mb="lg">
|
||||||
{profile.companyType === "customer"
|
{profile.companyType === "customer"
|
||||||
|
|||||||
Reference in New Issue
Block a user