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,
});
}
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 { 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 { toast } from 'sonner';
import { Button } from '@/components/ui/button';
@ -599,12 +601,28 @@ function HistoryVisibilityDialog({ bill, onClose, onSaved }) {
// Bills Page
export default function BillsPage() {
const [searchParams] = useSearchParams();
const [bills, setBills] = useState([]);
const [categories, setCategories] = useState([]);
const [savedTemplates, setSavedTemplates] = useState([]);
const [deletedBills, setDeletedBills] = useState([]);
const queryClient = useQueryClient();
// React Query is the source of truth (reuses the shared ['bills']/['categories']
// caches, so bill mutations here also refresh the Tracker/badge). Optimistic
// 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 [loading, setLoading] = useState(true);
const [showInactive, setShowInactive] = useState(false);
const [search, setSearch] = useState('');
const [draggingId, setDraggingId] = useState(null);
@ -641,26 +659,6 @@ export default function BillsPage() {
localStorage.setItem(BILLS_SORT_KEY, 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(() => {
const querySearch = searchParams.get('search') || '';
@ -745,7 +743,7 @@ export default function BillsPage() {
if (bill.active && reason) payload.inactive_reason = reason;
await api.updateBill(bill.id, payload);
toast.success(bill.active ? 'Bill deactivated' : 'Bill activated');
load();
refresh();
} catch (err) {
toast.error(err.message);
}
@ -771,7 +769,7 @@ export default function BillsPage() {
try {
await api.restoreBill(bill.id);
toast.success(`"${bill.name}" restored`);
load();
refresh();
} catch (err) {
toast.error(err.message || 'Failed to restore bill');
}
@ -780,7 +778,7 @@ export default function BillsPage() {
});
setDeleteTarget(null);
setDeleteConfirmed(false);
load();
refresh();
} catch (err) {
toast.error(err.message || 'Failed to delete bill');
} finally {
@ -792,7 +790,7 @@ export default function BillsPage() {
try {
await api.restoreBill(bill.id);
toast.success(`"${bill.name}" restored`);
await load();
await refresh();
} catch (err) {
toast.error(err.message || 'Failed to restore bill');
}
@ -850,10 +848,10 @@ export default function BillsPage() {
try {
await api.reorderBills(reorderPayload(nextBills));
toast.success('Bill order saved');
load();
refresh();
} catch (err) {
toast.error(err.message || 'Failed to save bill order');
load();
refresh();
} finally {
setMovingBillId(null);
}
@ -1155,7 +1153,7 @@ export default function BillsPage() {
initialBill={modal.initialBill}
categories={categories}
onClose={() => setModal(null)}
onSave={load}
onSave={refresh}
onDuplicate={handleDuplicateBill}
/>
)}
@ -1164,7 +1162,7 @@ export default function BillsPage() {
<HistoryVisibilityDialog
bill={historyTarget}
onClose={() => setHistoryTarget(null)}
onSaved={load}
onSaved={refresh}
/>
)}