diff --git a/src/workspaces/officerdev/src/apps/Invoices/CustomersListView.tsx b/src/workspaces/officerdev/src/apps/Invoices/CustomersListView.tsx index d00dd59c..36f79f46 100644 --- a/src/workspaces/officerdev/src/apps/Invoices/CustomersListView.tsx +++ b/src/workspaces/officerdev/src/apps/Invoices/CustomersListView.tsx @@ -2,7 +2,7 @@ import type { Column } from './components'; import type { ConfirmState } from './ConfirmDialog'; import type { Address, Customer } from './shared'; import { useState } from 'react'; -import { useNavigate } from 'react-router'; +import { Link } from 'react-router'; import { Filter, MoreHorizontal, Plus, Trash2, Users, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Checkbox } from '@/components/ui/checkbox'; @@ -17,7 +17,7 @@ import { import { EmptyState, ErrorState, Field, Pagination, SearchBox, SectionShell, SimpleTable } from './components'; import { ConfirmDialog } from './ConfirmDialog'; import { formatDate, formatMoney } from './format'; -import { PAGE_SIZE } from './shared'; +import { invoicesCustomerPath, PAGE_SIZE } from './shared'; import { useListFilters } from './useInvoicesSection'; import { useCurrency, useCustomerStats, useResourceList, useResourceMutations } from './useInvoiceShelfData'; @@ -30,7 +30,6 @@ type Props = { onEdit: (id: number | 'new') => void }; export const CustomersListView = ({ onEdit }: Props) => { const { filters, patch, select, setPage, clearFilters } = useListFilters(); const currency = useCurrency(); - const navigate = useNavigate(); const [showFilters, setShowFilters] = useState(false); const [selection, setSelection] = useState>(new Set()); const [confirm, setConfirm] = useState(null); @@ -120,11 +119,14 @@ export const CustomersListView = ({ onEdit }: Props) => { onEdit(row.id)}>Edit select(row.id)}>View - navigate(`/invoices/invoices?customer=${row.id}`)}> - Their invoices + {/* Both go somewhere and change nothing, so they are anchors wearing a menu item — `asChild` hands + Radix's item semantics to the `` rather than nesting one inside the other. Middle-click + opens the filtered list in a tab, which is the point of the filter living in the URL. */} + + Their invoices - navigate(`/invoices/payments?customer=${row.id}`)}> - Their payments + + Their payments void; onRecordPayment: (invoiceId: number) => void }; +type Props = { onEdit: (id: number | 'new') => void; recordPaymentHref: (invoiceId: number) => string }; -export const InvoicesListView = ({ onEdit, onRecordPayment }: Props) => { +export const InvoicesListView = ({ onEdit, recordPaymentHref }: Props) => { const { filters, patch, select, setPage, clearFilters } = useListFilters(); const currency = useCurrency(); const { customers } = useLookups(); @@ -189,10 +190,15 @@ export const InvoicesListView = ({ onEdit, onRecordPayment }: Props) => { Resend invoice )} + {/* Opening the payment editor is an address (`?edit=new&pay=`), not a side effect — the editor + prefills itself from those params — so it is an anchor, not a callback up to the parent and + back down through `navigate`. */} {(row.status === 'SENT' || row.status === 'VIEWED') && ( - onRecordPayment(row.id)}> - - Record payment + + + + Record payment + )} {row.status === 'DRAFT' && ( diff --git a/src/workspaces/officerdev/src/apps/Invoices/InvoicesView.tsx b/src/workspaces/officerdev/src/apps/Invoices/InvoicesView.tsx index 23fc7c74..24e5bf96 100644 --- a/src/workspaces/officerdev/src/apps/Invoices/InvoicesView.tsx +++ b/src/workspaces/officerdev/src/apps/Invoices/InvoicesView.tsx @@ -12,7 +12,7 @@ import { PaymentsListView } from './PaymentsListView'; import { RecurringListView } from './RecurringListView'; import { ReportsView } from './ReportsView'; import { CustomerEditor, ExpenseEditor, ItemEditor, PaymentEditor } from './RecordEditors'; -import { invoicesSectionPath } from './shared'; +import { invoicesNewPaymentPath, invoicesSectionPath } from './shared'; import { useInvoiceShelfHealth } from './useInvoiceShelfData'; import { useInvoicesSection } from './useInvoicesSection'; @@ -100,12 +100,7 @@ export const InvoicesView = () => { switch (section) { case 'invoices': - return ( - navigate(`/invoices/payments?edit=new&pay=${invoiceId}`)} - /> - ); + return ; case 'estimates': return ( diff --git a/src/workspaces/officerdev/src/apps/Invoices/shared.ts b/src/workspaces/officerdev/src/apps/Invoices/shared.ts index 96a8e598..0427cd87 100644 --- a/src/workspaces/officerdev/src/apps/Invoices/shared.ts +++ b/src/workspaces/officerdev/src/apps/Invoices/shared.ts @@ -42,6 +42,18 @@ export const invoicesSectionPath = (id: InvoicesSectionId) => `/invoices/${id}`; export const invoicesRecordPath = (section: InvoicesSectionId, id: number) => `${invoicesSectionPath(section)}?selected=${id}`; +/** A list section filtered to one customer — `useListFilters` reads `customer`. */ +export const invoicesCustomerPath = (section: InvoicesSectionId, customerId: number) => + `${invoicesSectionPath(section)}?customer=${customerId}`; + +/** + * The payment editor, opened blank against an invoice — `/invoices/payments?edit=new&pay=12`. + * `InvoicesView` reads both params (`edit` → which editor, `pay` → which invoice it prefills from), so + * "record a payment for this invoice" is a whole address and not a callback. + */ +export const invoicesNewPaymentPath = (invoiceId: number) => + `${invoicesSectionPath('payments')}?edit=new&pay=${invoiceId}`; + /** The sidecar resource slug behind each list section. `dashboard` and `reports` have none. */ export const SECTION_RESOURCE = { invoices: 'invoices',