import { useReducer, useCallback, useEffect, useRef } from "react"; import type { CategoryDelta } from "../shared/types"; import { getCompareMonthOverMonth, getCompareYearOverYear } from "../services/reportService"; import { useReportsPeriod } from "./useReportsPeriod"; import { defaultReferencePeriod as sharedDefaultReferencePeriod } from "../utils/referencePeriod"; export type CompareMode = "actual" | "budget"; export type CompareSubMode = "mom" | "yoy"; interface State { mode: CompareMode; subMode: CompareSubMode; year: number; month: number; rows: CategoryDelta[]; isLoading: boolean; error: string | null; } type Action = | { type: "SET_MODE"; payload: CompareMode } | { type: "SET_SUB_MODE"; payload: CompareSubMode } | { type: "SET_REFERENCE_PERIOD"; payload: { year: number; month: number } } | { type: "SET_LOADING"; payload: boolean } | { type: "SET_ROWS"; payload: CategoryDelta[] } | { type: "SET_ERROR"; payload: string }; /** * Wrap-around helper: returns (year, month) shifted back by one month. * Example: previousMonth(2026, 1) -> { year: 2025, month: 12 }. */ export function previousMonth(year: number, month: number): { year: number; month: number } { if (month === 1) return { year: year - 1, month: 12 }; return { year, month: month - 1 }; } /** * Default reference period for the Compare report: the month preceding `today`. * Thin wrapper around the shared helper — kept as a named export so existing * imports (and tests) keep working. */ export function defaultReferencePeriod(today: Date = new Date()): { year: number; month: number } { return sharedDefaultReferencePeriod(today); } /** * Returns the comparison meta for a given subMode + reference period. * - MoM: previous month vs current month * - YoY: same month previous year vs current year */ export function comparisonMeta( subMode: CompareSubMode, year: number, month: number, ): { previousYear: number; previousMonth: number } { if (subMode === "mom") { const prev = previousMonth(year, month); return { previousYear: prev.year, previousMonth: prev.month }; } return { previousYear: year - 1, previousMonth: month }; } const defaultRef = defaultReferencePeriod(); const initialState: State = { mode: "actual", subMode: "mom", year: defaultRef.year, month: defaultRef.month, rows: [], isLoading: false, error: null, }; function reducer(state: State, action: Action): State { switch (action.type) { case "SET_MODE": return { ...state, mode: action.payload }; case "SET_SUB_MODE": return { ...state, subMode: action.payload }; case "SET_REFERENCE_PERIOD": return { ...state, year: action.payload.year, month: action.payload.month }; case "SET_LOADING": return { ...state, isLoading: action.payload }; case "SET_ROWS": return { ...state, rows: action.payload, isLoading: false, error: null }; case "SET_ERROR": return { ...state, error: action.payload, isLoading: false }; default: return state; } } export function useCompare() { const { from, to } = useReportsPeriod(); const [state, dispatch] = useReducer(reducer, initialState); const fetchIdRef = useRef(0); const fetch = useCallback( async (mode: CompareMode, subMode: CompareSubMode, year: number, month: number) => { if (mode === "budget") return; // Budget view uses BudgetVsActualTable directly const id = ++fetchIdRef.current; dispatch({ type: "SET_LOADING", payload: true }); try { const rows = subMode === "mom" ? await getCompareMonthOverMonth(year, month) : await getCompareYearOverYear(year, month); if (id !== fetchIdRef.current) return; dispatch({ type: "SET_ROWS", payload: rows }); } catch (e) { if (id !== fetchIdRef.current) return; dispatch({ type: "SET_ERROR", payload: e instanceof Error ? e.message : String(e) }); } }, [], ); useEffect(() => { fetch(state.mode, state.subMode, state.year, state.month); }, [fetch, state.mode, state.subMode, state.year, state.month]); // When the URL period changes, align the reference month with `to`. // The explicit dropdown remains the primary selector — this effect only // keeps the two in sync when the user navigates via PeriodSelector. useEffect(() => { const [y, m] = to.split("-").map(Number); if (!Number.isFinite(y) || !Number.isFinite(m)) return; if (y !== state.year || m !== state.month) { dispatch({ type: "SET_REFERENCE_PERIOD", payload: { year: y, month: m } }); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [to]); const setMode = useCallback((m: CompareMode) => { dispatch({ type: "SET_MODE", payload: m }); }, []); const setSubMode = useCallback((s: CompareSubMode) => { dispatch({ type: "SET_SUB_MODE", payload: s }); }, []); const setReferencePeriod = useCallback((year: number, month: number) => { dispatch({ type: "SET_REFERENCE_PERIOD", payload: { year, month } }); }, []); return { ...state, setMode, setSubMode, setReferencePeriod, from, to }; }