import { useReducer, useCallback, useEffect, useRef } from "react"; import type { DashboardPeriod, DashboardSummary, CategoryBreakdownItem, CategoryOverTimeData, BudgetVsActualRow, } from "../shared/types"; import { getDashboardSummary, getExpensesByCategory, } from "../services/dashboardService"; import { getCategoryOverTime } from "../services/reportService"; import { getBudgetVsActualData } from "../services/budgetService"; import { computeDateRange } from "../utils/dateRange"; interface DashboardState { summary: DashboardSummary; categoryBreakdown: CategoryBreakdownItem[]; categoryOverTime: CategoryOverTimeData; budgetVsActual: BudgetVsActualRow[]; period: DashboardPeriod; budgetYear: number; budgetMonth: number; customDateFrom: string; customDateTo: string; isLoading: boolean; error: string | null; } type DashboardAction = | { type: "SET_LOADING"; payload: boolean } | { type: "SET_ERROR"; payload: string | null } | { type: "SET_DATA"; payload: { summary: DashboardSummary; categoryBreakdown: CategoryBreakdownItem[]; categoryOverTime: CategoryOverTimeData; budgetVsActual: BudgetVsActualRow[]; }; } | { type: "SET_PERIOD"; payload: DashboardPeriod } | { type: "SET_BUDGET_MONTH"; payload: { year: number; month: number } } | { type: "SET_CUSTOM_DATES"; payload: { dateFrom: string; dateTo: string } }; const now = new Date(); const todayStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; const yearStartStr = `${now.getFullYear()}-01-01`; const initialState: DashboardState = { summary: { totalCount: 0, totalAmount: 0, incomeTotal: 0, expenseTotal: 0 }, categoryBreakdown: [], categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {}, types: {}, tree: [] }, budgetVsActual: [], period: "year", budgetYear: now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(), budgetMonth: now.getMonth() === 0 ? 12 : now.getMonth(), customDateFrom: yearStartStr, customDateTo: todayStr, isLoading: false, error: null, }; function reducer(state: DashboardState, action: DashboardAction): DashboardState { switch (action.type) { case "SET_LOADING": return { ...state, isLoading: action.payload }; case "SET_ERROR": return { ...state, error: action.payload, isLoading: false }; case "SET_DATA": return { ...state, summary: action.payload.summary, categoryBreakdown: action.payload.categoryBreakdown, categoryOverTime: action.payload.categoryOverTime, budgetVsActual: action.payload.budgetVsActual, isLoading: false, }; case "SET_PERIOD": return { ...state, period: action.payload }; case "SET_BUDGET_MONTH": return { ...state, budgetYear: action.payload.year, budgetMonth: action.payload.month }; case "SET_CUSTOM_DATES": return { ...state, period: "custom" as DashboardPeriod, customDateFrom: action.payload.dateFrom, customDateTo: action.payload.dateTo }; default: return state; } } export function useDashboard() { const [state, dispatch] = useReducer(reducer, initialState); const fetchIdRef = useRef(0); const fetchData = useCallback(async ( period: DashboardPeriod, customFrom: string | undefined, customTo: string | undefined, bYear: number, bMonth: number, ) => { const fetchId = ++fetchIdRef.current; dispatch({ type: "SET_LOADING", payload: true }); dispatch({ type: "SET_ERROR", payload: null }); try { const { dateFrom, dateTo } = computeDateRange(period, customFrom, customTo); const [summary, categoryBreakdown, categoryOverTime, budgetVsActual] = await Promise.all([ getDashboardSummary(dateFrom, dateTo), getExpensesByCategory(dateFrom, dateTo), getCategoryOverTime(dateFrom, dateTo), getBudgetVsActualData(bYear, bMonth), ]); if (fetchId !== fetchIdRef.current) return; dispatch({ type: "SET_DATA", payload: { summary, categoryBreakdown, categoryOverTime, budgetVsActual } }); } catch (e) { if (fetchId !== fetchIdRef.current) return; dispatch({ type: "SET_ERROR", payload: e instanceof Error ? e.message : String(e), }); } }, []); useEffect(() => { fetchData(state.period, state.customDateFrom, state.customDateTo, state.budgetYear, state.budgetMonth); }, [state.period, state.customDateFrom, state.customDateTo, state.budgetYear, state.budgetMonth, fetchData]); const setPeriod = useCallback((period: DashboardPeriod) => { dispatch({ type: "SET_PERIOD", payload: period }); }, []); const setCustomDates = useCallback((dateFrom: string, dateTo: string) => { dispatch({ type: "SET_CUSTOM_DATES", payload: { dateFrom, dateTo } }); }, []); const setBudgetMonth = useCallback((year: number, month: number) => { dispatch({ type: "SET_BUDGET_MONTH", payload: { year, month } }); }, []); return { state, setPeriod, setCustomDates, setBudgetMonth }; }