import { useCallback, useMemo } from 'react'; import { useParams, useSearchParams } from 'react-router'; import { DEFAULT_INVOICES_SECTION, isInvoicesSection, type InvoicesSectionId } from './shared'; // The URL names the open section and the selected record — not a panel channel. See docs/navigation-audit.md. // InvoicesScreen redirects anything unrecognised, so the fallback here only covers the instant before that // lands. export function useInvoicesSection(): InvoicesSectionId { const { section } = useParams(); return isInvoicesSection(section) ? section : DEFAULT_INVOICES_SECTION; } export type ListFilters = { /** The record whose detail pane is open. `?selected=` because the list stays on screen beside it. */ selected: number | null; search: string; status: string | null; customerId: number | null; fromDate: string | null; toDate: string | null; page: number; isFiltered: boolean; }; /** * Read/write the list query string. * * Every setter drops `page` back to 1 — changing a filter while on page 4 of the old result set otherwise * lands you on a page that may not exist, and upstream answers that with an empty array rather than an * error, which reads as "no results" for a filter that has plenty. * * Selection is `replace: true` so clicking through a list does not bury the previous screen under a dozen * history entries; filter changes push normally, because backing out of a filter is a thing people want. */ export function useListFilters() { const [searchParams, setSearchParams] = useSearchParams(); const filters = useMemo(() => { const num = (key: string) => { const raw = searchParams.get(key); const n = raw == null ? NaN : Number(raw); return Number.isInteger(n) && n > 0 ? n : null; }; const status = searchParams.get('status'); const search = searchParams.get('q') ?? ''; const customerId = num('customer'); const fromDate = searchParams.get('from'); const toDate = searchParams.get('to'); return { selected: num('selected'), search, status, customerId, fromDate, toDate, page: num('page') ?? 1, isFiltered: Boolean(search || status || customerId || fromDate || toDate), }; }, [searchParams]); const patch = useCallback( (changes: Record, opts?: { replace?: boolean; keepPage?: boolean }) => { setSearchParams( (prev) => { const next = new URLSearchParams(prev); for (const [key, value] of Object.entries(changes)) { if (value === null || value === '') next.delete(key); else next.set(key, String(value)); } if (!opts?.keepPage && !('page' in changes)) next.delete('page'); return next; }, { replace: opts?.replace ?? false }, ); }, [setSearchParams], ); const select = useCallback( (id: number | null) => patch({ selected: id }, { replace: true, keepPage: true }), [patch], ); const setPage = useCallback((page: number) => patch({ page: page <= 1 ? null : page }, { keepPage: true }), [patch]); const clearFilters = useCallback( () => patch({ q: null, status: null, customer: null, from: null, to: null }), [patch], ); return { filters, patch, select, setPage, clearFilters }; }