From 57abd8c0354c177772656113e534473995da7e5e Mon Sep 17 00:00:00 2001 From: Greg Shear Date: Tue, 16 Jun 2026 13:22:19 -0400 Subject: [PATCH] Show all invoices in the history table, paginated The Recent History table was hard-capped at the 4 most recent invoices within the rolling six-month window (a slice(0, 4) placeholder). Show all of a tenant's invoices instead, paginated four per page (PRD #2). - useBillingInvoices now exposes allInvoices (the full newest-first list) alongside invoices (the windowed+manual subset the usage graphs chart). selectedInvoice resolves against the full list, so any row is clickable. - BillingHistoryTable paginates allInvoices with MUI TablePagination + the shared TablePaginationActions, resetting to the first page when the tenant changes. Rows no longer slices. The usage graphs and graph-state wrapper still read the windowed invoices, so they remain a six-month view. Fetch is capped at 100 invoices; true cursor pagination beyond that is a later step. --- src/components/tables/Billing/Rows.tsx | 3 +- src/components/tables/Billing/index.tsx | 135 ++++++++++++++---------- src/hooks/billing/useBillingInvoices.ts | 24 +++-- 3 files changed, 99 insertions(+), 63 deletions(-) diff --git a/src/components/tables/Billing/Rows.tsx b/src/components/tables/Billing/Rows.tsx index 8d0c2f17f..c913a0ad5 100644 --- a/src/components/tables/Billing/Rows.tsx +++ b/src/components/tables/Billing/Rows.tsx @@ -60,11 +60,10 @@ function Row({ row, isSelected }: RowProps) { ); } -// TODO (billing): Remove pagination placeholder when the new RPC is available. function Rows({ data, selectedInvoice }: RowsProps) { return ( <> - {data.slice(0, 4).map((record, index) => ( + {data.map((record, index) => ( - billingHistory.length > 0 ? ( - - ) : null, - [billingHistory, selectedInvoice] - ); + const { allInvoices, selectedInvoice, isLoading, networkFailed } = + useBillingInvoices(); + + const [page, setPage] = useState(0); + + // The selected tenant lives behind the hook; reset to the first (newest) + // page whenever the invoice set changes so a tenant switch starts at the + // top rather than stranding the view on a now-out-of-range page. + useEffect(() => { + setPage(0); + }, [allInvoices]); + + const pageCount = Math.ceil(allInvoices.length / ROWS_PER_PAGE); + const currentPage = Math.min(page, Math.max(0, pageCount - 1)); + + const dataRows = useMemo(() => { + if (allInvoices.length === 0) { + return null; + } + + const start = currentPage * ROWS_PER_PAGE; + + return ( + + ); + }, [allInvoices, currentPage, selectedInvoice]); return ( - - - - - + +
0 - ? { status: TableStatuses.DATA_FETCHED } - : networkFailed - ? { status: TableStatuses.NETWORK_FAILED } - : { status: TableStatuses.NO_EXISTING_DATA } - } - loading={isLoading} - rows={dataRows} + > + + + 0 + ? { status: TableStatuses.DATA_FETCHED } + : networkFailed + ? { status: TableStatuses.NETWORK_FAILED } + : { status: TableStatuses.NO_EXISTING_DATA } + } + loading={isLoading} + rows={dataRows} + /> +
+
+ + {allInvoices.length > ROWS_PER_PAGE ? ( + setPage(newPage)} + ActionsComponent={TablePaginationActions} /> - - + ) : null} + ); } diff --git a/src/hooks/billing/useBillingInvoices.ts b/src/hooks/billing/useBillingInvoices.ts index 5cb8462d2..7deb92092 100644 --- a/src/hooks/billing/useBillingInvoices.ts +++ b/src/hooks/billing/useBillingInvoices.ts @@ -21,7 +21,12 @@ import { useTenantStore } from 'src/stores/Tenant'; import { invoiceId, stripTimeFromDate } from 'src/utils/billing-utils'; export interface UseBillingInvoicesResult { + // The rolling-window subset (plus manual invoices) used by the usage + // graphs, which only chart the recent period. invoices: Invoice[]; + // Every invoice for the tenant, newest first. Drives the full, paginated + // history table. + allInvoices: Invoice[]; // The invoice currently shown in the line-item/detail views: the stored // selection if it still exists in this tenant's data, otherwise the newest // invoice. Falling back this way means an org switch self-corrects without @@ -111,34 +116,39 @@ export function useBillingInvoices(): UseBillingInvoicesResult { pause: !selectedTenant, }); - const invoices = useMemo(() => { + const allInvoices = useMemo(() => { const nodes = data?.tenant?.billing.invoices.nodes ?? []; return nodes .map((node) => mapInvoice(node, selectedTenant)) - .filter((invoice) => isVisible(invoice, dateWindow)) .sort((a, b) => compareDesc( stripTimeFromDate(a.date_start), stripTimeFromDate(b.date_start) ) ); - }, [data, dateWindow, selectedTenant]); + }, [data, selectedTenant]); + + const invoices = useMemo( + () => allInvoices.filter((invoice) => isVisible(invoice, dateWindow)), + [allInvoices, dateWindow] + ); const selectedInvoice = useMemo(() => { - if (invoices.length === 0) { + if (allInvoices.length === 0) { return null; } return ( - invoices.find( + allInvoices.find( (invoice) => invoiceId(invoice) === selectedInvoiceId - ) ?? invoices[0] + ) ?? allInvoices[0] ); - }, [invoices, selectedInvoiceId]); + }, [allInvoices, selectedInvoiceId]); return { invoices, + allInvoices, selectedInvoice, isLoading: fetching, networkFailed: Boolean(error?.networkError),