- 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>
47 lines
1.4 KiB
TypeScript
47 lines
1.4 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import BudgetVsActualTable from "./BudgetVsActualTable";
|
|
import { getBudgetVsActualData } from "../../services/budgetService";
|
|
import type { BudgetVsActualRow } from "../../shared/types";
|
|
|
|
export interface CompareBudgetViewProps {
|
|
year: number;
|
|
month: number;
|
|
}
|
|
|
|
export default function CompareBudgetView({ year, month }: CompareBudgetViewProps) {
|
|
const { t } = useTranslation();
|
|
const [rows, setRows] = useState<BudgetVsActualRow[]>([]);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
setError(null);
|
|
getBudgetVsActualData(year, month)
|
|
.then((data) => {
|
|
if (!cancelled) setRows(data);
|
|
})
|
|
.catch((e: unknown) => {
|
|
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
});
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [year, month]);
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="bg-[var(--negative)]/10 text-[var(--negative)] rounded-xl p-4">{error}</div>
|
|
);
|
|
}
|
|
|
|
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.bva.noData")}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <BudgetVsActualTable data={rows} />;
|
|
}
|