Simpl-Resultat/src/pages/ReportsPage.tsx
le king fu 31765e6d17
All checks were successful
PR Check / rust (push) Successful in 23m50s
PR Check / frontend (push) Successful in 2m21s
PR Check / rust (pull_request) Successful in 23m54s
PR Check / frontend (pull_request) Successful in 2m19s
feat(reports/cartes): new KPI dashboard sub-report with sparklines, top movers, budget adherence and seasonality (#97)
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>
2026-04-15 18:20:41 -04:00

79 lines
2.8 KiB
TypeScript

import { useTranslation } from "react-i18next";
import { Sparkles, TrendingUp, Scale, Search, LayoutDashboard } from "lucide-react";
import { PageHelp } from "../components/shared/PageHelp";
import PeriodSelector from "../components/dashboard/PeriodSelector";
import HubHighlightsPanel from "../components/reports/HubHighlightsPanel";
import HubReportNavCard from "../components/reports/HubReportNavCard";
import { useHighlights } from "../hooks/useHighlights";
import { useReportsPeriod } from "../hooks/useReportsPeriod";
export default function ReportsPage() {
const { t } = useTranslation();
const { period, setPeriod, from, to, setCustomDates } = useReportsPeriod();
const { data, isLoading, error } = useHighlights();
const preserveSearch = typeof window !== "undefined" ? window.location.search : "";
const navCards = [
{
to: `/reports/highlights${preserveSearch}`,
icon: <Sparkles size={24} />,
title: t("reports.hub.highlights"),
description: t("reports.hub.highlightsDescription"),
},
{
to: `/reports/trends${preserveSearch}`,
icon: <TrendingUp size={24} />,
title: t("reports.hub.trends"),
description: t("reports.hub.trendsDescription"),
},
{
to: `/reports/compare${preserveSearch}`,
icon: <Scale size={24} />,
title: t("reports.hub.compare"),
description: t("reports.hub.compareDescription"),
},
{
to: `/reports/category${preserveSearch}`,
icon: <Search size={24} />,
title: t("reports.hub.categoryZoom"),
description: t("reports.hub.categoryZoomDescription"),
},
{
to: `/reports/cartes${preserveSearch}`,
icon: <LayoutDashboard size={24} />,
title: t("reports.hub.cartes"),
description: t("reports.hub.cartesDescription"),
},
];
return (
<div>
<div className="relative flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold">{t("reports.hub.title")}</h1>
<PageHelp helpKey="reports" />
</div>
<PeriodSelector
value={period}
onChange={setPeriod}
customDateFrom={from}
customDateTo={to}
onCustomDateChange={setCustomDates}
/>
</div>
<HubHighlightsPanel data={data} isLoading={isLoading} error={error} />
<section>
<h2 className="text-sm font-semibold uppercase tracking-wide text-[var(--muted-foreground)] mb-3">
{t("reports.hub.explore")}
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-5 gap-3">
{navCards.map((card) => (
<HubReportNavCard key={card.to} {...card} />
))}
</div>
</section>
</div>
);
}