- Transform /reports into a hub: highlights panel + 4 nav cards - New service: reportService.getHighlights (parameterised SQL, deterministic via referenceDate argument for tests, computes current-month balance, YTD, 12-month sparkline series, top expense movers vs previous month, top recent transactions within configurable 30/60/90 day window) - Extended types: HighlightsData, HighlightMover, MonthBalance - Wired useHighlights hook with reducer + window-days state - Hub tiles (flat naming under src/components/reports): HubNetBalanceTile, HubTopMoversTile, HubTopTransactionsTile, HubHighlightsPanel, HubReportNavCard - Detailed ReportsHighlightsPage: balance tiles, sortable top movers table, diverging bar chart (Recharts + patterns SVG), top transactions list with 30/60/90 window toggle; ViewModeToggle persistence keyed as reports-viewmode-highlights - New i18n keys: reports.hub.*, reports.highlights.* - 5 new vitest cases: empty profile, parameterised queries, window sizing, delta computation, zero-previous divisor handling Fixes #71 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
76 lines
2.6 KiB
TypeScript
76 lines
2.6 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import { BarChart, Bar, XAxis, YAxis, Cell, ReferenceLine, Tooltip, ResponsiveContainer } from "recharts";
|
|
import type { HighlightMover } from "../../shared/types";
|
|
import { ChartPatternDefs, getPatternFill } from "../../utils/chartPatterns";
|
|
|
|
export interface HighlightsTopMoversChartProps {
|
|
movers: HighlightMover[];
|
|
}
|
|
|
|
function formatCurrency(amount: number, language: string): string {
|
|
return new Intl.NumberFormat(language === "fr" ? "fr-CA" : "en-CA", {
|
|
style: "currency",
|
|
currency: "CAD",
|
|
maximumFractionDigits: 0,
|
|
}).format(amount);
|
|
}
|
|
|
|
export default function HighlightsTopMoversChart({ movers }: HighlightsTopMoversChartProps) {
|
|
const { t, i18n } = useTranslation();
|
|
|
|
if (movers.length === 0) {
|
|
return (
|
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-6 text-center text-[var(--muted-foreground)] italic">
|
|
{t("reports.empty.noData")}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const chartData = movers
|
|
.map((m, i) => ({
|
|
name: m.categoryName,
|
|
color: m.categoryColor,
|
|
delta: m.deltaAbs,
|
|
index: i,
|
|
}))
|
|
.sort((a, b) => a.delta - b.delta);
|
|
|
|
return (
|
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-4">
|
|
<ResponsiveContainer width="100%" height={Math.max(200, chartData.length * 36 + 40)}>
|
|
<BarChart data={chartData} layout="vertical" margin={{ top: 10, right: 20, bottom: 10, left: 10 }}>
|
|
<ChartPatternDefs
|
|
prefix="highlights-movers"
|
|
categories={chartData.map((d) => ({ color: d.color, index: d.index }))}
|
|
/>
|
|
<XAxis
|
|
type="number"
|
|
tickFormatter={(v) => formatCurrency(v, i18n.language)}
|
|
stroke="var(--muted-foreground)"
|
|
fontSize={11}
|
|
/>
|
|
<YAxis type="category" dataKey="name" width={120} stroke="var(--muted-foreground)" fontSize={11} />
|
|
<ReferenceLine x={0} stroke="var(--border)" />
|
|
<Tooltip
|
|
formatter={(value) =>
|
|
typeof value === "number" ? formatCurrency(value, i18n.language) : String(value)
|
|
}
|
|
contentStyle={{
|
|
backgroundColor: "var(--card)",
|
|
border: "1px solid var(--border)",
|
|
borderRadius: "0.5rem",
|
|
}}
|
|
/>
|
|
<Bar dataKey="delta">
|
|
{chartData.map((entry) => (
|
|
<Cell
|
|
key={entry.name}
|
|
fill={getPatternFill("highlights-movers", entry.index, entry.color)}
|
|
/>
|
|
))}
|
|
</Bar>
|
|
</BarChart>
|
|
</ResponsiveContainer>
|
|
</div>
|
|
);
|
|
}
|