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" + ? + : } + )} - - - + {/* Avatar pill */} + - - - - {initials} - - - - - {userName} - - - Customer - - - - + + {initials} + + - - {userName} - - {userEmail && ( - - {userEmail} - - )} + {userName} + {userEmail && {userEmail}} - } - onClick={() => navigate("/profile")} - > + } onClick={() => navigate("/profile")}> Profile - } - color="red" - onClick={onLogout} - > + } color="red" onClick={onLogout}> Logout @@ -301,38 +283,67 @@ export function AppLayout({ - {/* ── Sidebar ─────────────────────────────────────────────────────────── */} + {/* ── Sidebar ── */} {/* Brand */} - - - - - + + + EDR - - {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. - + + - - - )} + - {/* ── Welcome (branded hero) ──────────────────────────────────── */} - - {/* faint rail-line motif */} - - - - - - - {initials} - - - - - {today} - - - Welcome back, {displayName.split(" ")[0]} - - - Here's what's moving across your account today. - - - - - + {/* ── 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 - - - - + {/* ── 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" } }, }, }, });