Adds a read-only Settings subpage at /settings/categories/standard that
exposes the full v1 IPC category taxonomy:
- Recursive tree with per-root expand/collapse (chevron buttons), clickable
only via the disclosure caret — no destructive actions anywhere on the
page.
- Live counter banner: roots / subcategories / leaves / total, computed
from the bundled categoryTaxonomyV1 JSON.
- Accent- and case-insensitive full-text search over translated names;
matching nodes keep their ancestor chain visible, non-matching branches
are pruned from the visible tree.
- Hover tooltips (native `title`) showing i18n_key, type (income /
expense / transfer — translated) and numeric id of each node — useful
for power-users cross-referencing the consolidated schema.
- Export as PDF button that triggers window.print(); a dedicated
@media print rule in styles.css forces every branch to render fully
expanded during printing regardless of the on-screen collapse state,
and hides the toolbar / back-link.
- All labels resolve via t(node.i18n_key, { defaultValue: node.name })
to be forward-compatible with future user-created taxonomy rows that
have no i18n_key.
Also:
- New CategoriesCard in Settings that links to the page (FolderTree
icon, consistent with the userGuide / changelog card pattern).
- i18n keys added under categoriesSeed.guidePage.* and
settings.categoriesCard.* (FR + EN).
- CHANGELOG.md + CHANGELOG.fr.md updated under [Unreleased] / Added.
Route uses the English-style `/settings/categories/standard` to match
the rest of the app (/settings, /categories, /changelog, ...). The
original spec mentions a French-accented path but the existing router
is English-only; documenting here so reviewers can see the decision.
No SQL migration, no schema change, no write to the database — this
is strictly a read-only view on the TS-side taxonomy bundle.
Type-check clean (tsc --noEmit), 148/148 vitest tests pass, vite build
succeeds.
Source of truth for the v1 IPC taxonomy on the TS side. Loads the bundled
JSON, exposes typed helpers (findById, findByPath, getLeaves, getParentById)
used by the upcoming Guide (#117) and Migration (#121) pages.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Livraison 1 du milestone spec-refonte-seed-categories-ipc. Applies the
new v1 IPC (Indice des prix à la consommation) taxonomy to freshly
created profiles while leaving existing v2 profiles untouched until the
migration wizard (upcoming issue #121) prompts them to move.
- Migration v8 (additive only):
- ALTER TABLE categories ADD COLUMN i18n_key TEXT
- INSERT OR IGNORE user_preferences.categories_schema_version=v2
(existing profiles tagged as v2 for later migration)
- consolidated_schema.sql rewritten with the full v1 seed and
categories_schema_version='v1' default for brand-new profiles
- src/data/categoryTaxonomyV1.json bundled as the TS-side source of
truth (consumed by #116 categoryTaxonomyService next)
- categoriesSeed.* i18n namespace (FR/EN) — 150 entries each
- CategoryTree and CategoryCombobox fall back to the raw `name` when
i18n_key is null (user-created categories stay literal)
- CategoryTreeNode and CategoryRow gain the i18n_key field end-to-end
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrapper around dataExportService that creates and verifies a full SREF
backup before the v2->v1 categories migration. Throws on any failure to
ensure migration aborts cleanly.
- Generates filename <ProfileName>_avant-migration-<ISO8601>.sref
- Writes to ~/Documents/Simpl-Resultat/backups/ (creates dir if missing)
- Verifies integrity via re-read + SHA-256 checksum
- Reuses profile PIN for encryption when protected
- Adds two minimal Tauri commands: ensure_backup_dir, get_file_size
- Stable error codes (BackupError) to map to i18n keys in the UI layer
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Source of truth for milestone spec-refonte-seed-categories-ipc
(issues #115-#123).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Expense budgets are stored signed-negative by budgetService. The Cartes
budget-adherence card used raw values in its filter (monthBudget > 0),
its in-target comparison (|actual| <= monthBudget), and its overrun
calculation — all of which silently rejected every expense row. Route
every amount through Math.abs() so the card reflects real budget data.
Test: regression fixture with a signed-negative monthBudget that must
pass the filter and count as in-target or overrun based on absolute
values.
Fixes#112
- 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
The legacy chart was a stacked BarChart, not a LineChart — the initial 'line'
naming was misleading. Rename internal type, i18n key (chartLine -> chartBar,
Lignes -> Barres, Lines -> Bars) and icon. Legacy 'line' in localStorage is
migrated to 'bar' on read.
Adds a segmented chart-type toggle to the /reports/trends "By category"
sub-view that switches between the existing stacked bars (default,
unchanged) and a new Recharts AreaChart with stackId="1" showing total
composition over time. Both modes share the same category palette and
SVG grayscale patterns so the visual signature is preserved.
- CategoryOverTimeChart gains a chartType: 'line' | 'area' prop
(default 'line' for backward compatibility with the dashboard usage).
- New TrendsChartTypeToggle component, persisted in localStorage under
"reports-trends-category-charttype".
- Toggle only renders in the "By category" sub-view with chart view
mode selected; hidden otherwise.
- i18n keys reports.trends.chartLine / chartArea / chartTypeAria in
FR and EN.
- CHANGELOG entries in both languages.
Mirror the BudgetVsActualTable structure in the Actual-vs-Actual compare
mode so MoM and YoY both surface a Monthly block (reference month vs
comparison month) and a Cumulative YTD block (progress through the
reference month vs progress through the previous window).
- CategoryDelta gains cumulative{Previous,Current}Amount and
cumulativeDelta{Abs,Pct}. Legacy previousAmount / currentAmount /
deltaAbs / deltaPct are kept as aliases of the monthly block so the
Highlights hub, Cartes dashboard and ComparePeriodChart keep working
unchanged.
- getCompareMonthOverMonth: cumulative-previous window ends at the end
of the previous month within the SAME year; when the reference month
is January, the previous window sits entirely in the prior calendar
year (Jan → Dec).
- getCompareYearOverYear: now takes an optional reference month
(defaults to December for backward compatibility). Monthly block
compares the single reference month across years; cumulative block
compares Jan → refMonth across years.
- ComparePeriodTable rebuilt with two colspan header groups, four
sub-columns each, a totals row and month/year boundary sub-labels.
- ComparePeriodChart unchanged: still reads the monthly primary fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Swap the native <select> in CategoryZoomHeader for the reusable
CategoryCombobox. Enhances the combobox with ARIA compliance
(combobox, listbox, option roles + aria-expanded, aria-controls,
aria-activedescendant) and hierarchy indentation based on parent_id
depth. Adds reports.category.searchPlaceholder in FR/EN.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Remove the non-functional PeriodSelector from /reports/cartes — the Cartes
report is by design a "month X vs X-1 vs X-12" snapshot, so the
reference-month picker is the only control needed.
- Simplify useCartes to drop its useReportsPeriod dependency; the hook now
only exposes the reference year/month and its setter.
- Add a (?) help bubble (lucide HelpCircle) next to the savings-rate KPI
title, wired up via a new `tooltip?: string` prop on KpiCard.
- Propagate `number | null` through CartesKpi.current and buildKpi so
savings rate is now null (rendered as "—") when reference-month income
is 0 instead of a misleading "0 %". Use refExpenses directly for
seasonality.referenceAmount since it is always numeric.
- Update the cartes snapshot tests to expect null for the zero-income case.
- Add FR/EN strings reports.cartes.savingsRateTooltip + CHANGELOG entries
in both locales.
Closes#67
Add opt-in Feedback Hub widget integrated into the Settings Logs card. Routes through a Rust command to bypass CORS and centralize privacy audit. First submission triggers a one-time consent dialog; three opt-in checkboxes (context, logs, identify with Maximus account) all unchecked by default. Wording and payload follow the cross-app conventions in la-compagnie-maximus/docs/feedback-hub-ops.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New /reports/cartes page surfaces a dashboard-style snapshot of the
reference month:
- 4 KPI cards (income / expenses / net / savings rate) showing MoM and
YoY deltas simultaneously, each with a 13-month sparkline highlighting
the reference month
- 12-month income vs expenses overlay chart (bars + net balance line)
- Top 5 category increases + top 5 decreases MoM, clickable through to
the category zoom report
- Budget adherence card: on-target count + 3 worst overruns with
progress bars
- Seasonality card: reference month vs same calendar month averaged
over the two previous years, with deviation indicator
All data is fetched in a single getCartesSnapshot() service call that
runs four queries concurrently (25-month flow, MoM category deltas,
budget-vs-actual, seasonality). Missing months are filled with zeroes
in the sparklines but preserved as null in the MoM/YoY deltas so the UI
can distinguish "no data" from "zero spend".
- Exported pure helpers: shiftMonth, defaultCartesReferencePeriod
- 13 vitest cases covering zero data, MoM/YoY computation, January
wrap-around, missing-month handling, division by zero for the
savings rate, seasonality with and without history, top mover sign
splitting and 5-cap
Note: src/components/reports/CompareReferenceMonthPicker.tsx is a
temporary duplicate — the canonical copy lives on the issue-96 branch
(refactor: compare report). Once both branches merge the content is
identical and git will dedupe. Keeping the local copy here means the
Cartes branch builds cleanly on main without depending on #96.
Closes#97
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Collapse the three Compare tabs (MoM / YoY / Budget) into two modes. The
new "Actual vs actual" mode exposes an explicit reference-month dropdown
in the header (defaults to the previous month, wraps around January) and
a MoM/YoY sub-toggle. The chart is rewritten to a grouped side-by-side
BarChart with two bars per category (reference period vs comparison
period) so both values are visible at a glance instead of just the
delta. The URL PeriodSelector stays in sync with the reference month.
- useCompare: state splits into { mode: "actual"|"budget", subMode:
"mom"|"yoy", year, month }. Pure helpers previousMonth(),
defaultReferencePeriod(), comparisonMeta() extracted for tests
- CompareModeTabs: 2 modes instead of 3
- New CompareSubModeToggle and CompareReferenceMonthPicker components
- ComparePeriodChart: grouped bars via two <Bar dataKey="..."/> on a
vertical BarChart
- i18n: modeActual / subModeMoM / subModeYoY / referenceMonth (FR+EN),
retire modeMoM / modeYoY
- 9 new vitest cases covering the pure helpers (January wrap-around for
both MoM and YoY, default reference period, month/year arithmetic)
Closes#96
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Milestone spec-refonte-rapports: reports hub + 4 sub-reports, per-domain
hooks, contextual keyword editing, category zoom with recursive CTE.
Dynamic pivot table removed. See CHANGELOG for the full list of changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Delete legacy src/hooks/useReports.ts (the monolithic hook is now fully
replaced by the per-domain hooks from #70)
- Delete src/components/reports/ReportFilterPanel.tsx (last caller was the
pre-refactor ReportsPage; no longer referenced anywhere)
- Update docs/architecture.md: reports hook list now lists the 5 per-domain
hooks, reports service entry lists every new endpoint, routing section
lists the 4 sub-routes, categorizationService entry mentions the new
keyword-editing helpers, components folder count + page count updated
- Update docs/guide-utilisateur.md section 9: rewrite around hub + 4
sub-reports, explain bookmarkable period via query string, walk through
the right-click keyword editing flow, remove stale pivot section
- Rewrite in-app docs.reports.* i18n in both FR and EN to match the new
UX (hub, sub-reports, contextual keywords)
- New ADR docs/adr/0007-reports-hub-refactor.md: context, decision (hub +
four routes, per-domain hooks, URL period, security guarantees on the
keyword dialog, bounded recursive CTE for category zoom), consequences,
alternatives considered
- CHANGELOG.md + CHANGELOG.fr.md: Unreleased entries describing the hub,
each sub-report, contextual keyword editing, bookmarkable period, view
mode persistence, useReports split, pivot removal, and the security
posture of AddKeywordDialog / getCategoryZoom
Fixes#76
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire the ContextMenu + AddKeywordDialog pair onto the remaining per-transaction
surfaces. No new business logic — pure composition of #69 / #74 pieces.
- HighlightsTopTransactionsList: optional onContextMenuRow prop, ReportsHighlightsPage
renders ContextMenu + AddKeywordDialog on right-click
- TransactionTable: optional onRowContextMenu prop on each <tr>; TransactionsPage
handles it and opens the dialog pre-filled with the row description + current
category
- Aggregate tables (HighlightsTopMoversTable, ComparePeriodTable, MonthlyTrendsTable,
CategoryOverTimeTable) are intentionally NOT wired: they show category / month
aggregates, not individual transactions, so there is no keyword to extract from
a row — the dialog would be nonsensical there
Fixes#75
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Service layer
- New reportService.getCategoryZoom(categoryId, from, to, includeChildren) —
bounded recursive CTE (WHERE ct.depth < 5) protects against parent_id cycles;
direct-only path skips the CTE; every binding is parameterised
- Export categorizationService helpers normalizeDescription / buildKeywordRegex /
compileKeywords so the dialog can reuse them
- New validateKeyword() enforces 2–64 char length (anti-ReDoS), whitespace-only
rejection, returns discriminated result
- New previewKeywordMatches(keyword, limit=50) uses parameterised LIKE + regex
filter in memory; caps candidate scan at 1000 rows to protect against
catastrophic backtracking
- New applyKeywordWithReassignment wraps INSERT (or UPDATE-reassign) +
per-transaction UPDATEs in an explicit BEGIN/COMMIT/ROLLBACK; rejects
existing keyword reassignment unless allowReplaceExisting is set; never
recategorises historical transactions beyond the ids the caller supplied
Hook
- Flesh out useCategoryZoom with reducer + fetch + refetch hook
Components (flat under src/components/reports/)
- CategoryZoomHeader — category combobox + include/direct toggle
- CategoryDonutChart — template'd from dashboard/CategoryPieChart with
innerRadius=55 and ChartPatternDefs for SVG patterns
- CategoryEvolutionChart — AreaChart with Intl-formatted axes
- CategoryTransactionsTable — sortable table with per-row onContextMenu
→ ContextMenu → "Add as keyword" action
AddKeywordDialog — src/components/categories/AddKeywordDialog.tsx
- Lives in categories/ (not reports/) because it is a keyword-editing widget
consumed from multiple sections
- Renders transaction descriptions as React children only (no
dangerouslySetInnerHTML); CSS truncation (CWE-79 safe)
- Per-row checkboxes for applying recategorisation; cap visible rows at 50;
explicit opt-in checkbox to extend to N-50 non-displayed matches
- Surfaces apply errors + "keyword already exists" replace prompt
- Re-runs category zoom fetch on success so the zoomed view updates
Page
- ReportsCategoryPage composes header + donut + evolution + transactions
+ AddKeywordDialog, fetches from useCategoryZoom, preserves query string
for back navigation
i18n
- New keys reports.category.* and reports.keyword.* in FR + EN
- Plural forms use i18next v25 _one / _other suffixes (nMatches)
Tests
- 3 reportService tests cover bounded CTE, cycle-guard depth check, direct-only fallthrough
- New categorizationService.test.ts: 13 tests covering validation boundaries,
parameterised LIKE preview, regex word-boundary filter, explicit BEGIN/COMMIT
wrapping, rollback on failure, existing keyword reassignment policy
- 62 total tests passing
Fixes#74
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Services: getCompareMonthOverMonth(year, month) and getCompareYearOverYear(year)
return CategoryDelta[] (expense-side, ABS aggregates, parameterised SQL only)
- Shared CategoryDelta type with HighlightMover now aliased to it
- Flesh out useCompare hook: reducer + fetch + automatic year/month inference
from the shared useReportsPeriod `to` date; budget mode skips fetch and
delegates to CompareBudgetView which wraps the existing BudgetVsActualTable
- Components: CompareModeTabs (MoM/YoY/Budget tabs), ComparePeriodTable (sortable
table with signed delta coloring), ComparePeriodChart (diverging horizontal
bar chart with ChartPatternDefs for SVG patterns), CompareBudgetView
(fetches budget rows for the current target year/month)
- ReportsComparePage wires everything with PeriodSelector + ViewModeToggle
(storage key reports-viewmode-compare); chart/table toggle is hidden in budget
mode since the budget table has its own presentation
- i18n keys: reports.compare.modeMoM / modeYoY / modeBudget in FR + EN
- 4 new vitest cases for the compare services: parameterised boundaries,
January wrap-around to December previous year, delta conversion with
previous=0 fallback to null pct, year-over-year spans
Fixes#73
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Flesh out ReportsTrendsPage with internal subview toggle
(global / byCategory) and ViewModeToggle (storage key reports-viewmode-trends)
- Reuse existing MonthlyTrendsChart/Table and CategoryOverTimeChart/Table
without modification; wire them through useTrends + useReportsPeriod so the
URL period is respected
- Add reports.trends.subviewGlobal / subviewByCategory keys in FR + EN
Fixes#72
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New useReportsPeriod hook reads/writes period via ?from=&to=&period= URL params,
default civil year, pure resolver exported for tests
- New per-domain hooks: useHighlights, useTrends, useCompare, useCategoryZoom
(stubs wired to useReportsPeriod, to be fleshed out in #71-#74)
- Rewire legacy useReports to consume useReportsPeriod; keep backward-compat
state shape (period/customDateFrom/customDateTo) so /reports tabs keep working
- Mark useReports @deprecated pending removal in #76
- Tests: 7 new cases covering resolveReportsPeriod defaults, bookmarks,
invalid inputs, preset resolution
Fixes#70
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>