- Services: getCompareMonthOverMonth(year, month) and getCompareYearOverYear(year) return CategoryDelta[] (expense-side, ABS aggregates, parameterised SQL only) - Shared CategoryDelta type with HighlightMover now aliased to it - Flesh out useCompare hook: reducer + fetch + automatic year/month inference from the shared useReportsPeriod `to` date; budget mode skips fetch and delegates to CompareBudgetView which wraps the existing BudgetVsActualTable - Components: CompareModeTabs (MoM/YoY/Budget tabs), ComparePeriodTable (sortable table with signed delta coloring), ComparePeriodChart (diverging horizontal bar chart with ChartPatternDefs for SVG patterns), CompareBudgetView (fetches budget rows for the current target year/month) - ReportsComparePage wires everything with PeriodSelector + ViewModeToggle (storage key reports-viewmode-compare); chart/table toggle is hidden in budget mode since the budget table has its own presentation - i18n keys: reports.compare.modeMoM / modeYoY / modeBudget in FR + EN - 4 new vitest cases for the compare services: parameterised boundaries, January wrap-around to December previous year, delta conversion with previous=0 fallback to null pct, year-over-year spans Fixes #73 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
38 lines
1.2 KiB
TypeScript
38 lines
1.2 KiB
TypeScript
import { useTranslation } from "react-i18next";
|
|
import type { CompareMode } from "../../hooks/useCompare";
|
|
|
|
export interface CompareModeTabsProps {
|
|
value: CompareMode;
|
|
onChange: (mode: CompareMode) => void;
|
|
}
|
|
|
|
export default function CompareModeTabs({ value, onChange }: CompareModeTabsProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const modes: { id: CompareMode; labelKey: string }[] = [
|
|
{ id: "mom", labelKey: "reports.compare.modeMoM" },
|
|
{ id: "yoy", labelKey: "reports.compare.modeYoY" },
|
|
{ id: "budget", labelKey: "reports.compare.modeBudget" },
|
|
];
|
|
|
|
return (
|
|
<div className="inline-flex gap-1" role="tablist">
|
|
{modes.map(({ id, labelKey }) => (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
role="tab"
|
|
onClick={() => onChange(id)}
|
|
aria-selected={value === id}
|
|
className={`px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
value === id
|
|
? "bg-[var(--primary)] text-white"
|
|
: "bg-[var(--card)] border border-[var(--border)] text-[var(--foreground)] hover:bg-[var(--muted)]"
|
|
}`}
|
|
>
|
|
{t(labelKey)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|