mirror of
https://github.com/escalante29/WealthySmart.git
synced 2026-07-17 10:08:46 +02:00
QueryClientProvider (30s staleTime, 1 retry) + Sonner Toaster at the root. api.ts gains AbortSignal support and a default 30s timeout (FE-24); query cancellation kills the stale-response races (FE-01, FE-03). useBudget is now queries + mutations with targeted ['budget'] invalidation and success/error toasts on every mutation (ARCH-13, UX-01); month navigation keeps the previous month visible while fetching. Budget's transaction list is a cancellable query with placeholder data; deferred-toggle gets feedback (UX-05); both panels render a shared ErrorState with retry instead of failing silently. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
104 lines
3.1 KiB
TypeScript
104 lines
3.1 KiB
TypeScript
import { useState } from 'react';
|
|
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
import { toast } from 'sonner';
|
|
import {
|
|
type RecurringItemCreate,
|
|
type RecurringItemUpdate,
|
|
getYearlyProjection,
|
|
getMonthlyDetail,
|
|
getRecurringItems,
|
|
createRecurringItem,
|
|
updateRecurringItem as apiUpdateItem,
|
|
deleteRecurringItem as apiDeleteItem,
|
|
upsertBalanceOverride,
|
|
deleteBalanceOverride,
|
|
} from '@/lib/api';
|
|
|
|
/** All budget data hangs off the ['budget', ...] key family; every mutation
|
|
* invalidates the family so projection, month detail and items stay in sync. */
|
|
export function useBudget(initialYear: number) {
|
|
const queryClient = useQueryClient();
|
|
const [year, setYear] = useState(initialYear);
|
|
const [selectedMonth, setSelectedMonth] = useState<number>(
|
|
new Date().getMonth() + 1,
|
|
);
|
|
|
|
const projectionQ = useQuery({
|
|
queryKey: ['budget', 'projection', year],
|
|
queryFn: () => getYearlyProjection(year).then((r) => r.data),
|
|
});
|
|
|
|
const monthDetailQ = useQuery({
|
|
queryKey: ['budget', 'month', year, selectedMonth],
|
|
queryFn: () => getMonthlyDetail(year, selectedMonth).then((r) => r.data),
|
|
placeholderData: (prev) => prev, // keep last month visible while navigating
|
|
});
|
|
|
|
const recurringQ = useQuery({
|
|
queryKey: ['budget', 'recurring'],
|
|
queryFn: () => getRecurringItems().then((r) => r.data),
|
|
});
|
|
|
|
const invalidate = () =>
|
|
queryClient.invalidateQueries({ queryKey: ['budget'] });
|
|
|
|
async function mutateWithToast(
|
|
action: () => Promise<unknown>,
|
|
success: string,
|
|
failure: string,
|
|
) {
|
|
try {
|
|
await action();
|
|
} catch (err) {
|
|
toast.error(failure);
|
|
throw err;
|
|
}
|
|
toast.success(success);
|
|
await invalidate();
|
|
}
|
|
|
|
return {
|
|
year,
|
|
setYear,
|
|
selectedMonth,
|
|
setSelectedMonth,
|
|
projection: projectionQ.data ?? null,
|
|
monthDetail: monthDetailQ.data ?? null,
|
|
recurringItems: recurringQ.data ?? [],
|
|
loading: projectionQ.isPending,
|
|
monthLoading: monthDetailQ.isFetching,
|
|
error: projectionQ.error ?? monthDetailQ.error ?? recurringQ.error ?? null,
|
|
addItem: (data: RecurringItemCreate) =>
|
|
mutateWithToast(
|
|
() => createRecurringItem(data),
|
|
'Item recurrente creado',
|
|
'No se pudo crear el item',
|
|
),
|
|
updateItem: (id: number, data: RecurringItemUpdate) =>
|
|
mutateWithToast(
|
|
() => apiUpdateItem(id, data),
|
|
'Item recurrente actualizado',
|
|
'No se pudo actualizar el item',
|
|
),
|
|
deleteItem: (id: number) =>
|
|
mutateWithToast(
|
|
() => apiDeleteItem(id),
|
|
'Item recurrente eliminado',
|
|
'No se pudo eliminar el item',
|
|
),
|
|
saveBalanceOverride: (overrideYear: number, month: number, value: number) =>
|
|
mutateWithToast(
|
|
() => upsertBalanceOverride(overrideYear, month, value),
|
|
'Balance ajustado',
|
|
'No se pudo ajustar el balance',
|
|
),
|
|
clearBalanceOverride: (overrideYear: number, month: number) =>
|
|
mutateWithToast(
|
|
() => deleteBalanceOverride(overrideYear, month),
|
|
'Ajuste de balance eliminado',
|
|
'No se pudo eliminar el ajuste',
|
|
),
|
|
refresh: invalidate,
|
|
};
|
|
}
|