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}
-
- ) : (
- }
- >
- Add {opt.label}
-
- )}
-
- );
- })}
-
- )}
-
-
- {/* Secure Account */}
-
-
-
-
-
-
- Secure Account
-
-
- Your information is protected by enterprise-grade security.
- Contact support for verified information updates.
-
-
- Edit Settings
-
-
-
-
-
-
-
- );
-}
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"}
+
+ 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"