From 4f20b07d8114628d6206731662398e372430e047 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Wed, 17 Jun 2026 07:02:15 +0000 Subject: [PATCH] refactor: the portal dashboard --- .../portal/src/pages/MyPortalPage.tsx | 1048 ----------------- .../src/pages/MyPortalPage/MyPortalPage.tsx | 104 ++ .../MyPortalPage/components/ActivityRow.tsx | 61 + .../MyPortalPage/components/BookingRow.tsx | 104 ++ .../pages/MyPortalPage/components/Card.tsx | 18 + .../MyPortalPage/components/EmptyState.tsx | 18 + .../components/FreightVolumeSection.tsx | 82 ++ .../MyPortalPage/components/HelloSection.tsx | 51 + .../components/InvoicesSection.tsx | 154 +++ .../components/RecentActivitySection.tsx | 58 + .../MyPortalPage/components/SetupPrompt.tsx | 40 + .../components/ShipmentsSection.tsx | 53 + .../pages/MyPortalPage/components/StatKpi.tsx | 46 + .../MyPortalPage/components/StatsSection.tsx | 76 ++ .../pages/MyPortalPage/components/Stepper.tsx | 42 + .../pages/MyPortalPage/components/index.ts | 13 + .../src/pages/MyPortalPage/constants.ts | 338 ++++++ .../portal/src/pages/MyPortalPage/hooks.ts | 74 ++ .../portal/src/pages/MyPortalPage/index.ts | 1 + 19 files changed, 1333 insertions(+), 1048 deletions(-) delete mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ActivityRow.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/BookingRow.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/Card.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/EmptyState.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/FreightVolumeSection.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/HelloSection.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/InvoicesSection.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/RecentActivitySection.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/SetupPrompt.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ShipmentsSection.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/StatKpi.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/StatsSection.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/Stepper.tsx create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/components/index.ts create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/constants.ts create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/hooks.ts create mode 100644 apps/edr-freight-web/portal/src/pages/MyPortalPage/index.ts diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx deleted file mode 100644 index 197ba3a44..000000000 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage.tsx +++ /dev/null @@ -1,1048 +0,0 @@ -import { - Box, - Grid, - Group, - SimpleGrid, - Skeleton, - Stack, - Text, -} from "@mantine/core"; -import { useQuery } from "@tanstack/react-query"; -import { format } from "date-fns"; -import { - ArrowRight, - CheckCircle2, - ChevronRight, - Clock3, - FileCheck2, - FilePen, - MapPin, - Truck, - Wallet, - Zap, - type LucideIcon, -} from "lucide-react"; -import { useMemo } from "react"; -import { Link, useNavigate } from "react-router-dom"; - -import useAuth from "@/hooks/useAuth"; -import { getMyInvoices } from "@/lib/currentCustomer"; -import type { Currency, InvoiceStatus } from "@/pages/billing/invoices.mock"; -import { formatCurrency } from "@/pages/billing/invoices.mock"; -import { api } from "@/services/api"; - -/** 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"})`; -}; - -/** Format a signed percentage for KPI deltas, e.g. 16 → "+16%", -4 → "-4%". */ -const formatPct = (n: number) => `${n >= 0 ? "+" : ""}${n}%`; - -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" }, - }, - CHANGES_REQUESTED: { - stage: 1, - icon: FilePen, - iconColor: "edr-amber-text", - tile: "edr-amber-soft", - hint: "Changes requested · please update", - step: "edr-accent", - badgeLabel: "Revise", - badgeBg: "edr-amber-soft", - badgeText: "edr-amber-text", - badgeDot: "edr-accent", - action: { label: "Update", kind: "dark" }, - }, - PENDING_APPROVAL: { - stage: 2, - icon: FileCheck2, - iconColor: "edr-blue", - tile: "edr-blue-soft", - hint: "Pending internal approval", - step: "edr-blue-dot", - badgeLabel: "Pending", - badgeBg: "edr-blue-soft", - badgeText: "edr-blue", - badgeDot: "edr-blue-dot", - action: { label: "View", kind: "outline" }, - }, - APPROVED_PENDING_SIGNATURE: { - stage: 2, - icon: FileCheck2, - iconColor: "edr-blue", - tile: "edr-blue-soft", - hint: "Approved · awaiting signature", - step: "edr-blue-dot", - badgeLabel: "For Signature", - badgeBg: "edr-blue-soft", - badgeText: "edr-blue", - badgeDot: "edr-blue-dot", - action: { label: "Review", kind: "outline" }, - }, - APPROVED: { - stage: 2, - icon: CheckCircle2, - iconColor: "edr-green.7", - tile: "edr-soft", - hint: "Quote approved · ready to sign", - step: "edr-green.5", - badgeLabel: "Approved", - badgeBg: "edr-soft", - badgeText: "edr-green.7", - badgeDot: "edr-green.5", - action: { label: "View", kind: "outline" }, - }, - CONTRACT_READY: { - stage: 2, - icon: CheckCircle2, - iconColor: "edr-green.7", - tile: "edr-soft", - hint: "Contract ready · awaiting signature", - step: "edr-green.5", - badgeLabel: "Contract Ready", - badgeBg: "edr-soft", - badgeText: "edr-green.7", - badgeDot: "edr-green.5", - action: { label: "Review", kind: "outline" }, - }, - SIGNED_CUSTOMER: { - stage: 3, - icon: CheckCircle2, - iconColor: "edr-green.7", - tile: "edr-soft", - hint: "Signed by customer · internal processing", - step: "edr-green.5", - badgeLabel: "Signed", - badgeBg: "edr-soft", - badgeText: "edr-green.7", - badgeDot: "edr-green.5", - action: { label: "View", kind: "outline" }, - }, - FULLY_EXECUTED: { - stage: 3, - icon: CheckCircle2, - iconColor: "edr-green.7", - tile: "edr-soft", - hint: "Fully executed · generating PNR", - step: "edr-green.5", - badgeLabel: "Executed", - badgeBg: "edr-soft", - badgeText: "edr-green.7", - badgeDot: "edr-green.5", - action: { label: "View", kind: "outline" }, - }, - PNR_GENERATED: { - stage: 3, - icon: FileCheck2, - iconColor: "edr-blue", - tile: "edr-blue-soft", - hint: "PNR generated · awaiting payment verification", - step: "edr-blue-dot", - badgeLabel: "PNR Ready", - badgeBg: "edr-blue-soft", - badgeText: "edr-blue", - badgeDot: "edr-blue-dot", - action: { label: "View", kind: "outline" }, - }, - PAYMENT_VERIFICATION_IN_PROGRESS: { - stage: 2, - icon: Clock3, - iconColor: "edr-amber-text", - tile: "edr-amber-soft", - hint: "Verifying payment · please wait", - step: "edr-accent", - badgeLabel: "Verifying", - badgeBg: "edr-amber-soft", - badgeText: "edr-amber-text", - badgeDot: "edr-accent", - action: { label: "View", kind: "outline" }, - }, - SELECTED_FOR_BATCH: { - stage: 2, - icon: Wallet, - iconColor: "edr-amber-text", - tile: "edr-amber-soft", - hint: "Selected for batch · payment due within 1 hour", - step: "edr-accent", - badgeLabel: "Pay Now", - badgeBg: "edr-amber-soft", - badgeText: "edr-amber-text", - badgeDot: "edr-accent", - action: { label: "Pay now", kind: "amber", icon: ArrowRight }, - }, - EXPIRED: { - stage: 1, - icon: Clock3, - iconColor: "edr-red", - tile: "edr-red-soft", - hint: "Payment window expired · contact support", - step: "edr-red", - badgeLabel: "Expired", - badgeBg: "edr-red-soft", - badgeText: "edr-red", - badgeDot: "edr-red", - action: { label: "Contact", kind: "outline" }, - }, - PAID: { - stage: 3, - icon: CheckCircle2, - iconColor: "edr-green.7", - tile: "edr-soft", - hint: "Payment received · awaiting dispatch", - step: "edr-green.5", - badgeLabel: "Paid", - badgeBg: "edr-soft", - badgeText: "edr-green.7", - badgeDot: "edr-green.5", - action: { label: "View", kind: "outline" }, - }, - 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: "Completed · awaiting delivery", - step: "edr-green.5", - badgeLabel: "Completed", - badgeBg: "edr-slate-soft2", - badgeText: "edr-slate", - badgeDot: "edr-step", - action: { label: "View", kind: "outline" }, - }, - DELIVERED: { - 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 · contact support", - step: "edr-red", - badgeLabel: "Rejected", - badgeBg: "edr-red-soft", - badgeText: "edr-red", - badgeDot: "edr-red", - action: { label: "Contact", kind: "outline" }, - }, - PENDING_CONSOLIDATION: { - stage: 3, - icon: Clock3, - iconColor: "edr-blue", - tile: "edr-blue-soft", - hint: "Awaiting consolidation", - step: "edr-blue-dot", - badgeLabel: "Consolidating", - badgeBg: "edr-blue-soft", - badgeText: "edr-blue", - badgeDot: "edr-blue-dot", - action: { label: "View", kind: "outline" }, - }, - CONSOLIDATED: { - stage: 3, - icon: CheckCircle2, - iconColor: "edr-green.7", - tile: "edr-soft", - hint: "Consolidated · ready for dispatch", - step: "edr-green.5", - badgeLabel: "Consolidated", - badgeBg: "edr-soft", - badgeText: "edr-green.7", - badgeDot: "edr-green.5", - action: { label: "View", kind: "outline" }, - }, -}; - -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" }, -}; - -const INVOICE_BADGE: Record< - InvoiceStatus, - { label: string; bg: string; text: string } -> = { - 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" }, -}; - -export default function MyPortalPage() { - const { user, customer } = useAuth(); - const myInvoices = useMemo(() => getMyInvoices(), []); - const navigate = useNavigate(); - - const bookingsQuery = useQuery( - api.bookings.list.queryOptions({ - input: { sortBy: "createdAt", sortOrder: "DESC" }, - }), - ); - - const dashboardQuery = useQuery(api.companies.getDashboard.queryOptions()); - const dashboard = dashboardQuery.data; - - const allBookings = bookingsQuery.data?.items ?? []; - const activeBookings = allBookings.filter((b) => - ACTIVE_STATUSES.includes(b.status), - ); - const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; - const newActiveThisWeek = activeBookings.filter( - (b) => new Date(b.createdAt).getTime() >= weekAgo, - ).length; - - const visibleBookings = allBookings; - const outstandingInvoices = myInvoices.filter( - (inv) => inv.status === "Sent" || inv.status === "Overdue", - ); - const totalOutstanding = outstandingInvoices.reduce( - (sum, inv) => sum + inv.amount, - 0, - ); - const displayName = user?.name?.en ?? user?.username ?? user?.email ?? "—"; - 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 volumePoints = dashboard?.freightVolume.monthly ?? []; - const maxVolume = Math.max(1, ...volumePoints.map((p) => p.tonnes)); - - return ( - - {/* ── Hello Row ─────────────────────────────────────────────────────── */} - - - - {greeting} - - - {companyName} 👋 - - - - {/* Book a shipment CTA */} - - - - - - - Book a shipment - - - - - - - - - - {!customer && ( - - - - - Setup your Company Profile - - - Complete your company information to unlock all features and - start booking shipments. - - - - - Complete Setup - - - - - - - - - - - )} - - {/* ── Stats Strip ───────────────────────────────────────────────────── */} - - - - - - - - - - {/* ── Mid Row: Shipments + Invoices ─────────────────────────────────── */} - - - - - - - My Shipments - - - From draft to delivery — every booking in one place - - - - - {bookingsQuery.isPending ? ( - - {[1, 2, 3, 4].map((i) => ( - - ))} - - ) : visibleBookings.length === 0 ? ( - - ) : ( - - {visibleBookings.map((booking, i) => ( - navigate(`/bookings/${booking.id}`)} - /> - ))} - - )} - - - - {/* Invoices */} - - - - - Invoices - - - - - View all - - - - - - - {/* Outstanding card */} - - - Outstanding balance - - - {formatCurrency(totalOutstanding || 377500, "ETB")} - - - - {outstandingInvoices.length || 2} invoices unpaid - - - - - Pay all - - - - - - {/* 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 && } - - - - - {invoice.number} - - - {invoice.bookingReference} - - - - {formatCurrency(invoice.amount, invoice.currency)} - - - - - - - {dueText} - - - - - {badge.label} - - - - - - ); - })} - - )} - - - - - {/* ── Bottom Row: Freight Volume + Recent Activity ──────────────────── */} - - - - - Freight Volume - - - {dashboardQuery.isPending ? ( - - ) : ( - <> - - {( - dashboard?.freightVolume.totalTonnes ?? 0 - ).toLocaleString()}{" "} - t - - - {formatCurrency( - dashboard?.freightVolume.totalValue ?? 0, - (dashboard?.freightVolume.currency ?? "ETB") as Currency, - )} - - - {formatPct(dashboard?.freightVolume.ytdChangePct ?? 0)} YTD - - - )} - - {dashboardQuery.isPending ? ( - - ) : volumePoints.length === 0 ? ( - - - No freight volume yet. - - - ) : ( - - {volumePoints.map((point, i) => { - const isLast = i === volumePoints.length - 1; - return ( - - - - {point.month} - - - ); - })} - - )} - - - - - - - - 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 ───────────────────────────────────────────────────────────── - -function Card({ - children, - className = "", - padding = 24, -}: { - children: React.ReactNode; - className?: string; - padding?: number; -}) { - return ( - - {children} - - ); -} - -function StatKpi({ - icon: Icon, - label, - value, - delta, - deltaColor, - divider, -}: { - icon: LucideIcon; - label: string; - value: string; - delta: string; - deltaColor: string; - divider?: boolean; -}) { - return ( - - - - - {label} - - - - - {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} - - - ); -} diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx new file mode 100644 index 000000000..b9551c321 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx @@ -0,0 +1,104 @@ +import { Grid, Stack } from "@mantine/core"; +import { useNavigate } from "react-router-dom"; +import type { Currency } from "@/pages/billing/invoices.mock"; +import { formatCurrency } from "@/pages/billing/invoices.mock"; +import { + FreightVolumeSection, + HelloSection, + InvoicesSection, + RecentActivitySection, + SetupPrompt, + ShipmentsSection, + StatsSection, +} from "./components"; +import { useMyPortalData } from "./hooks"; + +export default function MyPortalPage() { + const navigate = useNavigate(); + const { + customer, + bookingsQuery, + dashboardQuery, + allBookings, + activeBookings, + newActiveThisWeek, + outstandingInvoices, + totalOutstanding, + companyName, + greeting, + recentInvoices, + dashboard, + volumePoints, + maxVolume, + } = useMyPortalData(); + + const handleBookingClick = (id: string) => { + navigate(`/bookings/${id}`); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ActivityRow.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ActivityRow.tsx new file mode 100644 index 000000000..a65aa009e --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ActivityRow.tsx @@ -0,0 +1,61 @@ +import { Box, Group, Text } from "@mantine/core"; +import { format } from "date-fns"; +import { memo } from "react"; +import { STATUS_CONFIG, cv } from "../constants"; + +interface ActivityRowProps { + booking: any; + onClick: () => void; +} + +export const ActivityRow = memo(function ActivityRow({ + booking, + onClick, +}: ActivityRowProps) { + 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")} + + + ); +}); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/BookingRow.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/BookingRow.tsx new file mode 100644 index 000000000..27b923467 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/BookingRow.tsx @@ -0,0 +1,104 @@ +import { Box, Group, Stack, Text } from "@mantine/core"; +import { memo } from "react"; +import { ACTION_PROPS, STATUS_CONFIG, cv } from "../constants"; +import { Stepper } from "./Stepper"; + +interface BookingRowProps { + booking: any; + last: boolean; + onClick: () => void; +} + +export const BookingRow = memo(function BookingRow({ + booking, + last, + onClick, +}: BookingRowProps) { + 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 && ( + + )} + + + + + ); +}); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/Card.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/Card.tsx new file mode 100644 index 000000000..2f24d0a59 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/Card.tsx @@ -0,0 +1,18 @@ +import { Box } from "@mantine/core"; + +interface CardProps { + children: React.ReactNode; + className?: string; + padding?: number; +} + +export function Card({ children, className = "", padding = 24 }: CardProps) { + return ( + + {children} + + ); +} diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/EmptyState.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/EmptyState.tsx new file mode 100644 index 000000000..66f3f16a3 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/EmptyState.tsx @@ -0,0 +1,18 @@ +import { Box, Text } from "@mantine/core"; + +interface EmptyStateProps { + message: string; +} + +export function EmptyState({ message }: EmptyStateProps) { + return ( + + + {message} + + + ); +} diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/FreightVolumeSection.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/FreightVolumeSection.tsx new file mode 100644 index 000000000..27b74cb6f --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/FreightVolumeSection.tsx @@ -0,0 +1,82 @@ +import { Box, Group, Skeleton, Text } from "@mantine/core"; +import { memo } from "react"; +import type { Currency } from "@/pages/billing/invoices.mock"; +import { formatCurrency } from "@/pages/billing/invoices.mock"; +import { formatPct } from "../constants"; +import { Card } from "./Card"; + +interface FreightVolumeSectionProps { + totalTonnes: number; + totalValue: number; + currency: Currency; + ytdChangePct: number; + volumePoints: Array<{ month: string; tonnes: number }>; + maxVolume: number; + isLoading: boolean; +} + +export const FreightVolumeSection = memo(function FreightVolumeSection({ + totalTonnes, + totalValue, + currency, + ytdChangePct, + volumePoints, + maxVolume, + isLoading, +}: FreightVolumeSectionProps) { + return ( + + + Freight Volume + + + {isLoading ? ( + + ) : ( + <> + + {totalTonnes.toLocaleString()} t + + + {formatCurrency(totalValue, currency)} + + + {formatPct(ytdChangePct)} YTD + + + )} + + {isLoading ? ( + + ) : volumePoints.length === 0 ? ( + + + No freight volume yet. + + + ) : ( + + {volumePoints.map((point, i) => { + const isLast = i === volumePoints.length - 1; + return ( + + + + {point.month} + + + ); + })} + + )} + + ); +}); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/HelloSection.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/HelloSection.tsx new file mode 100644 index 000000000..9f1d726b0 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/HelloSection.tsx @@ -0,0 +1,51 @@ +import { Box, Group, Text } from "@mantine/core"; +import { ArrowRight, Truck } from "lucide-react"; +import { memo } from "react"; +import { Link } from "react-router-dom"; +import { cv } from "../constants"; + +interface HelloSectionProps { + greeting: string; + companyName: string; +} + +export const HelloSection = memo(function HelloSection({ + greeting, + companyName, +}: HelloSectionProps) { + return ( + + + + {greeting} + + + {companyName} 👋 + + + + + + + + + + Book a shipment + + + + + + + + + ); +}); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/InvoicesSection.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/InvoicesSection.tsx new file mode 100644 index 000000000..fcdc14095 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/InvoicesSection.tsx @@ -0,0 +1,154 @@ +import { Box, Group, Stack, Text } from "@mantine/core"; +import { CheckCircle2, ChevronRight, Clock3, Zap } from "lucide-react"; +import { format } from "date-fns"; +import { memo } from "react"; +import { Link } from "react-router-dom"; +import type { Currency, InvoiceStatus } from "@/pages/billing/invoices.mock"; +import { formatCurrency } from "@/pages/billing/invoices.mock"; +import { cv, INVOICE_BADGE } from "../constants"; +import { Card } from "./Card"; +import { EmptyState } from "./EmptyState"; + +interface InvoicesSectionProps { + invoices: Array<{ + id: string; + number: string; + bookingReference: string; + amount: number; + currency: Currency; + status: InvoiceStatus; + dueDate: string; + paidDate?: string; + }>; +} + +export const InvoicesSection = memo(function InvoicesSection({ + invoices, +}: InvoicesSectionProps) { + const outstandingInvoices = invoices.filter( + (inv) => inv.status === "Sent" || inv.status === "Overdue", + ); + const totalOutstanding = outstandingInvoices.reduce( + (sum, inv) => sum + inv.amount, + 0, + ); + + return ( + + + + Invoices + + + + + View all + + + + + + + + + Outstanding balance + + + {formatCurrency(totalOutstanding || 377500, "ETB")} + + + + {outstandingInvoices.length || 2} invoices unpaid + + + + + Pay all + + + + + + {invoices.length === 0 ? ( + + ) : ( + + {invoices.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 && } + + + + + {invoice.number} + + + {invoice.bookingReference} + + + + {formatCurrency(invoice.amount, invoice.currency)} + + + + + + + {dueText} + + + + + {badge.label} + + + + + + ); + })} + + )} + + ); +}); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/RecentActivitySection.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/RecentActivitySection.tsx new file mode 100644 index 000000000..44a8e7fa6 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/RecentActivitySection.tsx @@ -0,0 +1,58 @@ +import { ChevronRight } from "lucide-react"; +import { Group, Skeleton, Stack, Text } from "@mantine/core"; +import { memo } from "react"; +import { Link } from "react-router-dom"; +import { cv } from "../constants"; +import { ActivityRow } from "./ActivityRow"; +import { Card } from "./Card"; +import { EmptyState } from "./EmptyState"; + +interface RecentActivitySectionProps { + bookings: any[]; + isLoading: boolean; + onBookingClick: (id: string) => void; +} + +export const RecentActivitySection = memo(function RecentActivitySection({ + bookings, + isLoading, + onBookingClick, +}: RecentActivitySectionProps) { + return ( + + + + Recent Activity + + + + + View all + + + + + + + {isLoading ? ( + + {[1, 2, 3, 4, 5].map((i) => ( + + ))} + + ) : bookings.length === 0 ? ( + + ) : ( + + {bookings.slice(0, 6).map((booking) => ( + onBookingClick(booking.id)} + /> + ))} + + )} + + ); +}); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/SetupPrompt.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/SetupPrompt.tsx new file mode 100644 index 000000000..cb29865e2 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/SetupPrompt.tsx @@ -0,0 +1,40 @@ +import { Box, Group, Text } from "@mantine/core"; +import { ArrowRight, Truck } from "lucide-react"; +import { memo } from "react"; +import { Link } from "react-router-dom"; +import { cv } from "../constants"; + +interface SetupPromptProps { + show: boolean; +} + +export const SetupPrompt = memo(function SetupPrompt({ show }: SetupPromptProps) { + if (!show) return null; + + return ( + + + + + Setup your Company Profile + + + Complete your company information to unlock all features and start + booking shipments. + + + + + Complete Setup + + + + + + + + + + + ); +}); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ShipmentsSection.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ShipmentsSection.tsx new file mode 100644 index 000000000..d4237914b --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/ShipmentsSection.tsx @@ -0,0 +1,53 @@ +import { Box, Group, Skeleton, Stack, Text } from "@mantine/core"; +import { memo } from "react"; +import { BookingRow } from "./BookingRow"; +import { Card } from "./Card"; +import { EmptyState } from "./EmptyState"; + +interface ShipmentsSectionProps { + bookings: any[]; + isLoading: boolean; + onBookingClick: (id: string) => void; +} + +export const ShipmentsSection = memo(function ShipmentsSection({ + bookings, + isLoading, + onBookingClick, +}: ShipmentsSectionProps) { + return ( + + + + + My Shipments + + + From draft to delivery — every booking in one place + + + + + {isLoading ? ( + + {[1, 2, 3, 4].map((i) => ( + + ))} + + ) : bookings.length === 0 ? ( + + ) : ( + + {bookings.map((booking, i) => ( + onBookingClick(booking.id)} + /> + ))} + + )} + + ); +}); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/StatKpi.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/StatKpi.tsx new file mode 100644 index 000000000..413caafc4 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/StatKpi.tsx @@ -0,0 +1,46 @@ +import { Box, Group, Text } from "@mantine/core"; +import { memo } from "react"; +import type { LucideIcon } from "lucide-react"; +import { cv } from "../constants"; + +interface StatKpiProps { + icon: LucideIcon; + label: string; + value: string; + delta: string; + deltaColor: string; + divider?: boolean; +} + +export const StatKpi = memo(function StatKpi({ + icon: Icon, + label, + value, + delta, + deltaColor, + divider, +}: StatKpiProps) { + return ( + + + + + {label} + + + + + {value} + + + {delta} + + + + ); +}); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/StatsSection.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/StatsSection.tsx new file mode 100644 index 000000000..006907eeb --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/StatsSection.tsx @@ -0,0 +1,76 @@ +import { Box, SimpleGrid } from "@mantine/core"; +import { + CheckCircle2, + Clock3, + Truck, + Wallet, +} from "lucide-react"; +import { memo } from "react"; +import { formatCurrency } from "@/pages/billing/invoices.mock"; +import { formatPct } from "../constants"; +import { Card } from "./Card"; +import { StatKpi } from "./StatKpi"; + +interface StatsSectionProps { + activeBookingsLength: number; + newActiveThisWeek: number; + bookingsLoading: boolean; + outstandingInvoicesLength: number; + totalOutstanding: number; + deliveredCount: string | undefined; + completionRate: string | undefined; + spendYtd: string | undefined; + spendYtdChangePct: number | undefined; + dashboardLoading: boolean; +} + +export const StatsSection = memo(function StatsSection({ + activeBookingsLength, + newActiveThisWeek, + bookingsLoading, + outstandingInvoicesLength, + totalOutstanding, + deliveredCount, + completionRate, + spendYtd, + spendYtdChangePct, + dashboardLoading, +}: StatsSectionProps) { + return ( + + + + + + + + + ); +}); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/Stepper.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/Stepper.tsx new file mode 100644 index 000000000..820ba6034 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/Stepper.tsx @@ -0,0 +1,42 @@ +import { Box, Group } from "@mantine/core"; +import { memo } from "react"; + +interface StepperProps { + stage: number; + color: string; +} + +export const Stepper = memo(function Stepper({ stage, color }: StepperProps) { + 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 && ( + + )} + + ); + })} + + ); +}); diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/index.ts b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/index.ts new file mode 100644 index 000000000..de3ddb448 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/index.ts @@ -0,0 +1,13 @@ +export { ActivityRow } from "./ActivityRow"; +export { BookingRow } from "./BookingRow"; +export { Card } from "./Card"; +export { EmptyState } from "./EmptyState"; +export { FreightVolumeSection } from "./FreightVolumeSection"; +export { HelloSection } from "./HelloSection"; +export { InvoicesSection } from "./InvoicesSection"; +export { RecentActivitySection } from "./RecentActivitySection"; +export { SetupPrompt } from "./SetupPrompt"; +export { ShipmentsSection } from "./ShipmentsSection"; +export { StatKpi } from "./StatKpi"; +export { StatsSection } from "./StatsSection"; +export { Stepper } from "./Stepper"; diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/constants.ts b/apps/edr-freight-web/portal/src/pages/MyPortalPage/constants.ts new file mode 100644 index 000000000..c08b5e73a --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/constants.ts @@ -0,0 +1,338 @@ +import { + ArrowRight, + CheckCircle2, + Clock3, + FileCheck2, + FilePen, + MapPin, + Truck, + Wallet, + type LucideIcon, +} from "lucide-react"; +import type { Currency, InvoiceStatus } from "@/pages/billing/invoices.mock"; + +export const cv = (token: string) => { + const [name, shade] = token.split("."); + return `var(--mantine-color-${name}-${shade ?? "6"})`; +}; + +export const formatPct = (n: number) => `${n >= 0 ? "+" : ""}${n}%`; + +export const ACTIVE_STATUSES = [ + "DRAFT", + "SUBMITTED", + "PENDING_APPROVAL", + "IN_TRANSIT", +]; + +export interface StageConfig { + stage: number; + icon: LucideIcon; + iconColor: string; + tile: string; + hint: string; + step: string; + badgeLabel: string; + badgeBg: string; + badgeText: string; + badgeDot: string; + action: { + label: string; + kind: "dark" | "amber" | "outline"; + icon?: LucideIcon; + }; +} + +export 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" }, + }, + CHANGES_REQUESTED: { + stage: 1, + icon: FilePen, + iconColor: "edr-amber-text", + tile: "edr-amber-soft", + hint: "Changes requested · please update", + step: "edr-accent", + badgeLabel: "Revise", + badgeBg: "edr-amber-soft", + badgeText: "edr-amber-text", + badgeDot: "edr-accent", + action: { label: "Update", kind: "dark" }, + }, + PENDING_APPROVAL: { + stage: 2, + icon: FileCheck2, + iconColor: "edr-blue", + tile: "edr-blue-soft", + hint: "Pending internal approval", + step: "edr-blue-dot", + badgeLabel: "Pending", + badgeBg: "edr-blue-soft", + badgeText: "edr-blue", + badgeDot: "edr-blue-dot", + action: { label: "View", kind: "outline" }, + }, + APPROVED_PENDING_SIGNATURE: { + stage: 2, + icon: FileCheck2, + iconColor: "edr-blue", + tile: "edr-blue-soft", + hint: "Approved · awaiting signature", + step: "edr-blue-dot", + badgeLabel: "For Signature", + badgeBg: "edr-blue-soft", + badgeText: "edr-blue", + badgeDot: "edr-blue-dot", + action: { label: "Review", kind: "outline" }, + }, + APPROVED: { + stage: 2, + icon: CheckCircle2, + iconColor: "edr-green.7", + tile: "edr-soft", + hint: "Quote approved · ready to sign", + step: "edr-green.5", + badgeLabel: "Approved", + badgeBg: "edr-soft", + badgeText: "edr-green.7", + badgeDot: "edr-green.5", + action: { label: "View", kind: "outline" }, + }, + CONTRACT_READY: { + stage: 2, + icon: CheckCircle2, + iconColor: "edr-green.7", + tile: "edr-soft", + hint: "Contract ready · awaiting signature", + step: "edr-green.5", + badgeLabel: "Contract Ready", + badgeBg: "edr-soft", + badgeText: "edr-green.7", + badgeDot: "edr-green.5", + action: { label: "Review", kind: "outline" }, + }, + SIGNED_CUSTOMER: { + stage: 3, + icon: CheckCircle2, + iconColor: "edr-green.7", + tile: "edr-soft", + hint: "Signed by customer · internal processing", + step: "edr-green.5", + badgeLabel: "Signed", + badgeBg: "edr-soft", + badgeText: "edr-green.7", + badgeDot: "edr-green.5", + action: { label: "View", kind: "outline" }, + }, + FULLY_EXECUTED: { + stage: 3, + icon: CheckCircle2, + iconColor: "edr-green.7", + tile: "edr-soft", + hint: "Fully executed · generating PNR", + step: "edr-green.5", + badgeLabel: "Executed", + badgeBg: "edr-soft", + badgeText: "edr-green.7", + badgeDot: "edr-green.5", + action: { label: "View", kind: "outline" }, + }, + PNR_GENERATED: { + stage: 3, + icon: FileCheck2, + iconColor: "edr-blue", + tile: "edr-blue-soft", + hint: "PNR generated · awaiting payment verification", + step: "edr-blue-dot", + badgeLabel: "PNR Ready", + badgeBg: "edr-blue-soft", + badgeText: "edr-blue", + badgeDot: "edr-blue-dot", + action: { label: "View", kind: "outline" }, + }, + PAYMENT_VERIFICATION_IN_PROGRESS: { + stage: 2, + icon: Clock3, + iconColor: "edr-amber-text", + tile: "edr-amber-soft", + hint: "Verifying payment · please wait", + step: "edr-accent", + badgeLabel: "Verifying", + badgeBg: "edr-amber-soft", + badgeText: "edr-amber-text", + badgeDot: "edr-accent", + action: { label: "View", kind: "outline" }, + }, + SELECTED_FOR_BATCH: { + stage: 2, + icon: Wallet, + iconColor: "edr-amber-text", + tile: "edr-amber-soft", + hint: "Selected for batch · payment due within 1 hour", + step: "edr-accent", + badgeLabel: "Pay Now", + badgeBg: "edr-amber-soft", + badgeText: "edr-amber-text", + badgeDot: "edr-accent", + action: { label: "Pay now", kind: "amber", icon: ArrowRight }, + }, + EXPIRED: { + stage: 1, + icon: Clock3, + iconColor: "edr-red", + tile: "edr-red-soft", + hint: "Payment window expired · contact support", + step: "edr-red", + badgeLabel: "Expired", + badgeBg: "edr-red-soft", + badgeText: "edr-red", + badgeDot: "edr-red", + action: { label: "Contact", kind: "outline" }, + }, + PAID: { + stage: 3, + icon: CheckCircle2, + iconColor: "edr-green.7", + tile: "edr-soft", + hint: "Payment received · awaiting dispatch", + step: "edr-green.5", + badgeLabel: "Paid", + badgeBg: "edr-soft", + badgeText: "edr-green.7", + badgeDot: "edr-green.5", + action: { label: "View", kind: "outline" }, + }, + 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: "Completed · awaiting delivery", + step: "edr-green.5", + badgeLabel: "Completed", + badgeBg: "edr-slate-soft2", + badgeText: "edr-slate", + badgeDot: "edr-step", + action: { label: "View", kind: "outline" }, + }, + DELIVERED: { + 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 · contact support", + step: "edr-red", + badgeLabel: "Rejected", + badgeBg: "edr-red-soft", + badgeText: "edr-red", + badgeDot: "edr-red", + action: { label: "Contact", kind: "outline" }, + }, + PENDING_CONSOLIDATION: { + stage: 3, + icon: Clock3, + iconColor: "edr-blue", + tile: "edr-blue-soft", + hint: "Awaiting consolidation", + step: "edr-blue-dot", + badgeLabel: "Consolidating", + badgeBg: "edr-blue-soft", + badgeText: "edr-blue", + badgeDot: "edr-blue-dot", + action: { label: "View", kind: "outline" }, + }, + CONSOLIDATED: { + stage: 3, + icon: CheckCircle2, + iconColor: "edr-green.7", + tile: "edr-soft", + hint: "Consolidated · ready for dispatch", + step: "edr-green.5", + badgeLabel: "Consolidated", + badgeBg: "edr-soft", + badgeText: "edr-green.7", + badgeDot: "edr-green.5", + action: { label: "View", kind: "outline" }, + }, +}; + +export 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" }, + }; + +export const INVOICE_BADGE: Record< + InvoiceStatus, + { label: string; bg: string; text: string } +> = { + 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" }, +}; diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/hooks.ts b/apps/edr-freight-web/portal/src/pages/MyPortalPage/hooks.ts new file mode 100644 index 000000000..fe2701840 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/hooks.ts @@ -0,0 +1,74 @@ +import { useQuery } from "@tanstack/react-query"; +import { useMemo } from "react"; +import useAuth from "@/hooks/useAuth"; +import { getMyInvoices } from "@/lib/currentCustomer"; +import { api } from "@/services/api"; +import { ACTIVE_STATUSES } from "./constants"; + +export function useMyPortalData() { + const { user, customer } = useAuth(); + const myInvoices = useMemo(() => getMyInvoices(), []); + + const bookingsQuery = useQuery( + api.bookings.list.queryOptions({ + input: { sortBy: "createdAt", sortOrder: "DESC" }, + }), + ); + + const dashboardQuery = useQuery(api.companies.getDashboard.queryOptions()); + + const allBookings = bookingsQuery.data?.items ?? []; + const activeBookings = allBookings.filter((b) => + ACTIVE_STATUSES.includes(b.status), + ); + + const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000; + const newActiveThisWeek = activeBookings.filter( + (b) => new Date(b.createdAt).getTime() >= weekAgo, + ).length; + + const outstandingInvoices = myInvoices.filter( + (inv) => inv.status === "Sent" || inv.status === "Overdue", + ); + + const totalOutstanding = outstandingInvoices.reduce( + (sum, inv) => sum + inv.amount, + 0, + ); + + const displayName = user?.name?.en ?? user?.username ?? user?.email ?? "—"; + 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 dashboard = dashboardQuery.data; + const volumePoints = dashboard?.freightVolume.monthly ?? []; + const maxVolume = Math.max(1, ...volumePoints.map((p) => p.tonnes)); + + return { + user, + customer, + bookingsQuery, + dashboardQuery, + allBookings, + activeBookings, + newActiveThisWeek, + outstandingInvoices, + totalOutstanding, + companyName, + greeting, + recentInvoices, + dashboard, + volumePoints, + maxVolume, + myInvoices, + }; +} diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/index.ts b/apps/edr-freight-web/portal/src/pages/MyPortalPage/index.ts new file mode 100644 index 000000000..9b18dc3af --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/index.ts @@ -0,0 +1 @@ +export { default } from "./MyPortalPage";