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>
79 lines
2.8 KiB
TypeScript
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>
|
|
);
|
|
}
|