From 86d69a95779c049b13e3382974b3f6ac37e29003 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Thu, 13 Aug 2026 11:22:45 +0000 Subject: [PATCH] chore: group invoices, payments and usd payments --- apps/edr-freight-web/backoffice/src/App.tsx | 32 +-- .../src/components/layout/route-meta.ts | 8 +- .../components/layout/sidebar-sections.tsx | 20 +- .../src/pages/invoices/FinanceHubPage.tsx | 100 +++++++++ .../src/pages/invoices/InvoicesPage.tsx | 205 +++++++++--------- .../src/pages/invoices/UsdPaymentsPage.tsx | 35 ++- .../src/pages/payments/PaymentsPage.tsx | 14 +- 7 files changed, 244 insertions(+), 170 deletions(-) create mode 100644 apps/edr-freight-web/backoffice/src/pages/invoices/FinanceHubPage.tsx diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index a176855f4..cf6e275ca 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -37,15 +37,13 @@ import DocumentClearanceListPage from "./pages/bookings/DocumentClearanceListPag import CustomerDetailPage from "./pages/customers/CustomerDetailPage"; import CustomersPage from "./pages/customers/CustomersPage"; import InvoiceDetailPage from "./pages/invoices/InvoiceDetailPage"; -import InvoicesPage from "./pages/invoices/InvoicesPage"; -import UsdPaymentsPage from "./pages/invoices/UsdPaymentsPage"; +import FinanceHubPage from "./pages/invoices/FinanceHubPage"; import MyProfilePage from "./pages/dashboard/MyProfilePage"; import OverviewPage from "./pages/dashboard/OverviewPage"; import ReportsIndexRedirect from "./pages/reports/ReportsIndexRedirect"; import ReportPage from "./pages/reports/ReportPage"; import AuditLogsPage from "./pages/AuditLogsPage"; import AiBookingMockTestPage from "./pages/ai/AiBookingMockTestPage"; -import PaymentsPage from "./pages/payments/PaymentsPage"; //import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage"; import { RequirePermission } from "./components/auth/RequirePermission"; import { FREIGHT_PERMS } from "./lib/permissions"; @@ -266,13 +264,11 @@ const App = () => { } /> + {/* Payments used to be its own page; it's now the "payments" tab on + the merged Invoices hub. Old bookmarks/links still land there. */} - - - } + element={} /> { } /> + {/* Merged Invoices / Payments / USD Payments hub — tabs switch via + ?tab=invoices|payments|usd-payments (default invoices). Access is + OR'd across both keys so a user with just one still gets in; each + tab hides itself if the user lacks the permission it used to be + routed on. */} - + + } /> - - - } + element={} /> = [ }, }, { - prefix: "/dashboard/payments", + // Invoices, Payments, and USD Payments are tabs on one page now + // (FinanceHubPage); the header title itself is set per-tab there. + prefix: "/dashboard/invoices", meta: { - title: "Payments", - subtitle: "View booking payment transactions", + title: "Invoices", + subtitle: "Invoices, payments, and USD bank transfers", }, }, { diff --git a/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx b/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx index 7114fa129..e3db92296 100644 --- a/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx +++ b/apps/edr-freight-web/backoffice/src/components/layout/sidebar-sections.tsx @@ -9,7 +9,6 @@ import { FileText, Hammer, History, - Landmark, LayoutDashboard, LayoutGrid, MapPin, @@ -97,22 +96,13 @@ export const buildSidebarSections = ( permission: FREIGHT_PERMS.contracts.opsClearanceReview, }, { - label: "Payments", - href: "/dashboard/payments", - icon: , - permission: FREIGHT_PERMS.payments.view, - }, - { - label: "Invoices", + // Invoices, Payments, and USD Payments live on one page as tabs + // (FinanceHubPage) — single nav entry, OR'd across both keys so + // either permission alone still gets a user in. + label: "Transactions", href: "/dashboard/invoices", icon: , - permission: FREIGHT_PERMS.invoices.view, - }, - { - label: "USD Payments", - href: "/dashboard/usd-payments", - icon: , - permission: FREIGHT_PERMS.invoices.view, + permission: [FREIGHT_PERMS.invoices.view, FREIGHT_PERMS.payments.view], }, { label: "Support", diff --git a/apps/edr-freight-web/backoffice/src/pages/invoices/FinanceHubPage.tsx b/apps/edr-freight-web/backoffice/src/pages/invoices/FinanceHubPage.tsx new file mode 100644 index 000000000..bd5169262 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/pages/invoices/FinanceHubPage.tsx @@ -0,0 +1,100 @@ +import { Tabs } from "@mantine/core"; +import { Landmark, Receipt, Wallet } from "lucide-react"; +import { useSearchParams } from "react-router-dom"; + +import { useAuth } from "@/auth/useAuth"; +import { PageContainer, PageHeader } from "@/components/page"; +import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions"; + +import InvoicesPanel from "./InvoicesPage"; +import UsdPaymentsPanel from "./UsdPaymentsPage"; +import PaymentsPanel from "../payments/PaymentsPage"; + +/** + * Invoices, Payments, and USD Payments used to be three separate routes/pages + * with near-identical chrome. They're merged here as URL-linkable tabs + * (`?tab=`) on one page — each tab keeps the permission it was individually + * gated on before, and just doesn't render if the user lacks it. + */ +const TABS = [ + { + key: "invoices", + label: "Invoices", + icon: Receipt, + permission: FREIGHT_PERMS.invoices.view, + subtitle: + "Every invoice issued across bookings, warehouse fees and clearance charges.", + Panel: InvoicesPanel, + }, + { + key: "payments", + label: "Payments", + icon: Wallet, + permission: FREIGHT_PERMS.payments.view, + subtitle: "View and reconcile booking payment transactions.", + Panel: PaymentsPanel, + }, + { + key: "usd-payments", + label: "USD Payments", + icon: Landmark, + // Same gate as Invoices, not a dedicated key — mirrors the old route. + permission: FREIGHT_PERMS.invoices.view, + subtitle: + "USD invoices are paid by bank transfer. Upload the customer's slip and confirm the payment before the pay window closes.", + Panel: UsdPaymentsPanel, + }, +] as const; + +type TabKey = (typeof TABS)[number]["key"]; + +export default function FinanceHubPage() { + const { user } = useAuth(); + const [searchParams, setSearchParams] = useSearchParams(); + + const visibleTabs = TABS.filter((tab) => hasPermission(user, tab.permission)); + const requested = searchParams.get("tab"); + const active: TabKey = + visibleTabs.find((tab) => tab.key === requested)?.key ?? + visibleTabs[0]?.key ?? + "invoices"; + const activeTab = visibleTabs.find((tab) => tab.key === active); + + const handleChange = (value: string | null) => { + if (!value) return; + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + next.set("tab", value); + return next; + }, + { replace: true }, + ); + }; + + return ( + + + + + + {visibleTabs.map((tab) => ( + } + > + {tab.label} + + ))} + + + {visibleTabs.map((tab) => ( + + + + ))} + + + ); +} diff --git a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx index fd839de16..afa4fa603 100644 --- a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx @@ -21,7 +21,6 @@ import { formatMoney, humanize, } from "@/components/customers"; -import { PageContainer, PageHeader } from "@/components/page"; import { api } from "@/services/api"; import type { Invoice } from "@/types/invoice"; import { @@ -31,7 +30,8 @@ import { type ColumnDef, } from "@edr/ui-common"; -export default function InvoicesPage() { +/** Invoices tab body of `FinanceHubPage` — page chrome lives in the parent. */ +export default function InvoicesPanel() { const navigate = useNavigate(); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [query, setQuery] = useState(""); @@ -127,112 +127,103 @@ export default function InvoicesPage() { ); return ( - - void refetch()} - > - - - } - /> + + + + + } + value={query} + onChange={(e) => setQuery(e.target.value)} + rightSection={ + query ? ( + setQuery("")} + > + + + ) : null + } + style={{ flex: 1, minWidth: "240px" }} + radius="lg" + /> + { + setStatusFilter( + v === "all" ? "" : (v as Freight.InvoiceStatus), + ); + setPagination((prev) => ({ ...prev, pageIndex: 0 })); + }} + data={[ + { label: "All", value: "all" }, + { label: "Pending", value: "PENDING" }, + { label: "Payment processing", value: "PAYMENT_PROCESSING" }, + { label: "Paid", value: "PAID" }, + { label: "Overdue", value: "OVERDUE" }, + ]} + /> + + {total} record{total !== 1 ? "s" : ""} + + void refetch()} + > + + + + - - - - - } - value={query} - onChange={(e) => setQuery(e.target.value)} - rightSection={ - query ? ( - setQuery("")} - > - - - ) : null - } - style={{ flex: 1, minWidth: "240px" }} - radius="lg" - /> - { - setStatusFilter( - v === "all" ? "" : (v as Freight.InvoiceStatus), - ); - setPagination((prev) => ({ ...prev, pageIndex: 0 })); - }} - data={[ - { label: "All", value: "all" }, - { label: "Pending", value: "PENDING" }, - { label: "Payment processing", value: "PAYMENT_PROCESSING" }, - { label: "Paid", value: "PAID" }, - { label: "Overdue", value: "OVERDUE" }, - ]} - /> - - {total} record{total !== 1 ? "s" : ""} - - + + + navigate(`/dashboard/invoices/${row.id}`)} + emptyMessage={ + debouncedQuery + ? "No invoices match your search." + : "No invoices yet." + } + error={ + isError + ? { + message: "Failed to load invoices.", + onRetry: () => void refetch(), + } + : undefined + } + pagination={{ + pageIndex: pagination.pageIndex, + pageSize: pagination.pageSize, + pageCount, + totalCount: total, + }} + tableOptions={{ + state: { pagination }, + onPaginationChange: setPagination, + manualPagination: true, + pageCount, + }} + containerClassName="border-0 shadow-none bg-transparent" + footer={DataTableFooter} + /> - - - - navigate(`/dashboard/invoices/${row.id}`)} - emptyMessage={ - debouncedQuery - ? "No invoices match your search." - : "No invoices yet." - } - error={ - isError - ? { - message: "Failed to load invoices.", - onRetry: () => void refetch(), - } - : undefined - } - pagination={{ - pageIndex: pagination.pageIndex, - pageSize: pagination.pageSize, - pageCount, - totalCount: total, - }} - tableOptions={{ - state: { pagination }, - onPaginationChange: setPagination, - manualPagination: true, - pageCount, - }} - containerClassName="border-0 shadow-none bg-transparent" - footer={DataTableFooter} - /> - - - - - + + + ); } diff --git a/apps/edr-freight-web/backoffice/src/pages/invoices/UsdPaymentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/invoices/UsdPaymentsPage.tsx index 585abea21..023b92e0d 100644 --- a/apps/edr-freight-web/backoffice/src/pages/invoices/UsdPaymentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/invoices/UsdPaymentsPage.tsx @@ -25,7 +25,6 @@ import { humanize, } from "@/components/customers"; import { PhasedFileDropzone } from "@/components/contracts/PhasedFileDropzone"; -import { PageContainer, PageHeader } from "@/components/page"; import { useAuth } from "@/auth/useAuth"; import { FREIGHT_PERMS, hasPermission } from "@/lib/permissions"; import { api } from "@/services/api"; @@ -95,7 +94,8 @@ function windowClosed(row: OfflineUsdInvoice): boolean { return Boolean(deadline && new Date(deadline).getTime() <= Date.now()); } -export default function UsdPaymentsPage() { +/** USD Payments tab body of `FinanceHubPage` — page chrome lives in the parent. */ +export default function UsdPaymentsPanel() { const navigate = useNavigate(); const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [query, setQuery] = useState(""); @@ -262,24 +262,7 @@ export default function UsdPaymentsPage() { ); return ( - - void refetch()} - > - - - } - /> - + <> @@ -324,6 +307,16 @@ export default function UsdPaymentsPage() { {total} record{total !== 1 ? "s" : ""} + void refetch()} + > + + @@ -421,6 +414,6 @@ export default function UsdPaymentsPage() { )} - + ); } diff --git a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx index 60044e550..2d98cc805 100644 --- a/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/payments/PaymentsPage.tsx @@ -25,7 +25,7 @@ import { useMemo, useState } from "react"; import { useQuery } from "@tanstack/react-query"; -import { KpiStrip, PageContainer, PageHeader } from "@/components/page"; +import { KpiStrip } from "@/components/page"; import { api } from "@/services/api"; import type { PaymentMethod, PaymentRow } from "@/services/payments.service"; import { @@ -101,7 +101,8 @@ function formatDate(iso: string | null): string { const tableHeader = "text-xs font-semibold uppercase tracking-wide text-muted-foreground"; -export default function PaymentsPage() { +/** Payments tab body of `FinanceHubPage` — page chrome lives in the parent. */ +export default function PaymentsPanel() { const { pagination, setPagination } = usePagination({ pageSize: 10 }); const [query, setQuery] = useState(""); const [statusTab, setStatusTab] = useState("all"); @@ -205,12 +206,7 @@ export default function PaymentsPage() { ]; return ( - - - + - + ); }