- Extract shared defaultReferencePeriod helper (src/utils/referencePeriod.ts) - useHighlights now reads ?refY=YYYY&refM=MM, defaults to previous month - getHighlights signature: (referenceYear, referenceMonth, ytdYear, windowDays, ...) - YTD tile pinned to Jan 1 of current civil year, independent of reference month - CompareReferenceMonthPicker surfaced on /reports/highlights - Hub highlights panel inherits the same default via useHighlights - useCartes and useCompare now delegate their default-period helpers to the shared util
130 lines
3.8 KiB
TypeScript
130 lines
3.8 KiB
TypeScript
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,
|
|
};
|
|
}
|