refactor(snowball): migrate SnowballPage to React Query (R5.5)
useSnowball + useSnowballSettings + useSnowballActivePlan + useSnowballPlans (+ shared useCategories). The settings-derived form fields (extra payment, ramsey mode, ready flags) are seeded via a settings-synced effect; the many optimistic list/plan edits route through queryClient.setQueryData wrappers; load() is an invalidate wrapper. The debounced live-projection stays a client computation (not page data). Removed the now-dead loadPlans (hooks auto-fetch). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a37697d492
commit
1cb0325560
|
|
@ -110,6 +110,30 @@ export function useSummary(year, month) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useSnowball() {
|
||||||
|
return useQuery({ queryKey: ['snowball'], queryFn: () => api.snowball(), staleTime: 1000 * 60 * 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSnowballSettings() {
|
||||||
|
return useQuery({ queryKey: ['snowball-settings'], queryFn: () => api.snowballSettings(), staleTime: 1000 * 60 * 2 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSnowballActivePlan() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['snowball-active-plan'],
|
||||||
|
queryFn: () => api.snowballActivePlan().catch(() => null),
|
||||||
|
staleTime: 1000 * 60 * 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSnowballPlans() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['snowball-plans'],
|
||||||
|
queryFn: () => api.snowballPlans().then(d => d?.plans ?? []).catch(() => []),
|
||||||
|
staleTime: 1000 * 60 * 2,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function useSubscriptions() {
|
export function useSubscriptions() {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: ['subscriptions'],
|
queryKey: ['subscriptions'],
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
useSnowball, useSnowballSettings, useSnowballActivePlan, useSnowballPlans, useCategories,
|
||||||
|
} from '@/hooks/useQueries';
|
||||||
import { ArrowDown, ArrowUp, GripVertical, TrendingDown, Zap, Save, CalendarCheck, AlertTriangle, PenLine, EyeOff, CheckCircle2, Circle, AlertCircle, RefreshCw } from 'lucide-react';
|
import { ArrowDown, ArrowUp, GripVertical, TrendingDown, Zap, Save, CalendarCheck, AlertTriangle, PenLine, EyeOff, CheckCircle2, Circle, AlertCircle, RefreshCw } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { api } from '@/api';
|
import { api } from '@/api';
|
||||||
|
|
@ -266,10 +270,22 @@ function ReadinessStrip({ items, readyCount, totalCount, allReady, onToggle, dis
|
||||||
|
|
||||||
// ── Page ──────────────────────────────────────────────────────────────────────
|
// ── Page ──────────────────────────────────────────────────────────────────────
|
||||||
export default function SnowballPage() {
|
export default function SnowballPage() {
|
||||||
const [bills, setBills] = useState([]);
|
const queryClient = useQueryClient();
|
||||||
const [categories, setCategories] = useState([]);
|
const { data: bills = [], isPending: billsPending, error: snowballError } = useSnowball();
|
||||||
const [loading, setLoading] = useState(true);
|
const { data: categories = [] } = useCategories();
|
||||||
const [loadError, setLoadError] = useState(null);
|
const { data: settings } = useSnowballSettings();
|
||||||
|
const { data: activePlan = null } = useSnowballActivePlan();
|
||||||
|
const { data: allPlans = [] } = useSnowballPlans();
|
||||||
|
const loading = billsPending;
|
||||||
|
const loadError = snowballError ? (snowballError.message || 'Failed to load snowball data') : null;
|
||||||
|
// Optimistic list/plan edits write through the query cache so existing call
|
||||||
|
// sites (setBills(prev => …), setActivePlan(…), setAllPlans(prev => …)) work.
|
||||||
|
const setBills = useCallback((u) => queryClient.setQueryData(['snowball'],
|
||||||
|
prev => (typeof u === 'function' ? u(prev || []) : u)), [queryClient]);
|
||||||
|
const setActivePlan = useCallback((u) => queryClient.setQueryData(['snowball-active-plan'],
|
||||||
|
prev => (typeof u === 'function' ? u(prev ?? null) : u)), [queryClient]);
|
||||||
|
const setAllPlans = useCallback((u) => queryClient.setQueryData(['snowball-plans'],
|
||||||
|
prev => (typeof u === 'function' ? u(prev || []) : u)), [queryClient]);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [dirty, setDirty] = useState(false);
|
const [dirty, setDirty] = useState(false);
|
||||||
const [editBill, setEditBill] = useState(null);
|
const [editBill, setEditBill] = useState(null);
|
||||||
|
|
@ -288,8 +304,6 @@ export default function SnowballPage() {
|
||||||
|
|
||||||
const [editingBalance, setEditingBalance] = useState({ billId: null, value: '' });
|
const [editingBalance, setEditingBalance] = useState({ billId: null, value: '' });
|
||||||
|
|
||||||
const [activePlan, setActivePlan] = useState(null);
|
|
||||||
const [allPlans, setAllPlans] = useState([]);
|
|
||||||
const [startingPlan, setStartingPlan] = useState(false);
|
const [startingPlan, setStartingPlan] = useState(false);
|
||||||
const [readinessWarnOpen, setReadinessWarnOpen] = useState(false);
|
const [readinessWarnOpen, setReadinessWarnOpen] = useState(false);
|
||||||
const [draggingId, setDraggingId] = useState(null);
|
const [draggingId, setDraggingId] = useState(null);
|
||||||
|
|
@ -311,33 +325,27 @@ export default function SnowballPage() {
|
||||||
} finally { setProjectionLoading(false); }
|
} finally { setProjectionLoading(false); }
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(() => Promise.all([
|
||||||
setLoading(true);
|
queryClient.invalidateQueries({ queryKey: ['snowball'] }),
|
||||||
setLoadError(null);
|
queryClient.invalidateQueries({ queryKey: ['categories'] }),
|
||||||
try {
|
queryClient.invalidateQueries({ queryKey: ['snowball-settings'] }),
|
||||||
const [billsArr, catsArr, settings] = await Promise.all([
|
]), [queryClient]);
|
||||||
api.snowball(), api.categories(), api.snowballSettings(),
|
|
||||||
]);
|
|
||||||
setCategories(catsArr);
|
|
||||||
setBills(billsArr);
|
|
||||||
setDirty(false);
|
|
||||||
const ep = settings.extra_payment > 0 ? String(settings.extra_payment) : '';
|
|
||||||
setRamseyMode(settings.ramsey_mode !== false);
|
|
||||||
setReadyCurrentOnBills(!!settings.ready_current_on_bills);
|
|
||||||
setReadyEmergencyFund(!!settings.ready_emergency_fund);
|
|
||||||
setExtraPayment(ep);
|
|
||||||
extraPaymentRef.current = ep;
|
|
||||||
} catch (err) {
|
|
||||||
setLoadError(err.message || 'Failed to load snowball data');
|
|
||||||
} finally { setLoading(false); }
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const loadPlans = useCallback(() => {
|
// Seed the editable settings form (extra payment, ramsey mode, ready flags)
|
||||||
api.snowballActivePlan().then(p => setActivePlan(p)).catch(() => setActivePlan(null));
|
// from the loaded settings whenever they change — this is what `load` did inline.
|
||||||
api.snowballPlans().then(d => setAllPlans(d?.plans ?? [])).catch(err => console.error('[SnowballPage] failed to load plan history', err));
|
useEffect(() => {
|
||||||
}, []);
|
if (!settings) return;
|
||||||
|
setDirty(false);
|
||||||
|
const ep = settings.extra_payment > 0 ? String(settings.extra_payment) : '';
|
||||||
|
setRamseyMode(settings.ramsey_mode !== false);
|
||||||
|
setReadyCurrentOnBills(!!settings.ready_current_on_bills);
|
||||||
|
setReadyEmergencyFund(!!settings.ready_emergency_fund);
|
||||||
|
setExtraPayment(ep);
|
||||||
|
extraPaymentRef.current = ep;
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
useEffect(() => { Promise.all([load(), loadProjection()]); loadPlans(); }, [load, loadProjection, loadPlans]);
|
// The live projection stays a debounced client computation (not page data).
|
||||||
|
useEffect(() => { loadProjection(); }, [loadProjection]);
|
||||||
|
|
||||||
// ── auto-arrange ──────────────────────────────────────────────────────────
|
// ── auto-arrange ──────────────────────────────────────────────────────────
|
||||||
const handleAutoArrange = () => {
|
const handleAutoArrange = () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue