From ea1dae52808076247f2c1085e735db32b3a1071f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Fri, 7 Aug 2026 13:20:22 +0000 Subject: [PATCH] make the invoices dashboard tiles and rows real links --- .../src/apps/Invoices/DashboardView.tsx | 41 ++++++++++--------- .../officerdev/src/apps/Invoices/shared.ts | 8 ++++ 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/src/workspaces/officerdev/src/apps/Invoices/DashboardView.tsx b/src/workspaces/officerdev/src/apps/Invoices/DashboardView.tsx index d5d53c8c..7994b622 100644 --- a/src/workspaces/officerdev/src/apps/Invoices/DashboardView.tsx +++ b/src/workspaces/officerdev/src/apps/Invoices/DashboardView.tsx @@ -1,10 +1,11 @@ import type { Estimate, Invoice } from './shared'; import { useMemo } from 'react'; -import { useNavigate } from 'react-router'; +import { Link } from 'react-router'; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts'; import { ArrowDownRight, ArrowUpRight, CircleDollarSign, FileText, Receipt, Wallet } from 'lucide-react'; import { formatAmount, formatDate, formatMoney, relativeDue, toMajor } from './format'; import { EmptyState, ErrorState, LoadingState, StatusBadge } from './components'; +import { invoicesRecordPath, invoicesSectionPath } from './shared'; import { useSummary } from './useInvoiceShelfData'; // The /invoices landing section — InvoiceShelf's own dashboard, rebuilt. @@ -15,7 +16,6 @@ import { useSummary } from './useInvoiceShelfData'; export const DashboardView = () => { const { dashboard, currency, isLoading, error } = useSummary(); - const navigate = useNavigate(); const chartRows = useMemo(() => { const chart = dashboard?.chart_data; @@ -46,21 +46,21 @@ export const DashboardView = () => { label="Amount due" value={formatMoney(dashboard.total_amount_due, currency)} hint={`${dashboard.total_invoice_count} invoices`} - onClick={() => navigate('/invoices/invoices?status=UNPAID')} + to={`${invoicesSectionPath('invoices')}?status=UNPAID`} /> navigate('/invoices/invoices')} + to={invoicesSectionPath('invoices')} /> navigate('/invoices/payments')} + to={invoicesSectionPath('payments')} /> { label="Net income" value={formatMoney(dashboard.total_net_income, currency)} hint={`${formatMoney(dashboard.total_expenses, currency)} expenses`} - onClick={() => navigate('/invoices/expenses')} + to={invoicesSectionPath('expenses')} /> @@ -143,7 +143,7 @@ export const DashboardView = () => { title="Due invoices" empty="Nothing outstanding." rows={dashboard.recent_due_invoices ?? []} - onOpen={(row) => navigate(`/invoices/invoices?selected=${row.id}`)} + href={(row) => invoicesRecordPath('invoices', row.id)} render={(row: Invoice) => ({ primary: row.invoice_number, secondary: row.customer?.name ?? '—', @@ -156,7 +156,7 @@ export const DashboardView = () => { title="Recent estimates" empty="No estimates yet." rows={dashboard.recent_estimates ?? []} - onOpen={(row) => navigate(`/invoices/estimates?selected=${row.id}`)} + href={(row) => invoicesRecordPath('estimates', row.id)} render={(row: Estimate) => ({ primary: row.estimate_number, secondary: row.customer?.name ?? '—', @@ -192,24 +192,26 @@ const compact = (v: number): string => { return String(v); }; +// Every tile here only ever *goes somewhere* — no tile mutates anything — so they are anchors, not +// buttons. That is what makes the figures middle-clickable into a new tab and copyable as a link, +// which is the whole point of the section reading its filter out of the query string. const MetricCard = ({ icon: Icon, tone, label, value, hint, - onClick, + to, }: { icon: typeof Wallet; tone: string; label: string; value: string; hint?: string; - onClick?: () => void; + to: string; }) => ( - + ); type RecentRender = { primary: string; secondary: string; amount: string; status: string; note: string }; @@ -231,13 +233,13 @@ const RecentPanel = ({ title, rows, render, - onOpen, + href, empty, }: { title: string; rows: T[]; render: (row: T) => RecentRender; - onOpen: (row: T) => void; + href: (row: T) => string; empty: string; }) => (
@@ -249,10 +251,9 @@ const RecentPanel = ({ {rows.map((row) => { const r = render(row); return ( -
{r.amount} - + ); })} diff --git a/src/workspaces/officerdev/src/apps/Invoices/shared.ts b/src/workspaces/officerdev/src/apps/Invoices/shared.ts index 6a383ef0..96a8e598 100644 --- a/src/workspaces/officerdev/src/apps/Invoices/shared.ts +++ b/src/workspaces/officerdev/src/apps/Invoices/shared.ts @@ -34,6 +34,14 @@ export const isInvoicesSection = (value: string | undefined): value is InvoicesS export const invoicesSectionPath = (id: InvoicesSectionId) => `/invoices/${id}`; +/** + * A record open inside a list section — `/invoices/invoices?selected=12`. `useInvoicesSection` reads + * `selected` out of the query string, so this is the addressable form of "this row is open" and the + * thing a row anchor points at. + */ +export const invoicesRecordPath = (section: InvoicesSectionId, id: number) => + `${invoicesSectionPath(section)}?selected=${id}`; + /** The sidecar resource slug behind each list section. `dashboard` and `reports` have none. */ export const SECTION_RESOURCE = { invoices: 'invoices',