import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query'; import { useCallback } from 'react'; import { api } from '@/api'; // Custom hook for fetching tracker data export function useTracker(year, month) { return useQuery({ queryKey: ['tracker', year, month], queryFn: () => api.tracker(year, month), staleTime: 1000 * 60 * 5, // 5 minutes cacheTime: 1000 * 60 * 30, // 30 minutes }); } // Custom hook for fetching all bills export function useBills() { return useQuery({ queryKey: ['bills'], queryFn: () => api.allBills(), staleTime: 1000 * 60 * 5, // 5 minutes cacheTime: 1000 * 60 * 30, // 30 minutes }); } // Custom hook for fetching categories export function useCategories() { return useQuery({ queryKey: ['categories'], queryFn: () => api.categories(), staleTime: 1000 * 60 * 60, // 1 hour cacheTime: 1000 * 60 * 60 * 2, // 2 hours }); } // Lightweight overdue count for sidebar badge — polls every 5 minutes export function useOverdueCount() { return useQuery({ queryKey: ['overdue-count'], queryFn: () => api.overdueCount(), staleTime: 1000 * 60 * 2, // 2 minutes refetchInterval: 1000 * 60 * 5, // poll every 5 minutes refetchIntervalInBackground: false, // only when tab is active }); } // Drift / price-change report — refreshed on demand, not auto-polled export function useDriftReport() { return useQuery({ queryKey: ['drift-report'], queryFn: () => api.driftReport(), staleTime: 1000 * 60 * 10, refetchOnWindowFocus: false, }); } // A single invalidation for every query a bill mutation can affect. Previously a // row action only refetch()'d the one tracker query passed down as `refresh`, so // the sidebar overdue badge (['overdue-count'], 2-min staleTime), the drift // report and the bills list stayed stale after paying/skipping/editing a bill — // e.g. clearing your last overdue bill still showed "3" on the badge for minutes. // Hand this to rows / BillModal.onSave / payment + sync handlers so the whole // shell updates live. Returns a stable callback safe to use as an effect dep. export function useInvalidateTrackerData() { const queryClient = useQueryClient(); return useCallback(() => { for (const key of [['tracker'], ['overdue-count'], ['drift-report'], ['bills']]) { queryClient.invalidateQueries({ queryKey: key }); } }, [queryClient]); } // ── Page data (migrated off manual useEffect + load()) ─────────────────────── // The queryKey encodes the params, so React Query handles caching, request // dedup, cancellation, and out-of-order responses — no manual sequence guards. // keepPreviousData keeps the last result visible while a new month/filter loads. export function useAnalyticsSummary(params) { return useQuery({ queryKey: ['analytics-summary', params], queryFn: () => api.analyticsSummary(params), staleTime: 1000 * 60 * 2, 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, }); } 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 useBankLedger({ accountId, flow, page, query, sortBy, sortDir, pageSize }) { return useQuery({ queryKey: ['bank-ledger', accountId, flow, page, query, sortBy, sortDir], queryFn: () => api.bankTransactionsLedger({ limit: pageSize, offset: (page - 1) * pageSize, q: query, account_id: accountId === 'all' ? undefined : accountId, flow, sort_by: sortBy, sort_dir: sortDir, }), staleTime: 1000 * 60, placeholderData: keepPreviousData, }); } export function useSpendingSummary(year, month) { return useQuery({ queryKey: ['spending-summary', year, month], queryFn: () => api.spendingSummary({ year, month }), staleTime: 1000 * 60 * 2, placeholderData: keepPreviousData, }); } export function useSpendingTransactions({ year, month, activeCat, page }) { return useQuery({ queryKey: ['spending-transactions', year, month, activeCat ?? 'all', page], queryFn: () => { const params = { year, month, page, limit: 50 }; if (activeCat === null) params.category_id = 'null'; else if (activeCat !== undefined) params.category_id = activeCat; return api.spendingTransactions(params); }, staleTime: 1000 * 60 * 2, placeholderData: keepPreviousData, }); } export function useSpendingCategories() { return useQuery({ queryKey: ['spending-categories'], queryFn: async () => { const d = await api.categories(); return (d.categories || d || []).filter(c => !c.deleted_at && c.spending_enabled); }, staleTime: 1000 * 60 * 5, }); } export function useCategoryGroups() { return useQuery({ queryKey: ['category-groups'], queryFn: () => api.categoryGroups().then(g => g || []).catch(() => []), staleTime: 1000 * 60 * 5, }); } 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'], queryFn: () => api.subscriptions(), staleTime: 1000 * 60 * 5, }); } export function useSubscriptionRecommendations() { return useQuery({ queryKey: ['subscription-recommendations'], queryFn: () => api.subscriptionRecommendations().then(r => r.recommendations || []), staleTime: 1000 * 60 * 5, }); }