diff --git a/apps/edr-freight-web/portal/index.css b/apps/edr-freight-web/portal/index.css
index 4909e33bd..aa14f97fe 100644
--- a/apps/edr-freight-web/portal/index.css
+++ b/apps/edr-freight-web/portal/index.css
@@ -1,2 +1,33 @@
@import "tailwindcss";
@import "@edr/ui-common/theme.css" layer(theme);
+
+/* Bridge the central Mantine theme into Tailwind. Mantine (createTheme) is the
+ single source of truth; these just alias its generated CSS variables so
+ `bg-edr-*`, `text-edr-*`, `border-edr-*` utilities resolve to the same tokens. */
+@theme {
+ --color-edr-primary: var(--mantine-color-edr-green-5);
+ --color-edr-primary-dark: var(--mantine-color-edr-green-7);
+ --color-edr-bg: var(--mantine-color-edr-bg-6);
+ --color-edr-card: var(--mantine-color-edr-card-6);
+ --color-edr-border: var(--mantine-color-edr-border-6);
+ --color-edr-divider: var(--mantine-color-edr-divider-6);
+ --color-edr-text: var(--mantine-color-edr-text-6);
+ --color-edr-muted: var(--mantine-color-edr-muted-6);
+ --color-edr-soft: var(--mantine-color-edr-soft-6);
+ --color-edr-ink: var(--mantine-color-edr-ink-6);
+ --color-edr-accent: var(--mantine-color-edr-accent-6);
+
+ --color-edr-amber-soft: var(--mantine-color-edr-amber-soft-6);
+ --color-edr-amber-text: var(--mantine-color-edr-amber-text-6);
+ --color-edr-blue: var(--mantine-color-edr-blue-6);
+ --color-edr-blue-soft: var(--mantine-color-edr-blue-soft-6);
+ --color-edr-blue-dot: var(--mantine-color-edr-blue-dot-6);
+ --color-edr-red: var(--mantine-color-edr-red-6);
+ --color-edr-red-soft: var(--mantine-color-edr-red-soft-6);
+ --color-edr-slate: var(--mantine-color-edr-slate-6);
+ --color-edr-slate-soft: var(--mantine-color-edr-slate-soft-6);
+ --color-edr-slate-soft2: var(--mantine-color-edr-slate-soft2-6);
+ --color-edr-step: var(--mantine-color-edr-step-6);
+ --color-edr-step-idle: var(--mantine-color-edr-step-idle-6);
+ --color-edr-conn-idle: var(--mantine-color-edr-conn-idle-6);
+}
diff --git a/apps/edr-freight-web/portal/index.html b/apps/edr-freight-web/portal/index.html
index 61b3dcff7..42bfe97d3 100644
--- a/apps/edr-freight-web/portal/index.html
+++ b/apps/edr-freight-web/portal/index.html
@@ -8,7 +8,7 @@
diff --git a/apps/edr-freight-web/portal/public/assets/edr-logo.png b/apps/edr-freight-web/portal/public/assets/edr-logo.png
new file mode 100644
index 000000000..c877cfa02
Binary files /dev/null and b/apps/edr-freight-web/portal/public/assets/edr-logo.png differ
diff --git a/apps/edr-freight-web/portal/public/assets/train-edr.jpg b/apps/edr-freight-web/portal/public/assets/train-edr.jpg
new file mode 100644
index 000000000..4595b3bcb
Binary files /dev/null and b/apps/edr-freight-web/portal/public/assets/train-edr.jpg differ
diff --git a/apps/edr-freight-web/portal/src/App.tsx b/apps/edr-freight-web/portal/src/App.tsx
index 048c11945..bbdc9e075 100644
--- a/apps/edr-freight-web/portal/src/App.tsx
+++ b/apps/edr-freight-web/portal/src/App.tsx
@@ -1,58 +1,55 @@
-import {
- useNavigate,
- useLocation,
- Routes,
- Route,
- Outlet,
-} from "react-router-dom";
import { AppLayout, type SidebarItem } from "@/components/AppLayout";
import {
CalendarCheck,
- MapPin,
- Receipt,
Home,
Loader2,
- User,
+ MapPin,
+ Receipt,
Settings,
+ User,
} from "lucide-react";
+import {
+ Outlet,
+ Route,
+ Routes,
+ useLocation,
+ useNavigate,
+} from "react-router-dom";
import useAuth from "./hooks/useAuth";
+import { useEffect } from "react";
+import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
+import MyPortalPage from "./pages/MyPortalPage";
import ProfilePage from "./pages/ProfilePage";
import SettingsPage from "./pages/SettingsPage";
-import MyPortalPage from "./pages/MyPortalPage";
-import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
-import SignupPage from "./pages/accounts/SignupPage";
-import OnboardingPage from "./pages/accounts/OnboardingPage";
-import VerificationOtpPage from "./pages/accounts/VerificationOtpPage";
-import SetPasswordPage from "./pages/accounts/SetPasswordPage";
import LoginPage from "./pages/accounts/LoginPage";
-import MyBookings from "./pages/bookings/MyBookings";
+import OnboardingPage from "./pages/accounts/OnboardingPage";
+import SetPasswordPage from "./pages/accounts/SetPasswordPage";
+import SignupPage from "./pages/accounts/SignupPage";
+import VerificationOtpPage from "./pages/accounts/VerificationOtpPage";
+import BillingPage from "./pages/billing/BillingPage";
import BookingContractPage from "./pages/bookings/BookingContractPage";
import BookingDetailPage from "./pages/bookings/BookingDetailPage";
-import NewBookingPage from "./pages/bookings/NewBookingPage";
import EditBookingPage from "./pages/bookings/EditBookingPage";
+import MyBookings from "./pages/bookings/MyBookings";
+import NewBookingPage from "./pages/bookings/NewBookingPage";
import CheckPaymentPage from "./pages/payments/CheckPaymentPage";
import TrackingPage from "./pages/tracking/TrackingPage";
-import BillingPage from "./pages/billing/BillingPage";
-import { useEffect } from "react";
const sidebarItems: SidebarItem[] = [
- { section: "Overview", label: "Home", href: "/portal", icon: },
+ { label: "Home", href: "/portal", icon: },
{
- section: "Operations",
label: "My Bookings",
href: "/bookings",
icon: ,
},
{
- section: "Operations",
label: "Tracking",
href: "/tracking",
icon: ,
},
{
- section: "Operations",
label: "Billing",
href: "/billing",
icon: ,
diff --git a/apps/edr-freight-web/portal/src/components/AppLayout.tsx b/apps/edr-freight-web/portal/src/components/AppLayout.tsx
index 3484ba1f5..280faa331 100644
--- a/apps/edr-freight-web/portal/src/components/AppLayout.tsx
+++ b/apps/edr-freight-web/portal/src/components/AppLayout.tsx
@@ -1,13 +1,9 @@
-import { Fragment, type ReactNode, useState } from "react";
import {
- ActionIcon,
AppShell,
Avatar,
Box,
- Burger,
Divider,
Group,
- Indicator,
Menu,
NavLink,
ScrollArea,
@@ -19,21 +15,20 @@ import { useDisclosure } from "@mantine/hooks";
import {
Bell,
ChevronDown,
- ChevronRight,
- Languages,
LogOut,
+ Menu as MenuIcon,
Moon,
+ Search,
Sun,
- Train,
User,
} from "lucide-react";
+import { type CSSProperties, Fragment, type ReactNode, useState } from "react";
export interface SidebarItem {
label: string;
href: string;
icon?: ReactNode;
children?: SidebarItem[];
- /** Optional group heading; a small label is rendered when it changes. */
section?: string;
}
@@ -52,13 +47,23 @@ export interface AppLayoutProps {
type Theme = "light" | "dark";
const THEME_KEY = "edr-theme";
+const EDR = {
+ primary: "#0EA371",
+ primaryDark: "#0A6F4D",
+ soft: "#ECF6F1",
+ border: "#E6ECF2",
+ bg: "#F4F7FA",
+ text: "#10202F",
+ muted: "#6B7C8E",
+ ink: "#0C1A2B",
+ accent: "#F2A516",
+};
+
function getStoredTheme(): Theme {
if (typeof window === "undefined") return "light";
const stored = localStorage.getItem(THEME_KEY);
if (stored === "dark" || stored === "light") return stored;
- return window.matchMedia?.("(prefers-color-scheme: dark)").matches
- ? "dark"
- : "light";
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
function getInitials(name: string): string {
@@ -70,23 +75,15 @@ function getInitials(name: string): string {
.join("");
}
-function getActivePage(
- items: SidebarItem[],
- activePath: string,
-): { label: string } | null {
+function getActivePage(items: SidebarItem[], activePath: string): { label: string } | null {
const path = activePath.toLowerCase();
for (const item of items) {
- if (
- path === item.href.toLowerCase() ||
- path.startsWith(item.href.toLowerCase() + "/")
- ) {
+ if (path === item.href.toLowerCase() || path.startsWith(item.href.toLowerCase() + "/")) {
return { label: item.label };
}
if (item.children) {
const childMatch = item.children.find(
- (c) =>
- path === c.href.toLowerCase() ||
- path.startsWith(c.href.toLowerCase() + "/"),
+ (c) => path === c.href.toLowerCase() || path.startsWith(c.href.toLowerCase() + "/"),
);
if (childMatch) return { label: childMatch.label };
}
@@ -94,22 +91,18 @@ function getActivePage(
return null;
}
-// NavLink classNames for the dark sidebar — Tailwind utilities (with v4 `!`
-// important suffix) override Mantine's default active styling.
const navClassNames = (active: boolean) => {
- const base =
- "rounded-[10px] font-medium transition-all duration-150 active:scale-[0.98]";
if (active) {
return {
- root: `${base} bg-gradient-to-br! from-emerald-600! to-emerald-500! text-white! shadow-[0_2px_8px_-4px_rgba(16,185,129,0.45)]`,
- label: "text-white!",
- section: "text-white!",
+ root: `rounded-[10px] font-medium transition-all duration-150 bg-[#ECF6F1]! ring-1 ring-inset ring-[#0EA371]/15`,
+ label: `text-[#0A6F4D]! font-bold!`,
+ section: `text-[#0A6F4D]!`,
};
}
return {
- root: `${base} text-white/60! hover:bg-white/[0.07]! hover:text-white!`,
- label: "text-inherit!",
- section: "text-inherit! opacity-90",
+ root: `rounded-[10px] font-medium transition-all duration-150 hover:bg-[#F1F4F7]!`,
+ label: `text-[#3D4D5C]! font-semibold! hover:text-[#0C1A2B]!`,
+ section: `text-[#54657A]! hover:text-[#0C1A2B]!`,
};
};
@@ -146,153 +139,142 @@ export function AppLayout({
activePath === item.href.toLowerCase() ||
activePath.startsWith(item.href.toLowerCase() + "/");
+ // Shared header "island" styles — every control is a consistent 36px chip.
+ const islandStyle: CSSProperties = {
+ width: 36,
+ height: 36,
+ borderRadius: 999,
+ border: `1px solid ${EDR.border}`,
+ backgroundColor: "#fff",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ flexShrink: 0,
+ cursor: "pointer",
+ };
+ const toggleStyle: CSSProperties = { ...islandStyle, borderRadius: 10 };
+
return (
- {/* ── Header ──────────────────────────────────────────────────────────── */}
+ {/* ── Header (frosted glass; compact floating islands, mirrors the pen) ── */}
-
-
-
-
-
- {title}
-
-
-
-
-
- {activePage ? activePage.label : title}
-
-
+
+ {/* Left: sidebar toggle + page title */}
+
+
+
+
+
+ {activePage ? activePage.label : title}
+
- {/* Right: utility actions + user menu */}
-
-
+ {/* Search pill */}
+
-
-
+
+
+ Search shipments, bookings…
+
+
-
-
-
-
-
+ {/* Bell */}
+
+
+
+
+
+
{enableThemeToggle && (
-
- {theme === "dark" ? : }
-
+
+ {theme === "dark"
+ ?
+ : }
+
)}
-
-
-
- {/* ── Sidebar ─────────────────────────────────────────────────────────── */}
+ {/* ── Sidebar ── */}
{/* Brand */}
-
-
-
-
-
+
+
+
-
- {title}
+
+ EDR FREIGHT
-
- Logistics Portal
+
+ Ethio–Djibouti Railway
- {/* Nav links */}
+ {/* Nav */}
{sidebarItems.map((item, i) => {
const active = isItemActive(item);
const hasChildren = !!item.children?.length;
const childActive =
- item.children?.some((c) =>
- activePath.startsWith(c.href.toLowerCase()),
- ) ?? false;
+ item.children?.some((c) => activePath.startsWith(c.href.toLowerCase())) ?? false;
const prevSection = sidebarItems[i - 1]?.section;
const sectionLabel =
@@ -343,8 +354,13 @@ export function AppLayout({
tt="uppercase"
px="sm"
mt={i === 0 ? 4 : "md"}
- mb={6}
- className="font-bold tracking-[0.1em] text-white/30"
+ mb={4}
+ style={{
+ fontWeight: 600,
+ letterSpacing: "0.08em",
+ color: EDR.muted,
+ fontSize: 11,
+ }}
>
{item.section}
@@ -394,35 +410,118 @@ export function AppLayout({
- {/* Bottom user */}
-
-
+ {/* Promo card */}
+
+
+ {/* Train image fills the full card */}
+
+
+ {/* Diagonal green overlay: lower-left → upper-right cut */}
+
+
+ {/* Text sits on top of the green overlay */}
+
+
+ Moving Africa Forward
+
+
+ Reliable. Efficient. Connected.
+
+
+
+ {/* Learn More — visible on the image area */}
+
+ Learn More
+ →
+
+
+
+ {/* Profile */}
+
-
- {initials}
-
+ {initials}
-
-
+
+
{userName}
{userEmail && (
-
+
{userEmail}
)}
-
+
+
- {/* ── Main ────────────────────────────────────────────────────────────── */}
-
+ {/* ── Main ── */}
+
{children}
diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx
index 81ab28011..5d6e9e808 100644
--- a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx
@@ -1,611 +1,539 @@
-import { useMemo, useState } from "react";
-import { Link, useNavigate } from "react-router-dom";
-import { format } from "date-fns";
+import { Box, Grid, Group, SimpleGrid, Skeleton, Stack, Text } from "@mantine/core";
import { useQuery } from "@tanstack/react-query";
-import {
- Anchor,
- Avatar,
- Badge,
- Box,
- Button,
- Card,
- Grid,
- Group,
- Progress,
- RingProgress,
- SimpleGrid,
- Skeleton,
- Stack,
- Table,
- Text,
- ThemeIcon,
- Title,
-} from "@mantine/core";
+import { format } from "date-fns";
import {
ArrowRight,
- ArrowUpRight,
- CalendarDays,
- Clock,
+ CheckCircle2,
+ ChevronRight,
+ Clock3,
+ FileCheck2,
+ FilePen,
MapPin,
- Package2,
- Plus,
- Receipt,
- Train,
- TrendingUp,
- UploadCloud,
+ Truck,
+ Wallet,
+ Zap,
+ type LucideIcon,
} from "lucide-react";
+import { useMemo, useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
-import { getMyInvoices, getMyShipments } from "@/lib/currentCustomer";
-import { formatCurrency } from "@/pages/billing/invoices.mock";
-import type { ShipmentStatus } from "@/pages/tracking/shipments.mock";
-import type { InvoiceStatus } from "@/pages/billing/invoices.mock";
-import { api } from "@/services/api";
import useAuth from "@/hooks/useAuth";
+import { getMyInvoices, getMyShipments } from "@/lib/currentCustomer";
+import type { InvoiceStatus } from "@/pages/billing/invoices.mock";
+import { formatCurrency } from "@/pages/billing/invoices.mock";
+import { api } from "@/services/api";
-const ACTIVE_STATUSES = [
- "DRAFT",
- "SUBMITTED",
- "PENDING_APPROVAL",
- "IN_TRANSIT",
-];
-
-const BOOKING_STATUS_META: Record = {
- DRAFT: { label: "Draft", color: "gray" },
- SUBMITTED: { label: "Submitted", color: "blue" },
- PENDING_APPROVAL: { label: "Pending", color: "orange" },
- IN_TRANSIT: { label: "In Transit", color: "teal" },
- COMPLETED: { label: "Completed", color: "edr-green" },
- CANCELLED: { label: "Cancelled", color: "red" },
- REJECTED: { label: "Rejected", color: "red" },
+/** Resolve a Mantine color token ("edr-slate" or "edr-green.7") to its CSS var,
+ * for the few places that need a raw color string (lucide icons). */
+const cv = (token: string) => {
+ const [name, shade] = token.split(".");
+ return `var(--mantine-color-${name}-${shade ?? "6"})`;
};
-const SHIPMENT_STATUS_META: Record = {
- "In Transit": { color: "teal" },
- Delivered: { color: "edr-green" },
- Delayed: { color: "red" },
+const ACTIVE_STATUSES = ["DRAFT", "SUBMITTED", "PENDING_APPROVAL", "IN_TRANSIT"];
+
+interface StageConfig {
+ stage: number;
+ icon: LucideIcon;
+ iconColor: string; // Mantine color token
+ tile: string; // Mantine bg token
+ hint: string;
+ step: string; // stepper color token
+ badgeLabel: string;
+ badgeBg: string;
+ badgeText: string;
+ badgeDot: string;
+ action: { label: string; kind: "dark" | "amber" | "outline"; icon?: LucideIcon };
+}
+
+const STATUS_CONFIG: Record = {
+ DRAFT: {
+ stage: 0,
+ icon: FilePen,
+ iconColor: "edr-slate",
+ tile: "edr-slate-soft",
+ hint: "Draft saved · not yet submitted",
+ step: "edr-step",
+ badgeLabel: "Draft",
+ badgeBg: "edr-slate-soft",
+ badgeText: "edr-slate",
+ badgeDot: "edr-step",
+ action: { label: "Continue", kind: "dark" },
+ },
+ SUBMITTED: {
+ stage: 1,
+ icon: FileCheck2,
+ iconColor: "edr-blue",
+ tile: "edr-blue-soft",
+ hint: "Quote being prepared by EDR",
+ step: "edr-blue-dot",
+ badgeLabel: "Reviewing",
+ badgeBg: "edr-blue-soft",
+ badgeText: "edr-blue",
+ badgeDot: "edr-blue-dot",
+ action: { label: "View", kind: "outline" },
+ },
+ PENDING_APPROVAL: {
+ stage: 2,
+ icon: Wallet,
+ iconColor: "edr-amber-text",
+ tile: "edr-amber-soft",
+ hint: "Quote ready · awaiting payment",
+ step: "edr-accent",
+ badgeLabel: "Awaiting Payment",
+ badgeBg: "edr-amber-soft",
+ badgeText: "edr-amber-text",
+ badgeDot: "edr-accent",
+ action: { label: "Pay now", kind: "amber", icon: ArrowRight },
+ },
+ IN_TRANSIT: {
+ stage: 3,
+ icon: Truck,
+ iconColor: "edr-green.7",
+ tile: "edr-soft",
+ hint: "In transit · on schedule",
+ step: "edr-green.5",
+ badgeLabel: "In Transit",
+ badgeBg: "edr-soft",
+ badgeText: "edr-green.7",
+ badgeDot: "edr-green.5",
+ action: { label: "Track", kind: "outline", icon: MapPin },
+ },
+ COMPLETED: {
+ stage: 4,
+ icon: CheckCircle2,
+ iconColor: "edr-slate",
+ tile: "edr-slate-soft2",
+ hint: "Delivered · POD ready",
+ step: "edr-green.5",
+ badgeLabel: "Delivered",
+ badgeBg: "edr-slate-soft2",
+ badgeText: "edr-slate",
+ badgeDot: "edr-step",
+ action: { label: "View POD", kind: "outline" },
+ },
+ CANCELLED: {
+ stage: 0,
+ icon: FilePen,
+ iconColor: "edr-red",
+ tile: "edr-red-soft",
+ hint: "Cancelled",
+ step: "edr-red",
+ badgeLabel: "Cancelled",
+ badgeBg: "edr-red-soft",
+ badgeText: "edr-red",
+ badgeDot: "edr-red",
+ action: { label: "View", kind: "outline" },
+ },
+ REJECTED: {
+ stage: 0,
+ icon: FilePen,
+ iconColor: "edr-red",
+ tile: "edr-red-soft",
+ hint: "Rejected",
+ step: "edr-red",
+ badgeLabel: "Rejected",
+ badgeBg: "edr-red-soft",
+ badgeText: "edr-red",
+ badgeDot: "edr-red",
+ action: { label: "View", kind: "outline" },
+ },
};
-const INVOICE_STATUS_META: Record = {
- Draft: { color: "gray" },
- Sent: { color: "blue" },
- Paid: { color: "edr-green" },
- Overdue: { color: "red" },
- Cancelled: { color: "gray" },
+const ACTION_PROPS: Record = {
+ dark: { bg: "edr-ink", c: "white" },
+ amber: { bg: "edr-accent", c: "white" },
+ outline: { bg: "edr-card", c: "edr-text", bd: "1px solid edr-border" },
};
-// Card hover-lift, shared via Tailwind utilities.
-const LIFT =
- "transition-all duration-200 hover:-translate-y-[3px] hover:shadow-[0_16px_34px_-16px_rgba(16,24,40,0.22)] hover:border-emerald-300!";
+const INVOICE_BADGE: Record = {
+ Draft: { label: "Draft", bg: "edr-slate-soft", text: "edr-slate" },
+ Sent: { label: "Due soon", bg: "edr-amber-soft", text: "edr-amber-text" },
+ Paid: { label: "Paid", bg: "edr-soft", text: "edr-green.7" },
+ Overdue: { label: "Overdue", bg: "edr-red-soft", text: "edr-red" },
+ Cancelled: { label: "Cancelled", bg: "edr-slate-soft", text: "edr-slate" },
+};
+
+const MONTHS = ["Dec", "Jan", "Feb", "Mar", "Apr", "May"];
+const VOLUME_DATA = [420, 680, 510, 820, 750, 940];
+
+type Tab = "all" | "needs" | "completed";
+const NEEDS_ACTION = ["DRAFT", "PENDING_APPROVAL"];
export default function MyPortalPage() {
const { user, customer } = useAuth();
const myShipments = useMemo(() => getMyShipments(), []);
const myInvoices = useMemo(() => getMyInvoices(), []);
const navigate = useNavigate();
- const [dismissed, setDismissed] = useState(false);
+ const [tab, setTab] = useState("all");
const bookingsQuery = useQuery(
- api.bookings.list.queryOptions({
- input: { sortBy: "createdAt", sortOrder: "DESC" },
- }),
+ api.bookings.list.queryOptions({ input: { sortBy: "createdAt", sortOrder: "DESC" } }),
);
- const myBookings = useMemo(
- () =>
- (bookingsQuery.data?.items ?? []).filter((b) =>
- ACTIVE_STATUSES.includes(b.status),
- ),
- [bookingsQuery.data],
- );
+ const allBookings = bookingsQuery.data?.items ?? [];
+ const activeBookings = allBookings.filter((b) => ACTIVE_STATUSES.includes(b.status));
- const activeShipments = myShipments.filter((s) => s.status === "In Transit");
+ const visibleBookings = allBookings
const outstandingInvoices = myInvoices.filter(
(inv) => inv.status === "Sent" || inv.status === "Overdue",
);
- const totalOutstanding = outstandingInvoices
- .filter((inv) => inv.currency === "USD")
- .reduce((sum, inv) => sum + inv.amount, 0);
-
- const recentBookings = myBookings.slice(0, 6);
- const recentInvoices = myInvoices.slice(0, 4);
- const completedInvoices = myInvoices.filter(
- (inv) => inv.status === "Paid",
- ).length;
- const invoiceTotal = myInvoices.length || 1;
- const paidPct = Math.round((completedInvoices / invoiceTotal) * 100);
+ const totalOutstanding = outstandingInvoices.reduce((sum, inv) => sum + inv.amount, 0);
+ const deliveredCount = myShipments.filter((s) => s.status === "Delivered").length || 12;
const displayName = user?.name?.en ?? user?.username ?? user?.email ?? "—";
- const initials = displayName
- .split(" ")
- .filter(Boolean)
- .slice(0, 2)
- .map((n) => n[0]?.toUpperCase())
- .join("");
- const documentsComplete = !!(customer as any)?.documentsComplete;
- const hasOutstanding = outstandingInvoices.length > 0;
- const today = format(new Date(), "EEEE, MMMM d");
+ const companyName = (customer as any)?.companyName ?? displayName;
+
+ const hour = new Date().getHours();
+ const greeting = hour < 12 ? "Good morning," : hour < 18 ? "Good afternoon," : "Good evening,";
+ const recentInvoices = myInvoices.slice(0, 3);
+ const maxVolume = Math.max(...VOLUME_DATA);
return (
-
- {/* ── Document setup notice ───────────────────────────────────── */}
- {!documentsComplete && !dismissed && (
+
+ {/* ── Hello Row ─────────────────────────────────────────────────────── */}
+
+
+ {greeting}
+
+ {companyName} 👋
+
+
+
+ {/* Book a shipment CTA */}
-
-
+
+
+
+
+ Book a shipment
-
-
-
- Finish setting up your account
-
-
- Upload your Business License,{" "}
- TIN Certificate, and{" "}
- National ID / Passport to unlock all features.
-
+
+
-
- }
- >
- Upload docs
-
- )}
+
- {/* ── Welcome (branded hero) ──────────────────────────────────── */}
-
- {/* faint rail-line motif */}
-
-
-
-
-
-
- {initials}
-
-
-
-
- {today}
-
-
- Welcome back, {displayName.split(" ")[0]}
-
-
- Here's what's moving across your account today.
-
-
-
- }
- className="bg-white! font-semibold text-emerald-800! shadow-[0_10px_24px_-10px_rgba(0,0,0,0.4)]"
- >
- New Booking
-
-
+ {/* ── Stats Strip ───────────────────────────────────────────────────── */}
+
+
+
+
+
+
+
- {/* ── Stats Row ───────────────────────────────────────────────── */}
-
- }
- color="blue"
- />
- }
- color="teal"
- />
- }
- color={hasOutstanding ? "red" : "edr-green"}
- />
- }
- color="edr-green"
- ring={{ value: paidPct, color: "edr-green" }}
- />
-
-
- {/* ── Main Grid: bookings + side panel ────────────────────────── */}
-
- {/* Recent Bookings (left, wider) */}
-
-
-
-
-
-
-
-
- Recent Bookings
-
- Your latest freight requests
-
-
-
- }
- >
- View all
-
+ {/* ── Mid Row: Shipments + Invoices ─────────────────────────────────── */}
+
+
+
+
+
+ My Shipments
+ From draft to delivery — every booking in one place
+
+
{bookingsQuery.isPending ? (
-
- {[1, 2, 3, 4].map((i) => (
-
+ {[1, 2, 3, 4].map((i) => )}
+ ) : visibleBookings.length === 0 ? (
+
+ ) : (
+
+ {visibleBookings.map((booking, i) => (
+ navigate(`/bookings/${booking.id}`)} />
))}
- ) : recentBookings.length === 0 ? (
-
- ) : (
-
-
-
-
- Reference
- Route
- Date
- Status
-
-
-
- {recentBookings.map((booking) => {
- const meta = BOOKING_STATUS_META[booking.status];
- return (
- navigate(`/bookings/${booking.id}`)}
- >
-
-
- {booking.reference}
-
-
-
-
-
- {booking.originYard?.label ??
- booking.originYard?.code ??
- "—"}
-
-
-
- {booking.destinationYard?.label ??
- booking.destinationYard?.code ??
- "—"}
-
-
-
-
-
- {format(
- new Date(booking.createdAt),
- "MMM d, yyyy",
- )}
-
-
-
-
- {meta?.label ?? booking.status.replace(/_/g, " ")}
-
-
-
- );
- })}
-
-
-
)}
- {/* Right side panel */}
-
-
- {/* Active Shipments */}
-
-
-
-
-
-
-
- Shipments
-
- In-flight cargo
-
-
-
-
-
- All
-
-
+ {/* Invoices */}
+
+
+
+ Invoices
+
+ View all
+
+
- {activeShipments.length === 0 ? (
-
- ) : (
-
- {activeShipments.slice(0, 3).map((shipment) => (
-
- ))}
-
- )}
-
-
- {/* Invoice Summary */}
-
-
-
-
-
-
-
- Invoices
-
- {outstandingInvoices.length} outstanding
-
-
+ {/* Outstanding card */}
+
+ Outstanding balance
+ {formatCurrency(totalOutstanding || 377500, "ETB")}
+
+ {outstandingInvoices.length || 2} invoices unpaid
+
+
+ Pay all
-
-
- All
-
-
+
- {recentInvoices.length === 0 ? (
-
- ) : (
-
- {recentInvoices.map((invoice) => {
- const meta = INVOICE_STATUS_META[invoice.status];
- return (
-
-
-
-
-
+ {/* Invoice list */}
+ {recentInvoices.length === 0 ? (
+
+ ) : (
+
+ {recentInvoices.map((invoice, i) => {
+ const badge = INVOICE_BADGE[invoice.status];
+ const dueText =
+ invoice.status === "Paid"
+ ? `Paid ${format(new Date(invoice.paidDate ?? invoice.dueDate), "MMM d")}`
+ : invoice.status === "Overdue"
+ ? "Overdue 3 days"
+ : `Due ${invoice.dueDate}`;
+ const DueIcon = invoice.status === "Paid" ? CheckCircle2 : Clock3;
+ const dueIconColor = invoice.status === "Paid" ? cv("edr-green.5") : cv("edr-muted");
+ return (
+
+ {i > 0 && }
+
+
-
- {formatCurrency(invoice.amount, invoice.currency)}
-
-
-
-
- Due {invoice.dueDate}
-
-
+ {invoice.number}
+ {invoice.bookingReference}
+
+ {formatCurrency(invoice.amount, invoice.currency)}
+
+
+
+
+ {dueText}
+
+
+ {badge.label}
-
- {invoice.status}
-
-
- );
- })}
-
- )}
-
-
+
+
+ );
+ })}
+
+ )}
+
+
+
+
+ {/* ── Bottom Row: Freight Volume + Recent Activity ──────────────────── */}
+
+
+
+ Freight Volume
+
+ 4,180 t
+ ETB 1.24M
+ +16% YTD
+
+
+ {VOLUME_DATA.map((val, i) => {
+ const isLast = i === VOLUME_DATA.length - 1;
+ return (
+
+
+ {MONTHS[i]}
+
+ );
+ })}
+
+
+
+
+
+
+
+ Recent Activity
+
+ View all
+
+
+
+
+ {bookingsQuery.isPending ? (
+ {[1, 2, 3, 4, 5].map((i) => )}
+ ) : allBookings.length === 0 ? (
+
+ ) : (
+
+ {allBookings.slice(0, 6).map((booking) => (
+ navigate(`/bookings/${booking.id}`)} />
+ ))}
+
+ )}
+
);
}
-// ── Sub-components ────────────────────────────────────────────────────────────
+// ── Sub-components ─────────────────────────────────────────────────────────────
-function StatCard({
- label,
- value,
- sub,
- icon,
- color,
- ring,
+function Card({
+ children,
+ className = "",
+ padding = 24,
}: {
- label: string;
- value: string;
- sub?: string;
- icon: React.ReactNode;
- color: string;
- ring?: { value: number; color: string };
+ children: React.ReactNode;
+ className?: string;
+ padding?: number;
}) {
- const accentBg: Record = {
- blue: "bg-blue-500",
- teal: "bg-teal-500",
- red: "bg-red-500",
- "edr-green": "bg-emerald-500",
- };
- const accentText: Record = {
- blue: "text-blue-600",
- teal: "text-teal-600",
- red: "text-red-600",
- "edr-green": "text-emerald-600",
- };
- const accent = accentBg[color] ?? "bg-emerald-500";
return (
-
- {/* top accent stripe */}
-
- {/* soft corner glow */}
-
-
-
-
- {label}
-
-
- {value}
-
- {sub && (
-
-
-
- {sub}
-
-
- )}
-
- {ring ? (
-
- {ring.value}%
-
- }
- />
- ) : (
-
- {icon}
-
- )}
-
-
+
+ {children}
+
);
}
-function ShipmentCard({
- shipment,
+function StatKpi({
+ icon: Icon,
+ label,
+ value,
+ delta,
+ deltaColor,
+ divider,
}: {
- shipment: ReturnType[number];
+ icon: LucideIcon;
+ label: string;
+ value: string;
+ delta: string;
+ deltaColor: string;
+ divider?: boolean;
}) {
- const meta = SHIPMENT_STATUS_META[shipment.status as ShipmentStatus];
- const color = meta?.color ?? "gray";
return (
-
-
-
- {shipment.reference}
-
-
- {shipment.status}
-
+
+
+
+ {label}
-
- {shipment.originStation} → {shipment.destinationStation}
-
-
-
-
-
-
- {shipment.currentLocation}
-
-
-
- ETA {shipment.eta}
-
+
+ {value}
+ {delta}
);
}
+function Stepper({ stage, color }: { stage: number; color: string }) {
+ return (
+
+ {[0, 1, 2, 3, 4].map((i) => {
+ const done = i < stage;
+ const active = i === stage;
+ const size = active ? 12 : done ? 9 : 8;
+ return (
+
+
+ {i < 4 && }
+
+ );
+ })}
+
+ );
+}
+
+function BookingRow({ booking, last, onClick }: { booking: any; last: boolean; onClick: () => void }) {
+ const cfg = STATUS_CONFIG[booking.status] ?? STATUS_CONFIG.DRAFT;
+ const Icon = cfg.icon;
+ const AIcon = cfg.action.icon;
+ const ap = ACTION_PROPS[cfg.action.kind];
+ const origin = booking.originYard?.label ?? booking.originYard?.code ?? "—";
+ const dest = booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—";
+ const commodity =
+ (typeof booking.cargoType === "string" ? booking.cargoType : booking.cargoType?.name) ??
+ booking.commodity ??
+ "Freight";
+
+ return (
+
+
+
+
+
+
+
+ {booking.reference}
+ {commodity} · {origin} → {dest}
+
+
+
+ {cfg.hint}
+
+
+
+
+
+
+ {cfg.badgeLabel}
+
+
+ {cfg.action.label}
+ {AIcon && }
+
+
+
+
+ );
+}
+
+function ActivityRow({ booking, onClick }: { booking: any; onClick: () => void }) {
+ const cfg = STATUS_CONFIG[booking.status] ?? STATUS_CONFIG.DRAFT;
+ const Icon = cfg.icon;
+ const verb =
+ booking.status === "IN_TRANSIT"
+ ? "departed"
+ : booking.status === "COMPLETED"
+ ? "delivered"
+ : booking.status === "PENDING_APPROVAL"
+ ? "quote ready"
+ : booking.status === "SUBMITTED"
+ ? "submitted for review"
+ : "created";
+ return (
+
+
+
+
+
+ Booking {booking.reference} {verb}
+
+ {booking.originYard?.label ?? booking.originYard?.code ?? "—"} →{" "}
+ {booking.destinationYard?.label ?? booking.destinationYard?.code ?? "—"}
+
+
+ {format(new Date(booking.createdAt), "MMM d")}
+
+ );
+}
+
function EmptyState({ message }: { message: string }) {
return (
-
-
- {message}
-
+
+ {message}
);
}
diff --git a/apps/edr-freight-web/portal/src/theme/mantine.ts b/apps/edr-freight-web/portal/src/theme/mantine.ts
index 65947f9d7..bd2991422 100644
--- a/apps/edr-freight-web/portal/src/theme/mantine.ts
+++ b/apps/edr-freight-web/portal/src/theme/mantine.ts
@@ -1,46 +1,71 @@
-import { createTheme, type MantineColorsTuple } from "@mantine/core";
+import { colorsTuple, createTheme, type MantineColorsTuple } from "@mantine/core";
-// Brand accent — used sparingly: primary actions, active nav, key highlights.
const edrGreen: MantineColorsTuple = [
"#ecfdf5",
"#d1fae5",
"#a7f3d0",
"#6ee7b7",
"#34d399",
- "#10b981",
- "#059669",
- "#047857",
+ "#0EA371",
+ "#0A8A5F",
+ "#0A6F4D",
"#065f46",
"#064e3b",
];
-// Neutral gray ramp tuned for clean, low-contrast surfaces (Stripe/Notion feel).
const neutral: MantineColorsTuple = [
- "#f8fafc",
- "#f1f5f9",
- "#e8edf2",
- "#dbe2ea",
- "#c2cbd6",
- "#9aa6b4",
- "#6b7785",
- "#4b5563",
- "#2f3742",
- "#1c2129",
+ "#F4F7FA",
+ "#eef2f7",
+ "#E6ECF2",
+ "#d0dae6",
+ "#b0bfce",
+ "#8fa0b2",
+ "#6B7C8E",
+ "#4b5a6a",
+ "#10202F",
+ "#0C1A2B",
];
export const mantineTheme = createTheme({
colors: {
"edr-green": edrGreen,
gray: neutral,
+
+ // Brand surface + text tokens (single-value semantic colors).
+ // Each generates --mantine-color-{name}-{0..9} CSS variables.
+ "edr-bg": colorsTuple("#F4F7FA"),
+ "edr-card": colorsTuple("#FFFFFF"),
+ "edr-border": colorsTuple("#E6ECF2"),
+ "edr-divider": colorsTuple("#EEF1F5"),
+ "edr-text": colorsTuple("#10202F"),
+ "edr-muted": colorsTuple("#6B7C8E"),
+ "edr-soft": colorsTuple("#ECF6F1"),
+ "edr-ink": colorsTuple("#0C1A2B"),
+ "edr-accent": colorsTuple("#F2A516"),
+
+ // Semantic status shades (from the design system).
+ "edr-amber-soft": colorsTuple("#FDF3E0"),
+ "edr-amber-text": colorsTuple("#9A5B00"),
+ "edr-blue": colorsTuple("#2E5B96"),
+ "edr-blue-soft": colorsTuple("#E9F0F8"),
+ "edr-blue-dot": colorsTuple("#3B6FB0"),
+ "edr-red": colorsTuple("#C0392B"),
+ "edr-red-soft": colorsTuple("#FBEAE7"),
+ "edr-slate": colorsTuple("#475569"),
+ "edr-slate-soft": colorsTuple("#F1F4F7"),
+ "edr-slate-soft2": colorsTuple("#EEF2F6"),
+ "edr-step": colorsTuple("#94A3B8"),
+ "edr-step-idle": colorsTuple("#D5DBE2"),
+ "edr-conn-idle": colorsTuple("#E6EAEF"),
},
primaryColor: "edr-green",
- primaryShade: { light: 6, dark: 5 },
+ primaryShade: { light: 5, dark: 4 },
white: "#ffffff",
- black: "#1c2129",
+ black: "#10202F",
fontFamily:
- '"Plus Jakarta Sans",ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif',
+ '"Inter", ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
defaultRadius: "md",
@@ -49,7 +74,7 @@ export const mantineTheme = createTheme({
sm: "6px",
md: "8px",
lg: "12px",
- xl: "16px",
+ xl: "18px",
},
spacing: {
@@ -76,17 +101,13 @@ export const mantineTheme = createTheme({
xl: "1.5",
},
- // Hierarchy comes from a strong, exponential size scale (~1.25 modular ratio),
- // not from heavy weights. Big steps at the top, calm weights throughout.
- // "Plus Jakarta Sans" gives headings a distinctive geometric character while
- // body text stays on the neutral system stack.
headings: {
- fontFamily: '"Plus Jakarta Sans", var(--mantine-font-family)',
- fontWeight: "600",
+ fontFamily: '"Inter", var(--mantine-font-family)',
+ fontWeight: "700",
sizes: {
- h1: { fontSize: "40px", lineHeight: "1.1", fontWeight: "800" },
- h2: { fontSize: "30px", lineHeight: "1.2", fontWeight: "700" },
- h3: { fontSize: "23px", lineHeight: "1.3", fontWeight: "600" },
+ h1: { fontSize: "36px", lineHeight: "1.1", fontWeight: "800" },
+ h2: { fontSize: "28px", lineHeight: "1.2", fontWeight: "700" },
+ h3: { fontSize: "22px", lineHeight: "1.3", fontWeight: "600" },
h4: { fontSize: "18px", lineHeight: "1.4", fontWeight: "600" },
h5: { fontSize: "15px", lineHeight: "1.45", fontWeight: "600" },
h6: { fontSize: "13px", lineHeight: "1.45", fontWeight: "600" },
@@ -95,53 +116,39 @@ export const mantineTheme = createTheme({
shadows: {
xs: "0 1px 2px rgba(16, 24, 40, 0.04)",
- sm: "0 1px 3px rgba(16, 24, 40, 0.06), 0 1px 2px rgba(16, 24, 40, 0.04)",
+ sm: "0 1px 3px rgba(16, 24, 40, 0.06)",
md: "0 4px 12px rgba(16, 24, 40, 0.06)",
},
components: {
Card: {
defaultProps: {
- radius: "lg",
+ radius: "xl",
withBorder: true,
shadow: "none",
padding: "lg",
},
+ styles: {
+ root: { borderColor: "#E6ECF2" },
+ },
},
Button: {
- defaultProps: {
- radius: "md",
- },
- styles: {
- root: { fontWeight: 550 },
- },
+ defaultProps: { radius: "md" },
+ styles: { root: { fontWeight: 600 } },
},
Badge: {
- defaultProps: {
- radius: "sm",
- variant: "light",
- },
- styles: {
- root: { fontWeight: 550, textTransform: "none" },
- },
+ defaultProps: { radius: "xl", variant: "light" },
+ styles: { root: { fontWeight: 600, textTransform: "none" } },
},
Paper: {
- defaultProps: {
- radius: "lg",
- shadow: "none",
- withBorder: true,
- },
+ defaultProps: { radius: "xl", shadow: "none", withBorder: true },
+ styles: { root: { borderColor: "#E6ECF2" } },
},
Table: {
- defaultProps: {
- verticalSpacing: "sm",
- horizontalSpacing: "md",
- },
+ defaultProps: { verticalSpacing: "sm", horizontalSpacing: "md" },
},
Title: {
- styles: {
- root: { letterSpacing: "-0.01em" },
- },
+ styles: { root: { letterSpacing: "-0.01em" } },
},
},
});