Merge pull request 'fix: sticky category column and month dropdown selector (#29)' (#30) from fix/simpl-resultat-29-budget-visual-adjustments into main
This commit is contained in:
commit
7458e087e1
7 changed files with 69 additions and 38 deletions
|
|
@ -12,6 +12,9 @@
|
||||||
### Modifié
|
### Modifié
|
||||||
- Tableau de bord : le graphique circulaire prend 1/3 de la largeur au lieu de 1/2, donnant plus d'espace au tableau budget (#23)
|
- Tableau de bord : le graphique circulaire prend 1/3 de la largeur au lieu de 1/2, donnant plus d'espace au tableau budget (#23)
|
||||||
- Tableau de bord : les étiquettes du graphique circulaire s'affichent uniquement au survol via le tooltip (#23)
|
- Tableau de bord : les étiquettes du graphique circulaire s'affichent uniquement au survol via le tooltip (#23)
|
||||||
|
- Budget vs Réel : la colonne des catégories reste désormais fixe lors du défilement horizontal (#29)
|
||||||
|
- Budget vs Réel : titre changé pour « Budget vs Réel pour le mois de [mois] » avec un menu déroulant pour sélectionner le mois (#29)
|
||||||
|
- Budget vs Réel : le mois par défaut est maintenant le dernier mois complété au lieu du mois courant (#29)
|
||||||
|
|
||||||
## [0.6.3]
|
## [0.6.3]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@
|
||||||
### Changed
|
### Changed
|
||||||
- Dashboard: pie chart takes 1/3 width instead of 1/2, giving more space to the budget table (#23)
|
- Dashboard: pie chart takes 1/3 width instead of 1/2, giving more space to the budget table (#23)
|
||||||
- Dashboard: pie chart labels now shown only on hover via tooltip instead of permanent legend (#23)
|
- Dashboard: pie chart labels now shown only on hover via tooltip instead of permanent legend (#23)
|
||||||
|
- Budget vs Actual: category column now stays fixed when scrolling horizontally (#29)
|
||||||
|
- Budget vs Actual: title changed to "Budget vs Réel pour le mois de [month]" with a dropdown month selector (#29)
|
||||||
|
- Budget vs Actual: default month is now the last completed month instead of current month (#29)
|
||||||
|
|
||||||
## [0.6.3]
|
## [0.6.3]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
||||||
<table className="w-full text-sm">
|
<table className="w-full text-sm">
|
||||||
<thead className="sticky top-0 z-20">
|
<thead className="sticky top-0 z-20">
|
||||||
<tr className="border-b border-[var(--border)] bg-[var(--card)]">
|
<tr className="border-b border-[var(--border)] bg-[var(--card)]">
|
||||||
<th rowSpan={2} className="text-left px-3 py-2 font-medium text-[var(--muted-foreground)] align-bottom bg-[var(--card)]">
|
<th rowSpan={2} className="text-left px-3 py-2 font-medium text-[var(--muted-foreground)] align-bottom sticky left-0 bg-[var(--card)] z-30 min-w-[180px]">
|
||||||
{t("budget.category")}
|
{t("budget.category")}
|
||||||
</th>
|
</th>
|
||||||
<th colSpan={4} className="text-center px-3 py-1 font-medium text-[var(--muted-foreground)] border-l border-[var(--border)] bg-[var(--card)]">
|
<th colSpan={4} className="text-center px-3 py-1 font-medium text-[var(--muted-foreground)] border-l border-[var(--border)] bg-[var(--card)]">
|
||||||
|
|
@ -158,8 +158,8 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
||||||
const sectionYtdPct = sectionTotals.ytdBudget !== 0 ? sectionTotals.ytdVariation / Math.abs(sectionTotals.ytdBudget) : null;
|
const sectionYtdPct = sectionTotals.ytdBudget !== 0 ? sectionTotals.ytdVariation / Math.abs(sectionTotals.ytdBudget) : null;
|
||||||
return (
|
return (
|
||||||
<Fragment key={section.type}>
|
<Fragment key={section.type}>
|
||||||
<tr className="bg-[var(--muted)]/50">
|
<tr className="bg-[var(--muted)]">
|
||||||
<td colSpan={9} className="px-3 py-1.5 font-semibold text-[var(--muted-foreground)] uppercase text-xs tracking-wider">
|
<td colSpan={9} className="px-3 py-1.5 font-semibold text-[var(--muted-foreground)] uppercase text-xs tracking-wider sticky left-0 bg-[var(--muted)]">
|
||||||
{section.label}
|
{section.label}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -173,11 +173,17 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
||||||
<tr
|
<tr
|
||||||
key={`${row.category_id}-${row.is_parent}-${depth}`}
|
key={`${row.category_id}-${row.is_parent}-${depth}`}
|
||||||
className={`border-b border-[var(--border)]/50 ${
|
className={`border-b border-[var(--border)]/50 ${
|
||||||
isTopParent ? "bg-[var(--muted)]/30 font-semibold" :
|
isTopParent ? "bg-[color-mix(in_srgb,var(--muted)_30%,var(--card))] font-semibold" :
|
||||||
isIntermediateParent ? "bg-[var(--muted)]/15 font-medium" : ""
|
isIntermediateParent ? "bg-[color-mix(in_srgb,var(--muted)_15%,var(--card))] font-medium" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<td className={`py-1.5 ${isTopParent ? "px-3" : paddingClass}`}>
|
<td className={`py-1.5 sticky left-0 z-10 ${
|
||||||
|
isTopParent
|
||||||
|
? "px-3 bg-[color-mix(in_srgb,var(--muted)_30%,var(--card))]"
|
||||||
|
: isIntermediateParent
|
||||||
|
? `${paddingClass} bg-[color-mix(in_srgb,var(--muted)_15%,var(--card))]`
|
||||||
|
: `${paddingClass} bg-[var(--card)]`
|
||||||
|
}`}>
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className="w-2.5 h-2.5 rounded-full shrink-0"
|
className="w-2.5 h-2.5 rounded-full shrink-0"
|
||||||
|
|
@ -209,8 +215,8 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<tr className="border-b border-[var(--border)] bg-[var(--muted)]/40 font-semibold text-sm">
|
<tr className="border-b border-[var(--border)] bg-[color-mix(in_srgb,var(--muted)_40%,var(--card))] font-semibold text-sm">
|
||||||
<td className="px-3 py-2.5">{t(typeTotalKeys[section.type])}</td>
|
<td className="px-3 py-2.5 sticky left-0 bg-[color-mix(in_srgb,var(--muted)_40%,var(--card))] z-10">{t(typeTotalKeys[section.type])}</td>
|
||||||
<td className="text-right px-3 py-2.5 border-l border-[var(--border)]/50">
|
<td className="text-right px-3 py-2.5 border-l border-[var(--border)]/50">
|
||||||
{cadFormatter(sectionTotals.monthActual)}
|
{cadFormatter(sectionTotals.monthActual)}
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -236,8 +242,8 @@ export default function BudgetVsActualTable({ data }: BudgetVsActualTableProps)
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{/* Grand totals */}
|
{/* Grand totals */}
|
||||||
<tr className="border-t-2 border-[var(--border)] font-bold text-sm bg-[var(--muted)]/20">
|
<tr className="border-t-2 border-[var(--border)] font-bold text-sm bg-[color-mix(in_srgb,var(--muted)_20%,var(--card))]">
|
||||||
<td className="px-3 py-3">{t("common.total")}</td>
|
<td className="px-3 py-3 sticky left-0 bg-[color-mix(in_srgb,var(--muted)_20%,var(--card))] z-10">{t("common.total")}</td>
|
||||||
<td className="text-right px-3 py-3 border-l border-[var(--border)]/50">
|
<td className="text-right px-3 py-3 border-l border-[var(--border)]/50">
|
||||||
{cadFormatter(totals.monthActual)}
|
{cadFormatter(totals.monthActual)}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,8 @@ const initialState: ReportsState = {
|
||||||
monthlyTrends: [],
|
monthlyTrends: [],
|
||||||
categorySpending: [],
|
categorySpending: [],
|
||||||
categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {} },
|
categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {} },
|
||||||
budgetYear: now.getFullYear(),
|
budgetYear: now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(),
|
||||||
budgetMonth: now.getMonth() + 1,
|
budgetMonth: now.getMonth() === 0 ? 12 : now.getMonth(),
|
||||||
budgetVsActual: [],
|
budgetVsActual: [],
|
||||||
pivotConfig: { rows: [], columns: [], filters: {}, values: [] },
|
pivotConfig: { rows: [], columns: [], filters: {}, values: [] },
|
||||||
pivotResult: { rows: [], columnValues: [], dimensionLabels: {} },
|
pivotResult: { rows: [], columnValues: [], dimensionLabels: {} },
|
||||||
|
|
@ -224,18 +224,9 @@ export function useReports() {
|
||||||
dispatch({ type: "SET_PERIOD", payload: period });
|
dispatch({ type: "SET_PERIOD", payload: period });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const navigateBudgetMonth = useCallback((delta: -1 | 1) => {
|
const setBudgetMonth = useCallback((year: number, month: number) => {
|
||||||
let newMonth = state.budgetMonth + delta;
|
dispatch({ type: "SET_BUDGET_MONTH", payload: { year, month } });
|
||||||
let newYear = state.budgetYear;
|
}, []);
|
||||||
if (newMonth < 1) {
|
|
||||||
newMonth = 12;
|
|
||||||
newYear -= 1;
|
|
||||||
} else if (newMonth > 12) {
|
|
||||||
newMonth = 1;
|
|
||||||
newYear += 1;
|
|
||||||
}
|
|
||||||
dispatch({ type: "SET_BUDGET_MONTH", payload: { year: newYear, month: newMonth } });
|
|
||||||
}, [state.budgetYear, state.budgetMonth]);
|
|
||||||
|
|
||||||
const setCustomDates = useCallback((dateFrom: string, dateTo: string) => {
|
const setCustomDates = useCallback((dateFrom: string, dateTo: string) => {
|
||||||
dispatch({ type: "SET_CUSTOM_DATES", payload: { dateFrom, dateTo } });
|
dispatch({ type: "SET_CUSTOM_DATES", payload: { dateFrom, dateTo } });
|
||||||
|
|
@ -249,5 +240,5 @@ export function useReports() {
|
||||||
dispatch({ type: "SET_SOURCE_ID", payload: id });
|
dispatch({ type: "SET_SOURCE_ID", payload: id });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { state, setTab, setPeriod, setCustomDates, navigateBudgetMonth, setPivotConfig, setSourceId };
|
return { state, setTab, setPeriod, setCustomDates, setBudgetMonth, setPivotConfig, setSourceId };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -377,7 +377,8 @@
|
||||||
"ytd": "Year-to-Date",
|
"ytd": "Year-to-Date",
|
||||||
"dollarVar": "$ Var",
|
"dollarVar": "$ Var",
|
||||||
"pctVar": "% Var",
|
"pctVar": "% Var",
|
||||||
"noData": "No budget or transaction data for this period."
|
"noData": "No budget or transaction data for this period.",
|
||||||
|
"titlePrefix": "Budget vs Actual for"
|
||||||
},
|
},
|
||||||
"dynamic": "Dynamic Report",
|
"dynamic": "Dynamic Report",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
|
|
|
||||||
|
|
@ -377,7 +377,8 @@
|
||||||
"ytd": "Cumul annuel",
|
"ytd": "Cumul annuel",
|
||||||
"dollarVar": "$ \u00c9cart",
|
"dollarVar": "$ \u00c9cart",
|
||||||
"pctVar": "% \u00c9cart",
|
"pctVar": "% \u00c9cart",
|
||||||
"noData": "Aucune donn\u00e9e de budget ou de transaction pour cette p\u00e9riode."
|
"noData": "Aucune donn\u00e9e de budget ou de transaction pour cette p\u00e9riode.",
|
||||||
|
"titlePrefix": "Budget vs Réel pour le mois de"
|
||||||
},
|
},
|
||||||
"dynamic": "Rapport dynamique",
|
"dynamic": "Rapport dynamique",
|
||||||
"export": "Exporter",
|
"export": "Exporter",
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ import { PageHelp } from "../components/shared/PageHelp";
|
||||||
import type { ReportTab, CategoryBreakdownItem, DashboardPeriod, ImportSource } from "../shared/types";
|
import type { ReportTab, CategoryBreakdownItem, DashboardPeriod, ImportSource } from "../shared/types";
|
||||||
import { getAllSources } from "../services/importSourceService";
|
import { getAllSources } from "../services/importSourceService";
|
||||||
import PeriodSelector from "../components/dashboard/PeriodSelector";
|
import PeriodSelector from "../components/dashboard/PeriodSelector";
|
||||||
import MonthNavigator from "../components/budget/MonthNavigator";
|
|
||||||
import MonthlyTrendsChart from "../components/reports/MonthlyTrendsChart";
|
import MonthlyTrendsChart from "../components/reports/MonthlyTrendsChart";
|
||||||
import MonthlyTrendsTable from "../components/reports/MonthlyTrendsTable";
|
import MonthlyTrendsTable from "../components/reports/MonthlyTrendsTable";
|
||||||
import CategoryBarChart from "../components/reports/CategoryBarChart";
|
import CategoryBarChart from "../components/reports/CategoryBarChart";
|
||||||
|
|
@ -48,8 +47,8 @@ function computeDateRange(
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ReportsPage() {
|
export default function ReportsPage() {
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { state, setTab, setPeriod, setCustomDates, navigateBudgetMonth, setPivotConfig, setSourceId } = useReports();
|
const { state, setTab, setPeriod, setCustomDates, setBudgetMonth, setPivotConfig, setSourceId } = useReports();
|
||||||
const [sources, setSources] = useState<ImportSource[]>([]);
|
const [sources, setSources] = useState<ImportSource[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -93,6 +92,19 @@ export default function ReportsPage() {
|
||||||
return [];
|
return [];
|
||||||
}, [state.tab, state.categorySpending, state.categoryOverTime]);
|
}, [state.tab, state.categorySpending, state.categoryOverTime]);
|
||||||
|
|
||||||
|
const monthOptions = useMemo(() => {
|
||||||
|
const now = new Date();
|
||||||
|
const currentMonth = now.getMonth();
|
||||||
|
const currentYear = now.getFullYear();
|
||||||
|
return Array.from({ length: 24 }, (_, i) => {
|
||||||
|
const d = new Date(currentYear, currentMonth - i, 1);
|
||||||
|
const y = d.getFullYear();
|
||||||
|
const m = d.getMonth() + 1;
|
||||||
|
const label = new Intl.DateTimeFormat(i18n.language, { month: "long", year: "numeric" }).format(d);
|
||||||
|
return { key: `${y}-${m}`, value: `${y}-${m}`, label: label.charAt(0).toUpperCase() + label.slice(1) };
|
||||||
|
});
|
||||||
|
}, [i18n.language]);
|
||||||
|
|
||||||
const hasCategories = ["byCategory", "overTime"].includes(state.tab) && filterCategories.length > 0;
|
const hasCategories = ["byCategory", "overTime"].includes(state.tab) && filterCategories.length > 0;
|
||||||
const showFilterPanel = hasCategories || (state.tab === "trends" && sources.length > 1);
|
const showFilterPanel = hasCategories || (state.tab === "trends" && sources.length > 1);
|
||||||
|
|
||||||
|
|
@ -100,16 +112,30 @@ export default function ReportsPage() {
|
||||||
<div className={state.isLoading ? "opacity-50 pointer-events-none" : ""}>
|
<div className={state.isLoading ? "opacity-50 pointer-events-none" : ""}>
|
||||||
<div className="relative flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
<div className="relative flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
|
{state.tab === "budgetVsActual" ? (
|
||||||
|
<h1 className="text-2xl font-bold flex items-center gap-2 flex-wrap">
|
||||||
|
{t("reports.bva.titlePrefix")}
|
||||||
|
<select
|
||||||
|
value={`${state.budgetYear}-${state.budgetMonth}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const [y, m] = e.target.value.split("-").map(Number);
|
||||||
|
setBudgetMonth(y, m);
|
||||||
|
}}
|
||||||
|
className="text-2xl font-bold bg-[var(--card)] border border-[var(--border)] rounded-lg px-3 py-1 cursor-pointer hover:bg-[var(--muted)] transition-colors"
|
||||||
|
>
|
||||||
|
{monthOptions.map((opt) => (
|
||||||
|
<option key={opt.key} value={opt.value}>
|
||||||
|
{opt.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</h1>
|
||||||
|
) : (
|
||||||
<h1 className="text-2xl font-bold">{t("reports.title")}</h1>
|
<h1 className="text-2xl font-bold">{t("reports.title")}</h1>
|
||||||
|
)}
|
||||||
<PageHelp helpKey="reports" />
|
<PageHelp helpKey="reports" />
|
||||||
</div>
|
</div>
|
||||||
{state.tab === "budgetVsActual" ? (
|
{state.tab !== "budgetVsActual" && (
|
||||||
<MonthNavigator
|
|
||||||
year={state.budgetYear}
|
|
||||||
month={state.budgetMonth}
|
|
||||||
onNavigate={navigateBudgetMonth}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<PeriodSelector
|
<PeriodSelector
|
||||||
value={state.period}
|
value={state.period}
|
||||||
onChange={setPeriod}
|
onChange={setPeriod}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue