diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 2fd9af4..9eb14fa 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -9,6 +9,7 @@ - Rapports : pose les fondations d'un futur filtre de comptes pour les rapports — le hook de période partagé suit désormais aussi une sélection multi-comptes via l'URL (mémorisable comme la plage de dates, et validée contre les URL malformées ou modifiées à la main). Purement interne pour l'instant : aucun contrôle de filtre visible encore, le sélecteur de comptes et le branchement par rapport arrivent dans des issues de suivi (#272). - Rapports : les sept services de rapports (tendances, catégories dans le temps, comparaison réel-vs-réel, budget-vs-réel, dépenses par catégorie, et le tableau de bord Cartes) acceptent désormais un filtre multi-comptes optionnel, comparé à une liste paramétrée de sources d'import. Plomberie interne seulement — omettre le filtre partout donne des résultats identiques au byte près, et aucune page de rapport n'expose encore de contrôle de filtre ; cela arrive dans des issues de suivi (#273). - Rapports : pose les fondations de l'interface du filtre de comptes — un panneau de filtre partagé affiche le contrôle de période propre à chaque rapport à côté d'une sélection multiple (cases à cocher) de vos sources d'import. Purement interne pour l'instant : aucune page de rapport n'affiche encore ce panneau, cela arrive dans des issues de suivi (#274). +- Rapports → Tendances : le panneau de filtre partagé apparaît désormais sur ce rapport, à côté du sélecteur de période. Cocher une ou plusieurs sources d'import restreint à la fois la vue mensuelle globale et le tableau de résultat par catégorie à ces sources ; ne rien cocher garde l'affichage de toutes les sources, inchangé (#275). ### Corrigé diff --git a/CHANGELOG.md b/CHANGELOG.md index 4892846..cb69c37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Reports: laid the groundwork for an upcoming account filter across the report pages — the shared period hook now also tracks a URL-backed, multi-account selection (bookmarkable like the date range, and validated against malformed/hand-edited URLs). Internal only for now: no filter control is visible yet, the account picker and per-report wiring land in follow-up issues (#272). - Reports: all seven report services (trends, by-category-over-time, real-vs-real compare, budget-vs-actual, expenses-by-category, and the Cartes dashboard) now accept an optional multi-account filter, matched against a parameterized list of import sources. Backend plumbing only — leaving every filter out still returns byte-identical results, and no report page exposes a filter control yet; that lands in follow-up issues (#273). - Reports: laid the groundwork for the account filter's UI — a shared filter panel renders each report page's own period control alongside a multi-select (checkbox list) of your import sources. Internal only for now: no report page renders this panel yet, that lands in follow-up issues (#274). +- Reports → Trends: the shared filter panel now appears on this report, next to the period selector. Checking one or more import sources narrows both the global monthly view and the by-category result table to those sources; leaving none checked keeps showing every source, unchanged (#275). ### Fixed diff --git a/docs/architecture.md b/docs/architecture.md index b0a628b..d201453 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -206,7 +206,7 @@ Chaque hook encapsule la logique d'état via `useReducer` : | `useAdjustments` | Ajustements | | `useBudget` | Budget | | `useDashboard` | Métriques du tableau de bord | -| `useReportsPeriod` | Période de reporting synchronisée via query string (bookmarkable) + filtre compte (`accountIds`, `import_sources.id`) via le paramètre `sources`, même mécanique bookmarkable ; défaut `[]` = aucun filtre (fondation #272, pas encore branché sur les services ni exposé dans une UI) | +| `useReportsPeriod` | Période de reporting synchronisée via query string (bookmarkable) + filtre compte (`accountIds`, `import_sources.id`) via le paramètre `sources`, même mécanique bookmarkable ; défaut `[]` = aucun filtre (fondation #272). Branché sur les 7 services de rapports (#273) et exposé via `` (#274) ; adopté sur la page Tendances (#275) — reste à adopter sur Comparaison/Budget/Dashboard/Hub (#276) | | `useHighlights` | Panneau de faits saillants du hub rapports | | `useTrends` | Rapport Tendances (sous-vue flux global / par catégorie) | | `useCompare` | Rapport Comparables (mode `actual`/`budget`, sous-toggle MoM ↔ YoY, mois de référence explicite avec wrap-around janvier) | diff --git a/src/hooks/useTrends.ts b/src/hooks/useTrends.ts index cc8d2eb..946e953 100644 --- a/src/hooks/useTrends.ts +++ b/src/hooks/useTrends.ts @@ -1,6 +1,7 @@ import { useReducer, useEffect, useRef, useCallback } from "react"; -import type { MonthlyTrendItem, CategoryOverTimeData } from "../shared/types"; +import type { MonthlyTrendItem, CategoryOverTimeData, ImportSource } from "../shared/types"; import { getMonthlyTrends, getCategoryOverTime } from "../services/reportService"; +import { getAllImportSources } from "../services/transactionService"; import { useReportsPeriod } from "./useReportsPeriod"; export type TrendsSubView = "global" | "byCategory"; @@ -9,6 +10,7 @@ interface State { subView: TrendsSubView; monthlyTrends: MonthlyTrendItem[]; categoryOverTime: CategoryOverTimeData; + accounts: ImportSource[]; isLoading: boolean; error: string | null; } @@ -18,12 +20,14 @@ type Action = | { type: "SET_LOADING"; payload: boolean } | { type: "SET_TRENDS"; payload: MonthlyTrendItem[] } | { type: "SET_CATEGORY_OVER_TIME"; payload: CategoryOverTimeData } + | { type: "SET_ACCOUNTS"; payload: ImportSource[] } | { type: "SET_ERROR"; payload: string }; const initialState: State = { subView: "byCategory", monthlyTrends: [], categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {}, types: {}, tree: [] }, + accounts: [], isLoading: false, error: null, }; @@ -38,6 +42,8 @@ function reducer(state: State, action: Action): State { return { ...state, monthlyTrends: action.payload, isLoading: false, error: null }; case "SET_CATEGORY_OVER_TIME": return { ...state, categoryOverTime: action.payload, isLoading: false, error: null }; + case "SET_ACCOUNTS": + return { ...state, accounts: action.payload }; case "SET_ERROR": return { ...state, error: action.payload, isLoading: false }; default: @@ -46,32 +52,48 @@ function reducer(state: State, action: Action): State { } export function useTrends() { - const { from, to } = useReportsPeriod(); + const { from, to, accountIds } = useReportsPeriod(); const [state, dispatch] = useReducer(reducer, initialState); const fetchIdRef = useRef(0); - const fetch = useCallback(async (subView: TrendsSubView, dateFrom: string, dateTo: string) => { - const id = ++fetchIdRef.current; - dispatch({ type: "SET_LOADING", payload: true }); - try { - if (subView === "global") { - const data = await getMonthlyTrends(dateFrom, dateTo); + const fetch = useCallback( + async (subView: TrendsSubView, dateFrom: string, dateTo: string, ids: number[]) => { + const id = ++fetchIdRef.current; + dispatch({ type: "SET_LOADING", payload: true }); + try { + if (subView === "global") { + const data = await getMonthlyTrends(dateFrom, dateTo, ids); + if (id !== fetchIdRef.current) return; + dispatch({ type: "SET_TRENDS", payload: data }); + } else { + const data = await getCategoryOverTime(dateFrom, dateTo, undefined, ids); + if (id !== fetchIdRef.current) return; + dispatch({ type: "SET_CATEGORY_OVER_TIME", payload: data }); + } + } catch (e) { if (id !== fetchIdRef.current) return; - dispatch({ type: "SET_TRENDS", payload: data }); - } else { - const data = await getCategoryOverTime(dateFrom, dateTo); - if (id !== fetchIdRef.current) return; - dispatch({ type: "SET_CATEGORY_OVER_TIME", payload: data }); + dispatch({ type: "SET_ERROR", payload: e instanceof Error ? e.message : String(e) }); } - } catch (e) { - if (id !== fetchIdRef.current) return; - dispatch({ type: "SET_ERROR", payload: e instanceof Error ? e.message : String(e) }); - } - }, []); + }, + [] + ); useEffect(() => { - fetch(state.subView, from, to); - }, [fetch, state.subView, from, to]); + fetch(state.subView, from, to, accountIds); + }, [fetch, state.subView, from, to, accountIds]); + + // Load the import-source list once, for the FilterPanel's account checkboxes + // (same query as the Transactions single-source filter — see useTransactions.ts). + useEffect(() => { + (async () => { + try { + const accounts = await getAllImportSources(); + dispatch({ type: "SET_ACCOUNTS", payload: accounts }); + } catch (e) { + dispatch({ type: "SET_ERROR", payload: e instanceof Error ? e.message : String(e) }); + } + })(); + }, []); const setSubView = useCallback((sv: TrendsSubView) => { dispatch({ type: "SET_SUBVIEW", payload: sv }); diff --git a/src/pages/ReportsTrendsPage.tsx b/src/pages/ReportsTrendsPage.tsx index 11594d2..4b11446 100644 --- a/src/pages/ReportsTrendsPage.tsx +++ b/src/pages/ReportsTrendsPage.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; import { ArrowLeft } from "lucide-react"; import PeriodSelector from "../components/dashboard/PeriodSelector"; +import FilterPanel from "../components/reports/FilterPanel"; import MonthlyTrendsChart from "../components/reports/MonthlyTrendsChart"; import MonthlyTrendsTable from "../components/reports/MonthlyTrendsTable"; import CategoryOverTimeChart from "../components/reports/CategoryOverTimeChart"; @@ -21,8 +22,8 @@ const STORAGE_KEY = "reports-viewmode-trends"; export default function ReportsTrendsPage() { const { t } = useTranslation(); - const { period, setPeriod, from, to, setCustomDates } = useReportsPeriod(); - const { subView, setSubView, monthlyTrends, categoryOverTime, isLoading, error } = useTrends(); + const { period, setPeriod, from, to, setCustomDates, accountIds, setAccountIds } = useReportsPeriod(); + const { subView, setSubView, monthlyTrends, categoryOverTime, accounts, isLoading, error } = useTrends(); const [viewMode, setViewMode] = useState(() => readViewMode(STORAGE_KEY, "table")); const [chartType, setChartType] = useState(() => readTrendsChartType()); const [hiddenCategories, setHiddenCategories] = useState>(new Set()); @@ -55,50 +56,56 @@ export default function ReportsTrendsPage() {

{t("reports.hub.trends")}

-
- -
-
- - -
- {subView === "byCategory" && viewMode === "chart" && ( - - )} - + + } + accountIds={accountIds} + onAccountIdsChange={setAccountIds} + accounts={accounts} + /> + +
+
+ +
+ {subView === "byCategory" && viewMode === "chart" && ( + + )} +
{error && (