diff --git a/apps/edr-freight-web/portal/src/App.tsx b/apps/edr-freight-web/portal/src/App.tsx index e436eaff6..89d094734 100644 --- a/apps/edr-freight-web/portal/src/App.tsx +++ b/apps/edr-freight-web/portal/src/App.tsx @@ -7,7 +7,6 @@ import { Receipt, Settings, Sparkles, - User, } from "lucide-react"; import { useDisclosure } from "@mantine/hooks"; import { useEffect, useRef } from "react"; @@ -24,7 +23,6 @@ import useAuth from "./hooks/useAuth"; import OnboardingWizardDialog from "./components/onboarding/OnboardingWizardDialog"; import EDRFreightLandingPage from "./pages/EDRFreightLandingPage"; import MyPortalPage from "./pages/MyPortalPage"; -import ProfilePage from "./pages/ProfilePage"; import MySignaturePage from "./pages/MySignaturePage"; import SettingsPage from "./pages/SettingsPage"; import LoginPage from "./pages/accounts/LoginPage"; @@ -211,12 +209,6 @@ const sidebarItems: SidebarItem[] = [ href: "/billing", icon: , }, - { - section: "Account", - label: "Profile", - href: "/profile", - icon: , - }, { section: "Account", label: "Settings", @@ -297,7 +289,8 @@ const App = () => { /> } /> } /> - } /> + {/* Profile was merged into Settings — keep old links working. */} + } /> } /> } /> diff --git a/apps/edr-freight-web/portal/src/pages/ProfilePage.tsx b/apps/edr-freight-web/portal/src/pages/ProfilePage.tsx deleted file mode 100644 index cf31d17a9..000000000 --- a/apps/edr-freight-web/portal/src/pages/ProfilePage.tsx +++ /dev/null @@ -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 ( - - {icon && ( - - {icon} - - )} - - - {label} - - - {value || "—"} - - - - ); -} - -function CardHeading({ - icon, - title, - description, -}: { - icon: React.ReactNode; - title: string; - description: string; -}) { - return ( - - - {icon} - - {title} - - - - {description} - - - ); -} - -function PersonnelGroup({ - color, - title, - children, -}: { - color: string; - title: string; - children: React.ReactNode; -}) { - return ( - - - - - {title} - - - - {children} - - - ); -} - -export default function ProfilePage() { - const { data: profile, isPending } = useQuery( - api.companies.getProfile.queryOptions(), - ); - - if (isPending) { - return ( -
- -
- ); - } - - if (!profile) { - return ( -
- No company profile found. -
- ); - } - - // 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 ( - - {/* Header */} - - - - - - - - {profile.companyName} - - - Verified - - - {activeOptions.length > 0 ? ( - - {activeOptions.map((opt) => ( - - {opt.label} · {refByType.get(opt.type)!.reference} - - ))} - - ) : ( - - - - {profile.companyType} - - - )} - - - - - - - {/* Left Column */} - - - {/* Company Details */} - - - } - title="Company Details" - description="Business registration information" - /> - - } - label="Location" - value={profile.companyLocation} - /> - } - label="Address" - value={profile.companyAddress} - /> - } - label="TIN Number" - value={profile.tinNumber} - /> - } - label="FAN Number" - value={profile.fanNumber} - /> - } - label="Email" - value={profile.companyEmail} - /> - } - label="Phone" - value={profile.companyPhone} - /> - - - - {/* Key Personnel */} - - - } - title="Key Personnel" - description="Management and contact persons" - /> - - - - - - - - - - - - - - {/* Power of Attorney */} - {profile.poaName && ( - - - } - title="Power of Attorney" - description="Authorized representative details" - /> - - - - - - - - )} - - - - {/* Right Column */} - - - {/* Operating Roles */} - - - } - title="Operating Roles" - description="Your registered freight roles and reference numbers" - /> - {roleOptions.length === 0 ? ( - - Role management for this company type is coming soon. - - ) : ( - - {roleOptions.map((opt) => { - const active = refByType.get(opt.type); - return ( - - - - {opt.icon} - - - - {opt.label} - - - {active ? active.reference : "Not registered"} - - - - {active ? ( - - {active.status} - - ) : ( - - )} - - ); - })} - - )} - - - {/* Secure Account */} - - - - - - - Secure Account - - - Your information is protected by enterprise-grade security. - Contact support for verified information updates. - - - - - - - - - ); -} diff --git a/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx b/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx index 6f0b17022..8c3204123 100644 --- a/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx @@ -1,27 +1,34 @@ import { api } from "@/services/api"; import type { ProfileResponse } from "@/types/profile"; import { - Alert, + Badge, + Box, Card, Center, Container, Group, Loader, + Stack, Tabs, Text, + ThemeIcon, Title, } from "@mantine/core"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { AlertCircle, + BadgeCheck, Briefcase, Building2, FileCheck, + Globe, + ShieldCheck, User, UserCheck, } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; -import { useNavigate, useSearchParams } from "react-router-dom"; +import { useCallback, useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; +import { rolesForCompanyType } from "./settings/companyRoles"; import TabCompanyProfile from "./settings/TabCompanyProfile"; import TabContactPerson from "./settings/TabContactPerson"; import TabDocuments from "./settings/TabDocuments"; @@ -30,35 +37,139 @@ import TabPowerOfAttorney from "./settings/TabPowerOfAttorney"; type SettingsTab = "company" | "contact" | "gm" | "poa" | "documents"; -function tabIncomplete(tabId: SettingsTab, profile?: ProfileResponse | null): boolean { - if (!profile) return false; +/** A section is "incomplete" when its required fields aren't filled in yet. */ +function tabIncomplete( + tabId: SettingsTab, + profile: ProfileResponse, +): boolean { switch (tabId) { case "company": - return !profile.companyEmail || !profile.companyPhone || !profile.companyAddress || !profile.fanNumber; + return ( + !profile.companyEmail || + !profile.companyPhone || + !profile.companyAddress || + !profile.fanNumber + ); case "contact": return !profile.contactPersonName || !profile.contactPersonPhone; case "gm": - return !profile.generalManagerName || !profile.generalManagerEmail || !profile.generalManagerPhone; + return ( + !profile.generalManagerName || + !profile.generalManagerEmail || + !profile.generalManagerPhone + ); case "poa": - return false; case "documents": return false; } } const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [ - { id: "company", label: "Company Profile", icon: }, + { id: "company", label: "Company", icon: }, { id: "contact", label: "Contact Person", icon: }, { id: "gm", label: "General Manager", icon: }, { id: "poa", label: "Power of Attorney", icon: }, { id: "documents", label: "Documents", icon: }, ]; +/** + * 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 ( + + + + + + + + + + + + + + {profile.companyName} + + } + > + Verified + + + + {activeRoles.length > 0 ? ( + + {activeRoles.map((opt) => ( + + {opt.label} · {refByType.get(opt.type)!.reference} + + ))} + + ) : ( + + {profile.companyType.replace(/_/g, " ")} + + )} + + + {profile.companyLocation && ( + + + {profile.companyLocation} + + )} + {profile.tinNumber && ( + + + + TIN {profile.tinNumber} + + + )} + + + + + + ); +} + export default function SettingsPage() { - const navigate = useNavigate(); const queryClient = useQueryClient(); const [searchParams, setSearchParams] = useSearchParams(); const tab = (searchParams.get("tab") as SettingsTab) || "company"; + const setTab = useCallback( (t: SettingsTab) => { setSearchParams( @@ -79,9 +190,10 @@ export default function SettingsPage() { refetchOnWindowFocus: false, }), ); - 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(() => { if (profileQuery.dataUpdatedAt > 0) { queryClient.invalidateQueries({ @@ -90,34 +202,6 @@ export default function SettingsPage() { } }, [profileQuery.dataUpdatedAt, queryClient]); - const [isOnboarding, setIsOnboarding] = useState(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) { return (
@@ -126,126 +210,82 @@ export default function SettingsPage() { ); } - const onboarding = isOnboarding === true; - - const renderProfileContent = (children: React.ReactNode) => { - if (onboarding && tab !== "company" && !profile) { - return ( -
- -
- ); - } - if (!profile) { - return ( - + if (!profile) { + return ( + +
- } - color="gray" - variant="light" - > - Please complete the company profile first. - + + + No company profile found. +
- ); - } - return children; - }; +
+ ); + } return ( - - + + + +
- - {onboarding ? "Complete Your Profile" : "Account Settings"} + <Title order={2} size="h3"> + Account Settings - {onboarding - ? "Set up your company profile, personnel, and documents to get started" - : "Manage your company profile, personnel, and documents"} + Manage your company profile, personnel, and documents.
-
- { - if (!value) return; - // if (onboarding) return; - setTab(value as SettingsTab); - }} - > - - {TABS.map((t) => ( - - ) : undefined - } - > - {t.label} - - ))} - + value && setTab(value as SettingsTab)} + variant="pills" + radius="md" + > + + {TABS.map((t) => ( + + ) : undefined + } + > + {t.label} + + ))} + - - {!profile ? ( - - ) : ( + - )} - - - - {renderProfileContent( - , - )} - - - - {renderProfileContent( - , - )} - - - - {renderProfileContent( - , - )} - - - - {renderProfileContent( - , - )} - - + + + + + + + + + + + + + + +
); } diff --git a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx index 11f75b4b7..9c2c4001d 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx @@ -57,7 +57,9 @@ const onboardingSchema = z.object({ companyPhone: z.string().min(1, "Company phone is required"), companyPhoneCountryCode: z.string().min(1, "Country code 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"), vatNumber: z .string() @@ -429,6 +431,27 @@ export default function CompanyProfileForm({ setValue("kebele", data.kebele); setValue("houseNo", data.houseNo); 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({ name: data.managerName, phone: data.managerPhone || data.regularPhone || data.mobilePhone, @@ -651,20 +674,12 @@ export default function CompanyProfileForm({ label="Company Phone" /> - - - - + - Business Profile + Operating Roles {profile.companyType === "customer"