BillTracker/client/hooks/useQueries.ts

241 lines
7.5 KiB
TypeScript

import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query';
import { useCallback } from 'react';
import { api, type QueryParams } from '@/api';
// Custom hook for fetching tracker data
export function useTracker(year: number, month: number) {
return useQuery({
queryKey: ['tracker', year, month],
queryFn: () => api.tracker(year, month),
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 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
gcTime: 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
gcTime: 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]);
}
// Prefetch a tracker month into the cache (e.g. on month-nav hover) so switching
// to it is instant. No-op if it's already cached and fresh.
export function usePrefetchTracker() {
const queryClient = useQueryClient();
return useCallback((year: number, month: number) => {
queryClient.prefetchQuery({
queryKey: ['tracker', year, month],
queryFn: () => api.tracker(year, month),
staleTime: 1000 * 60 * 5,
});
}, [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?: QueryParams) {
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: number, month: number) {
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,
});
}
interface BankLedgerArgs {
accountId: string;
flow: string;
page: number;
query: string;
sortBy: string;
sortDir: string;
pageSize: number;
}
export function useBankLedger({ accountId, flow, page, query, sortBy, sortDir, pageSize }: BankLedgerArgs) {
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: number, month: number) {
return useQuery({
queryKey: ['spending-summary', year, month],
queryFn: () => api.spendingSummary({ year, month }),
staleTime: 1000 * 60 * 2,
placeholderData: keepPreviousData,
});
}
interface SpendingTransactionsArgs {
year: number;
month: number;
activeCat?: number | string | null;
page: number;
}
export function useSpendingTransactions({ year, month, activeCat, page }: SpendingTransactionsArgs) {
return useQuery({
queryKey: ['spending-transactions', year, month, activeCat ?? 'all', page],
queryFn: () => {
const params: QueryParams = { 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 cats = await api.categories();
return cats.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: any) => 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,
});
}