Simpl-Resultat/src/components/reports/ComparePeriodChart.tsx
le king fu 4116db4090
All checks were successful
PR Check / rust (push) Successful in 24m37s
PR Check / frontend (push) Successful in 2m21s
PR Check / rust (pull_request) Successful in 24m25s
PR Check / frontend (pull_request) Successful in 2m26s
refactor(reports/compare): unify MoM/YoY under one Actual-vs-actual mode with reference month picker (#96)
Collapse the three Compare tabs (MoM / YoY / Budget) into two modes. The
new "Actual vs actual" mode exposes an explicit reference-month dropdown
in the header (defaults to the previous month, wraps around January) and
a MoM/YoY sub-toggle. The chart is rewritten to a grouped side-by-side
BarChart with two bars per category (reference period vs comparison
period) so both values are visible at a glance instead of just the
delta. The URL PeriodSelector stays in sync with the reference month.

- useCompare: state splits into { mode: "actual"|"budget", subMode:
  "mom"|"yoy", year, month }. Pure helpers previousMonth(),
  defaultReferencePeriod(), comparisonMeta() extracted for tests
- CompareModeTabs: 2 modes instead of 3
- New CompareSubModeToggle and CompareReferenceMonthPicker components
- ComparePeriodChart: grouped bars via two <Bar dataKey="..."/> on a
  vertical BarChart
- i18n: modeActual / subModeMoM / subModeYoY / referenceMonth (FR+EN),
  retire modeMoM / modeYoY
- 9 new vitest cases covering the pure helpers (January wrap-around for
  both MoM and YoY, default reference period, month/year arithmetic)

Closes #96

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 14:24:11 -04:00

111 lines
3.2 KiB
TypeScript

import { useTranslation } from "react-i18next";
import {
BarChart,
Bar,
XAxis,
YAxis,
Tooltip,
Legend,
ResponsiveContainer,
CartesianGrid,
} from "recharts";
import type { CategoryDelta } from "../../shared/types";
export interface ComparePeriodChartProps {
rows: CategoryDelta[];
previousLabel: string;
currentLabel: string;
}
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 ComparePeriodChart({
rows,
previousLabel,
currentLabel,
}: ComparePeriodChartProps) {
const { t, i18n } = useTranslation();
if (rows.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>
);
}
// Sort by current-period amount (largest spending first) so the user's eye
// lands on the biggest categories, then reverse so the biggest appears at
// the top of the vertical bar chart.
const chartData = [...rows]
.sort((a, b) => b.currentAmount - a.currentAmount)
.map((r) => ({
name: r.categoryName,
previousAmount: r.previousAmount,
currentAmount: r.currentAmount,
color: r.categoryColor,
}));
const previousFill = "var(--muted-foreground)";
const currentFill = "var(--primary)";
return (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-xl p-4">
<ResponsiveContainer width="100%" height={Math.max(280, chartData.length * 44 + 60)}>
<BarChart
data={chartData}
layout="vertical"
margin={{ top: 10, right: 24, bottom: 10, left: 10 }}
barCategoryGap="20%"
>
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" horizontal={false} />
<XAxis
type="number"
tickFormatter={(v) => formatCurrency(v, i18n.language)}
stroke="var(--muted-foreground)"
fontSize={11}
/>
<YAxis
type="category"
dataKey="name"
width={140}
stroke="var(--muted-foreground)"
fontSize={11}
/>
<Tooltip
formatter={(value) =>
typeof value === "number" ? formatCurrency(value, i18n.language) : String(value)
}
contentStyle={{
backgroundColor: "var(--card)",
border: "1px solid var(--border)",
borderRadius: "0.5rem",
}}
cursor={{ fill: "var(--muted)", fillOpacity: 0.2 }}
/>
<Legend
wrapperStyle={{ paddingTop: 8, fontSize: 12, color: "var(--muted-foreground)" }}
/>
<Bar
dataKey="previousAmount"
name={previousLabel}
fill={previousFill}
radius={[0, 4, 4, 0]}
/>
<Bar
dataKey="currentAmount"
name={currentLabel}
fill={currentFill}
radius={[0, 4, 4, 0]}
/>
</BarChart>
</ResponsiveContainer>
</div>
);
}