import { useCallback } from 'react'; import { Link, useNavigate, useSearchParams } from 'react-router'; import { ConnectionSection } from './ConnectionSection'; import { CustomersListView } from './CustomersListView'; import { DashboardView } from './DashboardView'; import { DocumentEditor } from './DocumentEditor'; import { EstimatesListView } from './EstimatesListView'; import { ExpensesListView } from './ExpensesListView'; import { InvoicesListView } from './InvoicesListView'; import { ItemsListView } from './ItemsListView'; import { PaymentsListView } from './PaymentsListView'; import { RecurringListView } from './RecurringListView'; import { ReportsView } from './ReportsView'; import { CustomerEditor, ExpenseEditor, ItemEditor, PaymentEditor } from './RecordEditors'; import { invoicesNewPaymentPath, invoicesSectionPath } from './shared'; import { useInvoiceShelfHealth } from './useInvoiceShelfData'; import { useInvoicesSection } from './useInvoicesSection'; // Right panel of the /invoices workspace: renders the section named by the URL, and owns the one piece of // state the sections share — which record is being edited. // // That lives in the query string (`?edit=42`, `?edit=new`) rather than in component state, for the same // reason selection does: it is addressable. Back closes the editor instead of leaving the screen, a // half-written form survives a reload of the tab, and "record a payment against invoice 12" is a link // (`?edit=new&pay=12`) rather than a click path. type EditTarget = number | 'new' | null; const parseEdit = (raw: string | null): EditTarget => { if (raw === 'new') return 'new'; const n = Number(raw); return Number.isInteger(n) && n > 0 ? n : null; }; export const InvoicesView = () => { const section = useInvoicesSection(); const { data: health, isLoading: healthLoading } = useInvoiceShelfHealth(); const [params, setParams] = useSearchParams(); const navigate = useNavigate(); const edit = parseEdit(params.get('edit')); const payFor = Number(params.get('pay')) || null; const openEditor = useCallback( (id: number | 'new', extra?: Record) => setParams( (prev) => { const next = new URLSearchParams(prev); next.set('edit', String(id)); for (const [key, value] of Object.entries(extra ?? {})) next.set(key, value); return next; }, { replace: false }, ), [setParams], ); const closeEditor = useCallback( () => setParams( (prev) => { const next = new URLSearchParams(prev); next.delete('edit'); next.delete('pay'); return next; }, { replace: true }, ), [setParams], ); // Health gates every section, and its two failure modes are answered differently: nothing configured yet is // the setup form, whatever section was asked for, because there is nothing else useful to show; a stored // instance that is failing keeps its own message and a way back, since replacing a working token by // accident is worse than a wall of text. if (section === 'settings') return ; if (!healthLoading && health && !health.ok) { if (health.configured === false) return ; return (

InvoiceShelf is not answering

{health.error ?? 'The invoiceshelf sidecar could not reach the configured instance.'}

Check the connection
); } // Documents get a full-height editor rather than a dialog — a line-item table does not fit in one, and // upstream gives them a whole page too. if (edit != null && (section === 'invoices' || section === 'estimates' || section === 'recurring')) { const resource = section === 'recurring' ? 'recurring-invoices' : section; return ; } switch (section) { case 'invoices': return ; case 'estimates': return ( navigate(`/invoices/invoices?selected=${invoiceId}`)} /> ); case 'recurring': return ; case 'payments': return ( <> {edit != null && } ); case 'expenses': return ( <> {edit != null && } ); case 'customers': return ( <> {edit != null && } ); case 'items': return ( <> {edit != null && } ); case 'reports': return ; default: return ; } };