Simpl-Resultat/src/hooks/useHighlights.ts
le king fu 8b90cb6489
All checks were successful
PR Check / rust (push) Successful in 21m14s
PR Check / frontend (push) Successful in 2m16s
PR Check / rust (pull_request) Successful in 21m31s
PR Check / frontend (pull_request) Successful in 2m14s
feat(reports/highlights): default reference month to previous month + YTD current year, user-changeable (#106)
- 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
2026-04-19 08:28:30 -04:00

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,
};
}