feat: trends report — global flow + by category (#72) #91

Merged
maximus merged 1 commit from issue-72-trends into main 2026-04-14 18:53:25 +00:00
3 changed files with 116 additions and 3 deletions
Showing only changes of commit d06dd7a858 - Show all commits

View file

@ -399,6 +399,10 @@
"categoryZoom": "Category Analysis",
"categoryZoomDescription": "Zoom in on a single category"
},
"trends": {
"subviewGlobal": "Global flow",
"subviewByCategory": "By category"
},
"highlights": {
"balances": "Balances",
"netBalanceCurrent": "This month",

View file

@ -399,6 +399,10 @@
"categoryZoom": "Analyse par catégorie",
"categoryZoomDescription": "Zoom sur une catégorie"
},
"trends": {
"subviewGlobal": "Flux global",
"subviewByCategory": "Par catégorie"
},
"highlights": {
"balances": "Soldes",
"netBalanceCurrent": "Ce mois-ci",

View file

@ -1,11 +1,116 @@
import { useState, useCallback } from "react";
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() {
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 (
<div className="p-8 text-center text-[var(--muted-foreground)]">
<h1 className="text-2xl font-bold mb-4">{t("reports.hub.trends")}</h1>
<p>{t("common.underConstruction")}</p>
<div className={isLoading ? "opacity-60" : ""}>
<div className="flex items-center gap-3 mb-4">
<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>
);
}