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() {
|
||||
return useQuery({
|
||||
queryKey: ['subscriptions'],
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
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 { toast } from 'sonner';
|
||||
import { api } from '@/api';
|
||||
|
|
@ -266,10 +270,22 @@ function ReadinessStrip({ items, readyCount, totalCount, allReady, onToggle, dis
|
|||
|
||||
// ── Page ──────────────────────────────────────────────────────────────────────
|
||||
export default function SnowballPage() {
|
||||
const [bills, setBills] = useState([]);
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [loadError, setLoadError] = useState(null);
|
||||
const queryClient = useQueryClient();
|
||||
const { data: bills = [], isPending: billsPending, error: snowballError } = useSnowball();
|
||||
const { data: categories = [] } = useCategories();
|
||||
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 [dirty, setDirty] = useState(false);
|
||||
const [editBill, setEditBill] = useState(null);
|
||||
|
|
@ -288,8 +304,6 @@ export default function SnowballPage() {
|
|||
|
||||
const [editingBalance, setEditingBalance] = useState({ billId: null, value: '' });
|
||||
|
||||
const [activePlan, setActivePlan] = useState(null);
|
||||
const [allPlans, setAllPlans] = useState([]);
|
||||
const [startingPlan, setStartingPlan] = useState(false);
|
||||
const [readinessWarnOpen, setReadinessWarnOpen] = useState(false);
|
||||
const [draggingId, setDraggingId] = useState(null);
|
||||
|
|
@ -311,33 +325,27 @@ export default function SnowballPage() {
|
|||
} finally { setProjectionLoading(false); }
|
||||
}, []);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setLoadError(null);
|
||||
try {
|
||||
const [billsArr, catsArr, settings] = await Promise.all([
|
||||
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 load = useCallback(() => Promise.all([
|
||||
queryClient.invalidateQueries({ queryKey: ['snowball'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['categories'] }),
|
||||
queryClient.invalidateQueries({ queryKey: ['snowball-settings'] }),
|
||||
]), [queryClient]);
|
||||
|
||||
const loadPlans = useCallback(() => {
|
||||
api.snowballActivePlan().then(p => setActivePlan(p)).catch(() => setActivePlan(null));
|
||||
api.snowballPlans().then(d => setAllPlans(d?.plans ?? [])).catch(err => console.error('[SnowballPage] failed to load plan history', err));
|
||||
}, []);
|
||||
// Seed the editable settings form (extra payment, ramsey mode, ready flags)
|
||||
// from the loaded settings whenever they change — this is what `load` did inline.
|
||||
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 ──────────────────────────────────────────────────────────
|
||||
const handleAutoArrange = () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue