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:
parent
bb024ce161
commit
9cb254ea13
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue