Simpl-Resultat/src/components/reports/CompareReferenceMonthPicker.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

93 lines
2.5 KiB
TypeScript

import { useMemo } from "react";
import { useTranslation } from "react-i18next";
export interface CompareReferenceMonthPickerProps {
year: number;
month: number;
onChange: (year: number, month: number) => void;
/** Number of recent months to show in the dropdown. Default: 24. */
monthCount?: number;
/** "today" override for tests. */
today?: Date;
}
interface Option {
value: string; // "YYYY-MM"
year: number;
month: number;
label: string;
}
function formatMonth(year: number, month: number, language: string): string {
const date = new Date(year, month - 1, 1);
return new Intl.DateTimeFormat(language === "fr" ? "fr-CA" : "en-CA", {
month: "long",
year: "numeric",
}).format(date);
}
export default function CompareReferenceMonthPicker({
year,
month,
onChange,
monthCount = 24,
today = new Date(),
}: CompareReferenceMonthPickerProps) {
const { t, i18n } = useTranslation();
const options = useMemo<Option[]>(() => {
const list: Option[] = [];
let y = today.getFullYear();
let m = today.getMonth() + 1;
for (let i = 0; i < monthCount; i++) {
list.push({
value: `${y}-${String(m).padStart(2, "0")}`,
year: y,
month: m,
label: formatMonth(y, m, i18n.language),
});
m -= 1;
if (m === 0) {
m = 12;
y -= 1;
}
}
return list;
}, [today, monthCount, i18n.language]);
const currentValue = `${year}-${String(month).padStart(2, "0")}`;
const isKnown = options.some((o) => o.value === currentValue);
const displayOptions = isKnown
? options
: [
{
value: currentValue,
year,
month,
label: formatMonth(year, month, i18n.language),
},
...options,
];
return (
<label className="inline-flex items-center gap-2">
<span className="text-sm text-[var(--muted-foreground)]">
{t("reports.compare.referenceMonth")}
</span>
<select
value={currentValue}
onChange={(e) => {
const opt = displayOptions.find((o) => o.value === e.target.value);
if (opt) onChange(opt.year, opt.month);
}}
className="rounded-lg border border-[var(--border)] bg-[var(--card)] text-[var(--foreground)] px-3 py-2 text-sm hover:bg-[var(--muted)] focus:outline-none focus:ring-2 focus:ring-[var(--primary)]"
>
{displayOptions.map((o) => (
<option key={o.value} value={o.value}>
{o.label}
</option>
))}
</select>
</label>
);
}