feat: trends report — global flow + by category with view toggle (#72)
- 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>
This commit is contained in:
parent
5d206d5faf
commit
d06dd7a858
3 changed files with 116 additions and 3 deletions
|
|
@ -399,6 +399,10 @@
|
||||||
"categoryZoom": "Category Analysis",
|
"categoryZoom": "Category Analysis",
|
||||||
"categoryZoomDescription": "Zoom in on a single category"
|
"categoryZoomDescription": "Zoom in on a single category"
|
||||||
},
|
},
|
||||||
|
"trends": {
|
||||||
|
"subviewGlobal": "Global flow",
|
||||||
|
"subviewByCategory": "By category"
|
||||||
|
},
|
||||||
"highlights": {
|
"highlights": {
|
||||||
"balances": "Balances",
|
"balances": "Balances",
|
||||||
"netBalanceCurrent": "This month",
|
"netBalanceCurrent": "This month",
|
||||||
|
|
|
||||||
|
|
@ -399,6 +399,10 @@
|
||||||
"categoryZoom": "Analyse par catégorie",
|
"categoryZoom": "Analyse par catégorie",
|
||||||
"categoryZoomDescription": "Zoom sur une catégorie"
|
"categoryZoomDescription": "Zoom sur une catégorie"
|
||||||
},
|
},
|
||||||
|
"trends": {
|
||||||
|
"subviewGlobal": "Flux global",
|
||||||
|
"subviewByCategory": "Par catégorie"
|
||||||
|
},
|
||||||
"highlights": {
|
"highlights": {
|
||||||
"balances": "Soldes",
|
"balances": "Soldes",
|
||||||
"netBalanceCurrent": "Ce mois-ci",
|
"netBalanceCurrent": "Ce mois-ci",
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,116 @@
|
||||||
|
import { useState, useCallback } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
import PeriodSelector from "../components/dashboard/PeriodSelector";
|
||||||
|
import MonthlyTrendsChart from "../components/reports/MonthlyTrendsChart";
|
||||||
|
import MonthlyTrendsTable from "../components/reports/MonthlyTrendsTable";
|
||||||
|
import CategoryOverTimeChart from "../components/reports/CategoryOverTimeChart";
|
||||||
|
import CategoryOverTimeTable from "../components/reports/CategoryOverTimeTable";
|
||||||
|
import ViewModeToggle, { readViewMode, type ViewMode } from "../components/reports/ViewModeToggle";
|
||||||
|
import { useTrends } from "../hooks/useTrends";
|
||||||
|
import { useReportsPeriod } from "../hooks/useReportsPeriod";
|
||||||
|
import type { CategoryBreakdownItem } from "../shared/types";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "reports-viewmode-trends";
|
||||||
|
|
||||||
export default function ReportsTrendsPage() {
|
export default function ReportsTrendsPage() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const { period, setPeriod, from, to, setCustomDates } = useReportsPeriod();
|
||||||
|
const { subView, setSubView, monthlyTrends, categoryOverTime, isLoading, error } = useTrends();
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>(() => readViewMode(STORAGE_KEY));
|
||||||
|
const [hiddenCategories, setHiddenCategories] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
const preserveSearch = typeof window !== "undefined" ? window.location.search : "";
|
||||||
|
|
||||||
|
const toggleHidden = useCallback((name: string) => {
|
||||||
|
setHiddenCategories((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
if (next.has(name)) next.delete(name);
|
||||||
|
else next.add(name);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const showAll = useCallback(() => setHiddenCategories(new Set()), []);
|
||||||
|
// viewDetails not used in trends view — transactions details are accessed from category zoom.
|
||||||
|
const noOpDetails = useCallback((_item: CategoryBreakdownItem) => {}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-8 text-center text-[var(--muted-foreground)]">
|
<div className={isLoading ? "opacity-60" : ""}>
|
||||||
<h1 className="text-2xl font-bold mb-4">{t("reports.hub.trends")}</h1>
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<p>{t("common.underConstruction")}</p>
|
<Link
|
||||||
|
to={`/reports${preserveSearch}`}
|
||||||
|
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] p-1 rounded-md hover:bg-[var(--muted)]"
|
||||||
|
aria-label={t("reports.hub.title")}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={18} />
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-2xl font-bold">{t("reports.hub.trends")}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6 flex-wrap">
|
||||||
|
<PeriodSelector
|
||||||
|
value={period}
|
||||||
|
onChange={setPeriod}
|
||||||
|
customDateFrom={from}
|
||||||
|
customDateTo={to}
|
||||||
|
onCustomDateChange={setCustomDates}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2 items-center flex-wrap">
|
||||||
|
<div className="inline-flex gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSubView("global")}
|
||||||
|
aria-pressed={subView === "global"}
|
||||||
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
subView === "global"
|
||||||
|
? "bg-[var(--primary)] text-white"
|
||||||
|
: "bg-[var(--card)] border border-[var(--border)] text-[var(--foreground)] hover:bg-[var(--muted)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t("reports.trends.subviewGlobal")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSubView("byCategory")}
|
||||||
|
aria-pressed={subView === "byCategory"}
|
||||||
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||||
|
subView === "byCategory"
|
||||||
|
? "bg-[var(--primary)] text-white"
|
||||||
|
: "bg-[var(--card)] border border-[var(--border)] text-[var(--foreground)] hover:bg-[var(--muted)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{t("reports.trends.subviewByCategory")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<ViewModeToggle value={viewMode} onChange={setViewMode} storageKey={STORAGE_KEY} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<div className="bg-[var(--negative)]/10 text-[var(--negative)] rounded-xl p-4 mb-6">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subView === "global" ? (
|
||||||
|
viewMode === "chart" ? (
|
||||||
|
<MonthlyTrendsChart data={monthlyTrends} />
|
||||||
|
) : (
|
||||||
|
<MonthlyTrendsTable data={monthlyTrends} />
|
||||||
|
)
|
||||||
|
) : viewMode === "chart" ? (
|
||||||
|
<CategoryOverTimeChart
|
||||||
|
data={categoryOverTime}
|
||||||
|
hiddenCategories={hiddenCategories}
|
||||||
|
onToggleHidden={toggleHidden}
|
||||||
|
onShowAll={showAll}
|
||||||
|
onViewDetails={noOpDetails}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<CategoryOverTimeTable data={categoryOverTime} hiddenCategories={hiddenCategories} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue