import { useReducer, useEffect, useRef, useCallback, useMemo } from "react"; import { useSearchParams } from "react-router-dom"; import type { HighlightsData } from "../shared/types"; import { getHighlights } from "../services/reportService"; import { defaultReferencePeriod } from "../utils/referencePeriod"; interface State { data: HighlightsData | null; windowDays: 30 | 60 | 90; isLoading: boolean; error: string | null; } type Action = | { type: "SET_LOADING"; payload: boolean } | { type: "SET_DATA"; payload: HighlightsData } | { type: "SET_ERROR"; payload: string } | { type: "SET_WINDOW_DAYS"; payload: 30 | 60 | 90 }; const initialState: State = { data: null, windowDays: 30, isLoading: false, error: null, }; function reducer(state: State, action: Action): State { switch (action.type) { case "SET_LOADING": return { ...state, isLoading: action.payload }; case "SET_DATA": return { ...state, data: action.payload, isLoading: false, error: null }; case "SET_ERROR": return { ...state, error: action.payload, isLoading: false }; case "SET_WINDOW_DAYS": return { ...state, windowDays: action.payload }; default: return state; } } /** * Parses `?refY=YYYY&refM=MM` from the search string. Falls back to the * previous-month default when either is missing or invalid. Exposed for * unit tests. */ export function resolveHighlightsReference( rawYear: string | null, rawMonth: string | null, today: Date = new Date(), ): { year: number; month: number } { const y = rawYear !== null ? Number(rawYear) : NaN; const m = rawMonth !== null ? Number(rawMonth) : NaN; if ( Number.isInteger(y) && Number.isInteger(m) && y >= 1970 && y <= 9999 && m >= 1 && m <= 12 ) { return { year: y, month: m }; } return defaultReferencePeriod(today); } export function useHighlights() { const [searchParams, setSearchParams] = useSearchParams(); const rawRefY = searchParams.get("refY"); const rawRefM = searchParams.get("refM"); const { year: referenceYear, month: referenceMonth } = useMemo( () => resolveHighlightsReference(rawRefY, rawRefM), [rawRefY, rawRefM], ); // YTD is always anchored on the current civil year — independent of the // user-picked reference month. const ytdYear = useMemo(() => new Date().getFullYear(), []); const [state, dispatch] = useReducer(reducer, initialState); const fetchIdRef = useRef(0); const fetch = useCallback( async (windowDays: 30 | 60 | 90, year: number, month: number, ytd: number) => { const id = ++fetchIdRef.current; dispatch({ type: "SET_LOADING", payload: true }); try { const data = await getHighlights(year, month, ytd, windowDays); if (id !== fetchIdRef.current) return; dispatch({ type: "SET_DATA", payload: data }); } catch (e) { if (id !== fetchIdRef.current) return; dispatch({ type: "SET_ERROR", payload: e instanceof Error ? e.message : String(e) }); } }, [], ); useEffect(() => { fetch(state.windowDays, referenceYear, referenceMonth, ytdYear); }, [fetch, state.windowDays, referenceYear, referenceMonth, ytdYear]); const setWindowDays = useCallback((d: 30 | 60 | 90) => { dispatch({ type: "SET_WINDOW_DAYS", payload: d }); }, []); const setReferencePeriod = useCallback( (year: number, month: number) => { setSearchParams( (prev) => { const params = new URLSearchParams(prev); params.set("refY", String(year)); params.set("refM", String(month)); return params; }, { replace: true }, ); }, [setSearchParams], ); return { ...state, setWindowDays, year: referenceYear, month: referenceMonth, ytdYear, setReferencePeriod, }; }