Simpl-Resultat/src/hooks/useCartes.ts
le king fu 3be05db41a
All checks were successful
PR Check / rust (push) Successful in 21m48s
PR Check / frontend (push) Successful in 2m15s
PR Check / rust (pull_request) Successful in 21m44s
PR Check / frontend (pull_request) Successful in 2m16s
feat(reports/cartes): Mensuel/YTD toggle on KPI cards + user guide section (#102)
Adds a segmented Monthly/YTD toggle next to the reference-month picker that
flips the four KPI cards (income, expenses, net, savings rate) between the
reference-month value (unchanged default) and a Year-to-Date cumulative view.

In YTD mode, the "current" value sums January to the reference month of the
reference year; MoM delta compares it to Jan to (refMonth - 1) of the same
year (null in January, since no prior YTD window exists); YoY delta compares
it to Jan to refMonth of the previous year; savings rate is recomputed from
YTD income and expenses, and stays null when YTD income is zero.

The 13-month sparkline, top movers, seasonality and budget adherence cards
remain monthly regardless of the toggle (by design). The savings-rate tooltip
is now dynamic and mirrors the active mode. The mode is persisted in
localStorage under `reports-cartes-period-mode`.

Also adds a dedicated Cartes section to `docs/guide-utilisateur.md` covering
the four KPI formulas, the Monthly/YTD toggle and its effect on deltas, the
sparkline, top movers, seasonality, budget adherence and the savings-rate
edge case. Mirrored in the in-app `docs.reports` i18n tree (features/steps/
tips extended) for both FR and EN.

No SQL migration: YTD sums are computed from the already-fetched
`flowByMonth` map, so no extra round trip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 09:49:21 -04:00

110 lines
3.5 KiB
TypeScript

import { useReducer, useCallback, useEffect, useRef } from "react";
import type { CartesSnapshot, CartesKpiPeriodMode } from "../shared/types";
import { getCartesSnapshot } from "../services/reportService";
import { defaultReferencePeriod } from "../utils/referencePeriod";
export const CARTES_PERIOD_MODE_STORAGE_KEY = "reports-cartes-period-mode";
interface State {
year: number;
month: number;
mode: CartesKpiPeriodMode;
snapshot: CartesSnapshot | null;
isLoading: boolean;
error: string | null;
}
type Action =
| { type: "SET_REFERENCE_PERIOD"; payload: { year: number; month: number } }
| { type: "SET_MODE"; payload: CartesKpiPeriodMode }
| { type: "SET_LOADING"; payload: boolean }
| { type: "SET_SNAPSHOT"; payload: CartesSnapshot }
| { type: "SET_ERROR"; payload: string };
/**
* Re-exported so older imports keep working. New code should import
* `defaultReferencePeriod` from `../utils/referencePeriod`.
*/
export const defaultCartesReferencePeriod = defaultReferencePeriod;
/**
* Read the persisted period mode from localStorage. Unrecognised values fall
* back to "month" so a corrupted/legacy key never breaks the page.
*/
export function readCartesPeriodMode(
storageKey: string = CARTES_PERIOD_MODE_STORAGE_KEY,
fallback: CartesKpiPeriodMode = "month",
): CartesKpiPeriodMode {
if (typeof localStorage === "undefined") return fallback;
const saved = localStorage.getItem(storageKey);
return saved === "month" || saved === "ytd" ? saved : fallback;
}
const defaultRef = defaultReferencePeriod();
const initialState: State = {
year: defaultRef.year,
month: defaultRef.month,
mode: "month",
snapshot: null,
isLoading: false,
error: null,
};
function reducer(state: State, action: Action): State {
switch (action.type) {
case "SET_REFERENCE_PERIOD":
return { ...state, year: action.payload.year, month: action.payload.month };
case "SET_MODE":
return { ...state, mode: action.payload };
case "SET_LOADING":
return { ...state, isLoading: action.payload };
case "SET_SNAPSHOT":
return { ...state, snapshot: action.payload, isLoading: false, error: null };
case "SET_ERROR":
return { ...state, error: action.payload, isLoading: false };
default:
return state;
}
}
export function useCartes() {
const [state, dispatch] = useReducer(reducer, initialState, (init) => ({
...init,
mode: readCartesPeriodMode(),
}));
const fetchIdRef = useRef(0);
const fetch = useCallback(async (year: number, month: number, mode: CartesKpiPeriodMode) => {
const id = ++fetchIdRef.current;
dispatch({ type: "SET_LOADING", payload: true });
try {
const snapshot = await getCartesSnapshot(year, month, mode);
if (id !== fetchIdRef.current) return;
dispatch({ type: "SET_SNAPSHOT", payload: snapshot });
} catch (e) {
if (id !== fetchIdRef.current) return;
dispatch({ type: "SET_ERROR", payload: e instanceof Error ? e.message : String(e) });
}
}, []);
useEffect(() => {
fetch(state.year, state.month, state.mode);
}, [fetch, state.year, state.month, state.mode]);
const setReferencePeriod = useCallback((year: number, month: number) => {
dispatch({ type: "SET_REFERENCE_PERIOD", payload: { year, month } });
}, []);
const setMode = useCallback((mode: CartesKpiPeriodMode) => {
if (typeof localStorage !== "undefined") {
localStorage.setItem(CARTES_PERIOD_MODE_STORAGE_KEY, mode);
}
dispatch({ type: "SET_MODE", payload: mode });
}, []);
return {
...state,
setReferencePeriod,
setMode,
};
}