Simpl-Resultat/src/pages/ReportsTrendsPage.tsx
le king fu 02efc75542
Some checks failed
PR Check / rust (push) Has been cancelled
PR Check / frontend (push) Has been cancelled
PR Check / rust (pull_request) Successful in 21m22s
PR Check / frontend (pull_request) Successful in 2m15s
feat(reports/trends): add stacked-area chart option for category view (#105)
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.
2026-04-19 07:23:49 -04:00

130 lines
5.3 KiB
TypeScript

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 type { CategoryOverTimeChartType } from "../components/reports/CategoryOverTimeChart";
import CategoryOverTimeTable from "../components/reports/CategoryOverTimeTable";
import ViewModeToggle, { readViewMode, type ViewMode } from "../components/reports/ViewModeToggle";
import TrendsChartTypeToggle, {
readTrendsChartType,
TRENDS_CHART_TYPE_STORAGE_KEY,
} from "../components/reports/TrendsChartTypeToggle";
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 [chartType, setChartType] = useState<CategoryOverTimeChartType>(() => readTrendsChartType());
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={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>
{subView === "byCategory" && viewMode === "chart" && (
<TrendsChartTypeToggle
value={chartType}
onChange={setChartType}
storageKey={TRENDS_CHART_TYPE_STORAGE_KEY}
/>
)}
<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}
chartType={chartType}
/>
) : (
<CategoryOverTimeTable data={categoryOverTime} hiddenCategories={hiddenCategories} />
)}
</div>
);
}