refactor(bills): migrate BillsPage to React Query (R5.2)

Reuses the shared useBills/useCategories caches (+ new useBillTemplates/
useDeletedBills), so bill mutations here now also refresh the Tracker/badge
live via the shared ['bills'] key. Optimistic list edits (delete, reorder)
write through queryClient.setQueryData; post-mutation load() calls became a
refresh() that invalidates the 4 page queries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
null 2026-07-03 20:08:28 -05:00
parent bb024ce161
commit 9cb254ea13
2 changed files with 47 additions and 33 deletions

View File

@ -81,3 +81,19 @@ export function useAnalyticsSummary(params) {
placeholderData: keepPreviousData, placeholderData: keepPreviousData,
}); });
} }
export function useBillTemplates() {
return useQuery({
queryKey: ['bill-templates'],
queryFn: () => api.billTemplates(),
staleTime: 1000 * 60 * 5,
});
}
export function useDeletedBills() {
return useQuery({
queryKey: ['deleted-bills'],
queryFn: () => api.deletedBills().catch(() => []), // non-critical: never block the page
staleTime: 1000 * 60 * 5,
});
}

View File

@ -1,5 +1,7 @@
import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react'; import React, { useEffect, useState, useCallback, useMemo, useRef } from 'react';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { useBills, useCategories, useBillTemplates, useDeletedBills } from '@/hooks/useQueries';
import { Plus, ChevronRight, SlidersHorizontal, Search, Trash2 } from 'lucide-react'; import { Plus, ChevronRight, SlidersHorizontal, Search, Trash2 } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -599,12 +601,28 @@ function HistoryVisibilityDialog({ bill, onClose, onSaved }) {
// Bills Page // Bills Page
export default function BillsPage() { export default function BillsPage() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [bills, setBills] = useState([]); const queryClient = useQueryClient();
const [categories, setCategories] = useState([]); // React Query is the source of truth (reuses the shared ['bills']/['categories']
const [savedTemplates, setSavedTemplates] = useState([]); // caches, so bill mutations here also refresh the Tracker/badge). Optimistic
const [deletedBills, setDeletedBills] = useState([]); // list edits below write to the cache via setQueryData.
const { data: bills = [], isPending: billsPending } = useBills();
const { data: categories = [] } = useCategories();
const { data: savedTemplates = [] } = useBillTemplates();
const { data: deletedBills = [] } = useDeletedBills();
const loading = billsPending;
const setBills = useCallback(
(updater) => queryClient.setQueryData(['bills'], prev => (
typeof updater === 'function' ? updater(prev || []) : updater
)),
[queryClient],
);
const refresh = useCallback(() => Promise.all([
queryClient.invalidateQueries({ queryKey: ['bills'] }),
queryClient.invalidateQueries({ queryKey: ['categories'] }),
queryClient.invalidateQueries({ queryKey: ['bill-templates'] }),
queryClient.invalidateQueries({ queryKey: ['deleted-bills'] }),
]), [queryClient]);
const [showDeleted, setShowDeleted] = useState(false); const [showDeleted, setShowDeleted] = useState(false);
const [loading, setLoading] = useState(true);
const [showInactive, setShowInactive] = useState(false); const [showInactive, setShowInactive] = useState(false);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const [draggingId, setDraggingId] = useState(null); const [draggingId, setDraggingId] = useState(null);
@ -641,26 +659,6 @@ export default function BillsPage() {
localStorage.setItem(BILLS_SORT_KEY, billsSort); localStorage.setItem(BILLS_SORT_KEY, billsSort);
}, [billsSort]); }, [billsSort]);
const load = useCallback(async () => {
try {
const [billsRes, catRes, templateRes, deletedRes] = await Promise.all([
api.allBills(),
api.categories(),
api.billTemplates(),
api.deletedBills().catch(() => []), // non-critical: never block the page
]);
setBills(billsRes || []);
setCategories(catRes || []);
setSavedTemplates(templateRes || []);
setDeletedBills(deletedRes || []);
} catch (err) {
toast.error(err.message);
} finally {
setLoading(false);
}
}, []);
useEffect(() => { load(); }, [load]);
useEffect(() => { useEffect(() => {
const querySearch = searchParams.get('search') || ''; const querySearch = searchParams.get('search') || '';
@ -745,7 +743,7 @@ export default function BillsPage() {
if (bill.active && reason) payload.inactive_reason = reason; if (bill.active && reason) payload.inactive_reason = reason;
await api.updateBill(bill.id, payload); await api.updateBill(bill.id, payload);
toast.success(bill.active ? 'Bill deactivated' : 'Bill activated'); toast.success(bill.active ? 'Bill deactivated' : 'Bill activated');
load(); refresh();
} catch (err) { } catch (err) {
toast.error(err.message); toast.error(err.message);
} }
@ -771,7 +769,7 @@ export default function BillsPage() {
try { try {
await api.restoreBill(bill.id); await api.restoreBill(bill.id);
toast.success(`"${bill.name}" restored`); toast.success(`"${bill.name}" restored`);
load(); refresh();
} catch (err) { } catch (err) {
toast.error(err.message || 'Failed to restore bill'); toast.error(err.message || 'Failed to restore bill');
} }
@ -780,7 +778,7 @@ export default function BillsPage() {
}); });
setDeleteTarget(null); setDeleteTarget(null);
setDeleteConfirmed(false); setDeleteConfirmed(false);
load(); refresh();
} catch (err) { } catch (err) {
toast.error(err.message || 'Failed to delete bill'); toast.error(err.message || 'Failed to delete bill');
} finally { } finally {
@ -792,7 +790,7 @@ export default function BillsPage() {
try { try {
await api.restoreBill(bill.id); await api.restoreBill(bill.id);
toast.success(`"${bill.name}" restored`); toast.success(`"${bill.name}" restored`);
await load(); await refresh();
} catch (err) { } catch (err) {
toast.error(err.message || 'Failed to restore bill'); toast.error(err.message || 'Failed to restore bill');
} }
@ -850,10 +848,10 @@ export default function BillsPage() {
try { try {
await api.reorderBills(reorderPayload(nextBills)); await api.reorderBills(reorderPayload(nextBills));
toast.success('Bill order saved'); toast.success('Bill order saved');
load(); refresh();
} catch (err) { } catch (err) {
toast.error(err.message || 'Failed to save bill order'); toast.error(err.message || 'Failed to save bill order');
load(); refresh();
} finally { } finally {
setMovingBillId(null); setMovingBillId(null);
} }
@ -1155,7 +1153,7 @@ export default function BillsPage() {
initialBill={modal.initialBill} initialBill={modal.initialBill}
categories={categories} categories={categories}
onClose={() => setModal(null)} onClose={() => setModal(null)}
onSave={load} onSave={refresh}
onDuplicate={handleDuplicateBill} onDuplicate={handleDuplicateBill}
/> />
)} )}
@ -1164,7 +1162,7 @@ export default function BillsPage() {
<HistoryVisibilityDialog <HistoryVisibilityDialog
bill={historyTarget} bill={historyTarget}
onClose={() => setHistoryTarget(null)} onClose={() => setHistoryTarget(null)}
onSaved={load} onSaved={refresh}
/> />
)} )}