Merge pull request 'feat(categories): dashboard v1 discovery banner (#118)' (#130) from issue-118-dashboard-discovery-banner into main

This commit is contained in:
maximus 2026-04-21 01:16:20 +00:00
commit 084b621506
6 changed files with 120 additions and 0 deletions

View file

@ -3,6 +3,7 @@
## [Non publié]
### Ajouté
- **Bannière du tableau de bord invitant les profils v2 à découvrir la nouvelle taxonomie v1 IPC** : les profils existants (marqués `categories_schema_version='v2'`) voient désormais une bannière fermable en haut du tableau de bord qui pointe vers la nouvelle page Guide des catégories standard. La bannière est non destructive (CTA en lecture seule, aucun changement de catégories), ne s'affiche qu'aux profils v2 (les nouveaux profils semés en v1 ne la voient jamais), et sa fermeture est persistée dans `user_preferences` sous la clé `categories_v1_banner_dismissed` pour ne plus réapparaître (#118)
- **Page Guide des catégories standard** (Paramètres → *Structure standard des catégories*, route `/settings/categories/standard`) : nouvelle page en lecture seule qui expose la taxonomie v1 IPC complète sous forme d'arbre navigable avec repli/expansion par racine, un compteur global en direct (racines · sous-catégories · feuilles · total), une recherche plein texte insensible aux accents sur les noms traduits, des info-bulles au survol affichant la clé `i18n_key`, le type et l'identifiant de chaque nœud, et un bouton *Exporter en PDF* qui ouvre la boîte d'impression du navigateur. Une règle `@media print` dédiée force l'affichage complet de toutes les branches à l'impression, peu importe l'état de repli à l'écran. Tous les libellés passent par `categoriesSeed.*` avec `name` en repli pour les futures lignes personnalisées. Aucune écriture en base, aucune action destructive (#117)
- **Seed de catégories IPC pour les nouveaux profils** : les nouveaux profils sont désormais créés avec la taxonomie v1 IPC (Indice des prix à la consommation) — une hiérarchie alignée sur les catégories de Statistique Canada. Les noms des catégories du seed sont traduits dynamiquement depuis la clé i18n `categoriesSeed.*` (FR/EN), donc affichés dans la langue de l'utilisateur. Les profils existants gardent l'ancien seed v2, marqués via une nouvelle préférence `categories_schema_version` (une page de migration ultérieure offrira le passage v2→v1). Côté interne : colonne `categories.i18n_key` (nullable) ajoutée par la migration v8 (strictement additive), `src/data/categoryTaxonomyV1.json` livré comme source de vérité côté TS, les renderers `CategoryTree` et `CategoryCombobox` utilisent `name` en repli quand aucune clé de traduction n'est présente (catégories créées par l'utilisateur) (#115)

View file

@ -3,6 +3,7 @@
## [Unreleased]
### Added
- **Dashboard banner inviting v2 profiles to discover the new v1 IPC category taxonomy**: legacy profiles (tagged `categories_schema_version='v2'`) now see a dismissable banner at the top of the Dashboard pointing to the new standard categories guide page. The banner is non-destructive (read-only CTA, no category changes), only shown to v2 profiles (new v1-seeded profiles never see it), and its dismissal is persisted in `user_preferences` under `categories_v1_banner_dismissed` so it never reappears once closed (#118)
- **Standard categories guide page** (Settings → *Standard category structure*, route `/settings/categories/standard`): new read-only page that exposes the full v1 IPC taxonomy as a navigable tree with expand/collapse per root, a live category counter (roots · subcategories · leaves · total), accent-insensitive full-text search over translated names, hover tooltips showing the `i18n_key` / type / ID of each node, and a *Export as PDF* button that triggers the browser print dialog. A dedicated `@media print` rule forces every branch to render fully expanded regardless of the on-screen collapse state. All labels resolve via `categoriesSeed.*` with `name` as fallback for future custom rows. No database writes, no destructive actions (#117)
- **IPC-aligned categories seed for new profiles**: brand-new profiles are now seeded with the v1 IPC (Indice des prix à la consommation) taxonomy — a structured hierarchy aligned with Statistics Canada consumer price index categories. Category labels are now translated dynamically from the `categoriesSeed.*` i18n namespace (FR/EN), so seed categories display in the user's current language. Existing profiles remain on the legacy v2 seed, marked via a new `categories_schema_version` user preference (a later migration wizard will offer the v2→v1 transition). Internally: nullable `categories.i18n_key` column added in migration v8 (additive only), `src/data/categoryTaxonomyV1.json` bundled as the TS-side source of truth, `CategoryTree` and `CategoryCombobox` renderers fall back to the raw `name` when no translation key is present (user-created rows) (#115)

View file

@ -0,0 +1,104 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { Sparkles, X, ArrowRight } from "lucide-react";
import {
getPreference,
setPreference,
} from "../../services/userPreferenceService";
// Preference keys used to decide whether to show the banner.
// - CATEGORIES_SCHEMA_VERSION is set by migration v8 (see src-tauri/src/lib.rs):
// existing profiles are tagged 'v2' (legacy seed), new profiles are 'v1'
// (IPC taxonomy). Only v2 profiles are invited to discover the new v1 guide.
// - BANNER_DISMISSED is a persistent flag set when the user dismisses the
// banner; once set, the banner never reappears for this profile.
const CATEGORIES_SCHEMA_VERSION_KEY = "categories_schema_version";
const BANNER_DISMISSED_KEY = "categories_v1_banner_dismissed";
type Visibility = "loading" | "visible" | "hidden";
/**
* Dashboard banner that invites users on the legacy v2 category seed to
* discover the new v1 IPC taxonomy guide. It is:
* - Non-destructive (read-only CTA that navigates to the guide page).
* - Dismissable the dismissal is persisted in `user_preferences` and
* survives app restarts / profile reloads.
* - Only visible on profiles tagged `categories_schema_version='v2'`.
* Profiles on the new v1 seed never see it (they already have the IPC
* taxonomy).
*/
export default function CategoriesV1DiscoveryBanner() {
const { t } = useTranslation();
const [visibility, setVisibility] = useState<Visibility>("loading");
useEffect(() => {
let cancelled = false;
(async () => {
try {
const [schemaVersion, dismissed] = await Promise.all([
getPreference(CATEGORIES_SCHEMA_VERSION_KEY),
getPreference(BANNER_DISMISSED_KEY),
]);
if (cancelled) return;
const shouldShow = schemaVersion === "v2" && dismissed !== "1";
setVisibility(shouldShow ? "visible" : "hidden");
} catch {
// If prefs cannot be read (e.g. DB not ready), hide the banner
// silently — it is a purely informational, non-critical UI element.
if (!cancelled) setVisibility("hidden");
}
})();
return () => {
cancelled = true;
};
}, []);
const dismiss = async () => {
// Optimistically hide the banner, then persist the flag. If persistence
// fails we still keep it hidden for this session.
setVisibility("hidden");
try {
await setPreference(BANNER_DISMISSED_KEY, "1");
} catch {
// Ignore — the banner will reappear on next launch if the write failed,
// which is an acceptable degradation.
}
};
if (visibility !== "visible") return null;
return (
<div
role="status"
className="flex items-start gap-3 rounded-xl border border-[var(--primary)]/30 bg-[var(--primary)]/5 p-4 mb-6"
>
<div className="mt-0.5 shrink-0 rounded-lg bg-[var(--primary)]/10 p-2 text-[var(--primary)]">
<Sparkles size={18} />
</div>
<div className="flex-1 space-y-2 text-sm">
<p className="font-semibold text-[var(--foreground)]">
{t("dashboard.categoriesBanner.title")}
</p>
<p className="text-[var(--muted-foreground)]">
{t("dashboard.categoriesBanner.description")}
</p>
<Link
to="/settings/categories/standard"
className="inline-flex items-center gap-1.5 font-medium text-[var(--primary)] hover:underline"
>
{t("dashboard.categoriesBanner.cta")}
<ArrowRight size={14} />
</Link>
</div>
<button
type="button"
onClick={dismiss}
aria-label={t("dashboard.categoriesBanner.dismiss")}
className="shrink-0 rounded-md p-1 text-[var(--muted-foreground)] hover:bg-[var(--muted)] hover:text-[var(--foreground)] transition-colors"
>
<X size={16} />
</button>
</div>
);
}

View file

@ -28,6 +28,12 @@
"recentTransactions": "Recent Transactions",
"budgetVsActual": "Budget vs Actual",
"expensesOverTime": "Expenses Over Time",
"categoriesBanner": {
"title": "New category taxonomy available",
"description": "Your profile uses the legacy taxonomy. A new standard structure, aligned with Statistics Canada's CPI, is now available. Discover it from Settings.",
"cta": "Discover the standard structure",
"dismiss": "Dismiss banner"
},
"period": {
"month": "This month",
"3months": "3 months",

View file

@ -28,6 +28,12 @@
"recentTransactions": "Transactions récentes",
"budgetVsActual": "Budget vs Réel",
"expensesOverTime": "Dépenses dans le temps",
"categoriesBanner": {
"title": "Nouvelle taxonomie de catégories disponible",
"description": "Votre profil utilise la taxonomie historique. Une nouvelle structure standard, alignée sur l'IPC de Statistique Canada, est désormais disponible. Découvrez-la dans les paramètres.",
"cta": "Découvrir la structure standard",
"dismiss": "Fermer la bannière"
},
"period": {
"month": "Ce mois",
"3months": "3 mois",

View file

@ -5,6 +5,7 @@ import { useDashboard } from "../hooks/useDashboard";
import { PageHelp } from "../components/shared/PageHelp";
import PeriodSelector from "../components/dashboard/PeriodSelector";
import CategoryPieChart from "../components/dashboard/CategoryPieChart";
import CategoriesV1DiscoveryBanner from "../components/dashboard/CategoriesV1DiscoveryBanner";
import CategoryOverTimeChart from "../components/reports/CategoryOverTimeChart";
import BudgetVsActualTable from "../components/reports/BudgetVsActualTable";
import TransactionDetailModal from "../components/shared/TransactionDetailModal";
@ -71,6 +72,7 @@ export default function DashboardPage() {
return (
<div className={isLoading ? "opacity-50 pointer-events-none" : ""}>
<CategoriesV1DiscoveryBanner />
<div className="relative flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">{t("dashboard.title")}</h1>