Simpl-Resultat/src/hooks/useCompare.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

147 lines
5 KiB
TypeScript

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