Mirror the BudgetVsActualTable structure in the Actual-vs-Actual compare
mode so MoM and YoY both surface a Monthly block (reference month vs
comparison month) and a Cumulative YTD block (progress through the
reference month vs progress through the previous window).
- CategoryDelta gains cumulative{Previous,Current}Amount and
cumulativeDelta{Abs,Pct}. Legacy previousAmount / currentAmount /
deltaAbs / deltaPct are kept as aliases of the monthly block so the
Highlights hub, Cartes dashboard and ComparePeriodChart keep working
unchanged.
- getCompareMonthOverMonth: cumulative-previous window ends at the end
of the previous month within the SAME year; when the reference month
is January, the previous window sits entirely in the prior calendar
year (Jan → Dec).
- getCompareYearOverYear: now takes an optional reference month
(defaults to December for backward compatibility). Monthly block
compares the single reference month across years; cumulative block
compares Jan → refMonth across years.
- ComparePeriodTable rebuilt with two colspan header groups, four
sub-columns each, a totals row and month/year boundary sub-labels.
- ComparePeriodChart unchanged: still reads the monthly primary fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
134 lines
5.1 KiB
TypeScript
134 lines
5.1 KiB
TypeScript
import { useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Link } from "react-router-dom";
|
|
import { ArrowLeft } from "lucide-react";
|
|
import PeriodSelector from "../components/dashboard/PeriodSelector";
|
|
import CompareModeTabs from "../components/reports/CompareModeTabs";
|
|
import CompareSubModeToggle from "../components/reports/CompareSubModeToggle";
|
|
import CompareReferenceMonthPicker from "../components/reports/CompareReferenceMonthPicker";
|
|
import ComparePeriodTable from "../components/reports/ComparePeriodTable";
|
|
import ComparePeriodChart from "../components/reports/ComparePeriodChart";
|
|
import CompareBudgetView from "../components/reports/CompareBudgetView";
|
|
import ViewModeToggle, { readViewMode, type ViewMode } from "../components/reports/ViewModeToggle";
|
|
import { useCompare, comparisonMeta } from "../hooks/useCompare";
|
|
import { useReportsPeriod } from "../hooks/useReportsPeriod";
|
|
|
|
const STORAGE_KEY = "reports-viewmode-compare";
|
|
|
|
function formatMonthLabel(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 ReportsComparePage() {
|
|
const { t, i18n } = useTranslation();
|
|
const { period, setPeriod, from, to, setCustomDates } = useReportsPeriod();
|
|
const {
|
|
mode,
|
|
subMode,
|
|
setMode,
|
|
setSubMode,
|
|
setReferencePeriod,
|
|
year,
|
|
month,
|
|
rows,
|
|
isLoading,
|
|
error,
|
|
} = useCompare();
|
|
const [viewMode, setViewMode] = useState<ViewMode>(() => readViewMode(STORAGE_KEY));
|
|
|
|
const preserveSearch = typeof window !== "undefined" ? window.location.search : "";
|
|
|
|
const { previousYear, previousMonth: prevMonth } = comparisonMeta(subMode, year, month);
|
|
// Monthly block labels: a specific month on both sides. For MoM the
|
|
// previous = previous month of same year; for YoY the previous = same
|
|
// month one year earlier (comparisonMeta already resolves both).
|
|
const currentLabel = formatMonthLabel(year, month, i18n.language);
|
|
const previousLabel = formatMonthLabel(previousYear, prevMonth, i18n.language);
|
|
// Cumulative YTD block labels. For MoM both sides live in the same year
|
|
// but the previous side only covers up to the end of the previous month,
|
|
// so we surface the end-of-window month label on each side. For YoY we
|
|
// compare Jan→refMonth across two different years; the year + month label
|
|
// makes the window boundary unambiguous.
|
|
const cumulativeCurrentLabel =
|
|
subMode === "mom"
|
|
? `→ ${formatMonthLabel(year, month, i18n.language)}`
|
|
: `${String(year)} → ${formatMonthLabel(year, month, i18n.language)}`;
|
|
const cumulativePreviousLabel =
|
|
subMode === "mom"
|
|
? `→ ${formatMonthLabel(previousYear, prevMonth, i18n.language)}`
|
|
: `${String(previousYear)} → ${formatMonthLabel(previousYear, prevMonth, i18n.language)}`;
|
|
|
|
const showActualControls = mode === "actual";
|
|
|
|
return (
|
|
<div className={isLoading ? "opacity-60" : ""}>
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<Link
|
|
to={`/reports${preserveSearch}`}
|
|
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] p-1 rounded-md hover:bg-[var(--muted)]"
|
|
aria-label={t("reports.hub.title")}
|
|
>
|
|
<ArrowLeft size={18} />
|
|
</Link>
|
|
<h1 className="text-2xl font-bold">{t("reports.hub.compare")}</h1>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-4 flex-wrap">
|
|
<PeriodSelector
|
|
value={period}
|
|
onChange={setPeriod}
|
|
customDateFrom={from}
|
|
customDateTo={to}
|
|
onCustomDateChange={setCustomDates}
|
|
/>
|
|
<div className="flex gap-2 items-center flex-wrap">
|
|
<CompareModeTabs value={mode} onChange={setMode} />
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-6 flex-wrap">
|
|
<div className="flex items-center gap-3 flex-wrap">
|
|
<CompareReferenceMonthPicker
|
|
year={year}
|
|
month={month}
|
|
onChange={setReferencePeriod}
|
|
/>
|
|
{showActualControls && (
|
|
<CompareSubModeToggle value={subMode} onChange={setSubMode} />
|
|
)}
|
|
</div>
|
|
{showActualControls && (
|
|
<ViewModeToggle value={viewMode} onChange={setViewMode} storageKey={STORAGE_KEY} />
|
|
)}
|
|
</div>
|
|
|
|
{error && (
|
|
<div className="bg-[var(--negative)]/10 text-[var(--negative)] rounded-xl p-4 mb-6">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{mode === "budget" ? (
|
|
<CompareBudgetView year={year} month={month} />
|
|
) : viewMode === "chart" ? (
|
|
<ComparePeriodChart
|
|
rows={rows}
|
|
previousLabel={previousLabel}
|
|
currentLabel={currentLabel}
|
|
/>
|
|
) : (
|
|
<ComparePeriodTable
|
|
rows={rows}
|
|
previousLabel={previousLabel}
|
|
currentLabel={currentLabel}
|
|
cumulativePreviousLabel={cumulativePreviousLabel}
|
|
cumulativeCurrentLabel={cumulativeCurrentLabel}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|