B1: React Query deeper adoption (P1–P3) #86
|
|
@ -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() {
|
export function useSubscriptions() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['subscriptions'],
|
queryKey: ['subscriptions'],
|
||||||
|
|
|
||||||
|
|
@ -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 { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
ArrowDown,
|
ArrowDown,
|
||||||
|
|
@ -235,10 +237,12 @@ function ExpenseRow({ expense, moveControls, dragProps }) {
|
||||||
|
|
||||||
export default function SummaryPage() {
|
export default function SummaryPage() {
|
||||||
const [selected, setSelected] = useState(selectedFromToday);
|
const [selected, setSelected] = useState(selectedFromToday);
|
||||||
const [data, setData] = useState(null);
|
const queryClient = useQueryClient();
|
||||||
const [loading, setLoading] = useState(true);
|
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 [saving, setSaving] = useState(false);
|
||||||
const [error, setError] = useState('');
|
const error = queryError ? (queryError.message || 'Summary could not be loaded.') : '';
|
||||||
const [startingFirst, setStartingFirst] = useState('0');
|
const [startingFirst, setStartingFirst] = useState('0');
|
||||||
const [startingFifteenth, setStartingFifteenth] = useState('0');
|
const [startingFifteenth, setStartingFifteenth] = useState('0');
|
||||||
const [startingOther, setStartingOther] = useState('0');
|
const [startingOther, setStartingOther] = useState('0');
|
||||||
|
|
@ -250,37 +254,27 @@ export default function SummaryPage() {
|
||||||
const [dropTargetId, setDropTargetId] = useState(null);
|
const [dropTargetId, setDropTargetId] = useState(null);
|
||||||
const [movingBillId, setMovingBillId] = useState(null);
|
const [movingBillId, setMovingBillId] = useState(null);
|
||||||
|
|
||||||
const requestSeq = useRef(0);
|
const loadSummary = useCallback(
|
||||||
const loadSummary = useCallback(async () => {
|
() => queryClient.invalidateQueries({ queryKey: ['summary', selected.year, selected.month] }),
|
||||||
// Ignore out-of-order responses: only the newest month load updates state.
|
[queryClient, selected.month, selected.year],
|
||||||
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]);
|
|
||||||
|
|
||||||
|
// 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(() => {
|
useEffect(() => {
|
||||||
loadSummary();
|
if (!data) return;
|
||||||
}, [loadSummary]);
|
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 summary = data?.summary || {};
|
||||||
const expenses = data?.expenses || [];
|
const expenses = data?.expenses || [];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue