diff --git a/client/hooks/useQueries.js b/client/hooks/useQueries.js index 8e3818a..a47ba55 100644 --- a/client/hooks/useQueries.js +++ b/client/hooks/useQueries.js @@ -98,6 +98,18 @@ export function useDeletedBills() { }); } +export function useSummary(year, month) { + return useQuery({ + queryKey: ['summary', year, month], + queryFn: () => api.summary(year, month), + staleTime: 1000 * 60 * 2, + placeholderData: keepPreviousData, + // Editable form fields (starting amounts, income) are seeded from this + // result via an effect; don't let a focus refetch reset a mid-edit. + refetchOnWindowFocus: false, + }); +} + export function useSubscriptions() { return useQuery({ queryKey: ['subscriptions'], diff --git a/client/pages/SummaryPage.jsx b/client/pages/SummaryPage.jsx index fa0be77..d1d5e37 100644 --- a/client/pages/SummaryPage.jsx +++ b/client/pages/SummaryPage.jsx @@ -1,4 +1,6 @@ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { useSummary } from '@/hooks/useQueries'; import { toast } from 'sonner'; import { ArrowDown, @@ -235,10 +237,12 @@ function ExpenseRow({ expense, moveControls, dragProps }) { export default function SummaryPage() { const [selected, setSelected] = useState(selectedFromToday); - const [data, setData] = useState(null); - const [loading, setLoading] = useState(true); + const queryClient = useQueryClient(); + const { data = null, isPending: loading, error: queryError } = useSummary(selected.year, selected.month); + const setData = useCallback((u) => queryClient.setQueryData(['summary', selected.year, selected.month], + prev => (typeof u === 'function' ? u(prev) : u)), [queryClient, selected.year, selected.month]); const [saving, setSaving] = useState(false); - const [error, setError] = useState(''); + const error = queryError ? (queryError.message || 'Summary could not be loaded.') : ''; const [startingFirst, setStartingFirst] = useState('0'); const [startingFifteenth, setStartingFifteenth] = useState('0'); const [startingOther, setStartingOther] = useState('0'); @@ -250,37 +254,27 @@ export default function SummaryPage() { const [dropTargetId, setDropTargetId] = useState(null); const [movingBillId, setMovingBillId] = useState(null); - const requestSeq = useRef(0); - const loadSummary = useCallback(async () => { - // Ignore out-of-order responses: only the newest month load updates state. - const seq = ++requestSeq.current; - setLoading(true); - setError(''); - try { - const result = await api.summary(selected.year, selected.month); - if (seq !== requestSeq.current) return; // superseded by a newer request - setData(result); - setStartingFirst(String(result.starting_amounts?.first_amount ?? 0)); - setStartingFifteenth(String(result.starting_amounts?.fifteenth_amount ?? 0)); - setStartingOther(String(result.starting_amounts?.other_amount ?? 0)); - setEditingStarting(false); - setIncomeAmount(String(result.income?.amount ?? 0)); - setIncomeLabel(result.income?.label || 'Salary'); - setEditingIncome(false); - setDraggingId(null); - setDropTargetId(null); - setMovingBillId(null); - } catch (err) { - if (seq === requestSeq.current) setError(err.message || 'Summary could not be loaded.'); - toast.error(err.message || 'Summary could not be loaded.'); - } finally { - if (seq === requestSeq.current) setLoading(false); - } - }, [selected.month, selected.year]); + const loadSummary = useCallback( + () => queryClient.invalidateQueries({ queryKey: ['summary', selected.year, selected.month] }), + [queryClient, selected.month, selected.year], + ); + // Seed the editable form fields (starting amounts, income) from the loaded + // summary and reset transient edit/drag state whenever the data changes — + // this is what loadSummary used to do inline. useEffect(() => { - loadSummary(); - }, [loadSummary]); + if (!data) return; + setStartingFirst(String(data.starting_amounts?.first_amount ?? 0)); + setStartingFifteenth(String(data.starting_amounts?.fifteenth_amount ?? 0)); + setStartingOther(String(data.starting_amounts?.other_amount ?? 0)); + setEditingStarting(false); + setIncomeAmount(String(data.income?.amount ?? 0)); + setIncomeLabel(data.income?.label || 'Salary'); + setEditingIncome(false); + setDraggingId(null); + setDropTargetId(null); + setMovingBillId(null); + }, [data]); const summary = data?.summary || {}; const expenses = data?.expenses || [];