feat: add month dropdown to dashboard Budget vs Actual section (#31)
- Add budgetYear/budgetMonth state to useDashboard hook with last completed month as default - Add month dropdown selector in the dashboard BudgetVsActual title - Reduce dropdown font size in both Reports and Dashboard pages Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
376ca4b477
commit
7d770f8b66
5 changed files with 62 additions and 12 deletions
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
## [Non publié]
|
||||
|
||||
### Ajouté
|
||||
- Tableau de bord : menu déroulant de sélection du mois pour la section Budget vs Réel avec le dernier mois complété par défaut (#31)
|
||||
|
||||
### Modifié
|
||||
- Rapports et tableau de bord : police réduite dans le menu déroulant de mois pour un meilleur équilibre visuel (#31)
|
||||
|
||||
## [0.6.4]
|
||||
|
||||
### Ajouté
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- Dashboard: month dropdown selector for the Budget vs Actual section with last completed month as default (#31)
|
||||
|
||||
### Changed
|
||||
- Reports & Dashboard: reduced font size of month dropdown for better visual balance (#31)
|
||||
|
||||
## [0.6.4]
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ interface DashboardState {
|
|||
categoryOverTime: CategoryOverTimeData;
|
||||
budgetVsActual: BudgetVsActualRow[];
|
||||
period: DashboardPeriod;
|
||||
budgetYear: number;
|
||||
budgetMonth: number;
|
||||
customDateFrom: string;
|
||||
customDateTo: string;
|
||||
isLoading: boolean;
|
||||
|
|
@ -38,6 +40,7 @@ type DashboardAction =
|
|||
};
|
||||
}
|
||||
| { type: "SET_PERIOD"; payload: DashboardPeriod }
|
||||
| { type: "SET_BUDGET_MONTH"; payload: { year: number; month: number } }
|
||||
| { type: "SET_CUSTOM_DATES"; payload: { dateFrom: string; dateTo: string } };
|
||||
|
||||
const now = new Date();
|
||||
|
|
@ -50,6 +53,8 @@ const initialState: DashboardState = {
|
|||
categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {} },
|
||||
budgetVsActual: [],
|
||||
period: "year",
|
||||
budgetYear: now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(),
|
||||
budgetMonth: now.getMonth() === 0 ? 12 : now.getMonth(),
|
||||
customDateFrom: yearStartStr,
|
||||
customDateTo: todayStr,
|
||||
isLoading: false,
|
||||
|
|
@ -73,6 +78,8 @@ function reducer(state: DashboardState, action: DashboardAction): DashboardState
|
|||
};
|
||||
case "SET_PERIOD":
|
||||
return { ...state, period: action.payload };
|
||||
case "SET_BUDGET_MONTH":
|
||||
return { ...state, budgetYear: action.payload.year, budgetMonth: action.payload.month };
|
||||
case "SET_CUSTOM_DATES":
|
||||
return { ...state, period: "custom" as DashboardPeriod, customDateFrom: action.payload.dateFrom, customDateTo: action.payload.dateTo };
|
||||
default:
|
||||
|
|
@ -128,20 +135,18 @@ export function useDashboard() {
|
|||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
const fetchIdRef = useRef(0);
|
||||
|
||||
const fetchData = useCallback(async (period: DashboardPeriod, customFrom?: string, customTo?: string) => {
|
||||
const fetchData = useCallback(async (period: DashboardPeriod, customFrom?: string, customTo?: string, bYear?: number, bMonth?: number) => {
|
||||
const fetchId = ++fetchIdRef.current;
|
||||
dispatch({ type: "SET_LOADING", payload: true });
|
||||
dispatch({ type: "SET_ERROR", payload: null });
|
||||
|
||||
try {
|
||||
const { dateFrom, dateTo } = computeDateRange(period, customFrom, customTo);
|
||||
const currentMonth = new Date().getMonth() + 1;
|
||||
const currentYear = new Date().getFullYear();
|
||||
const [summary, categoryBreakdown, categoryOverTime, budgetVsActual] = await Promise.all([
|
||||
getDashboardSummary(dateFrom, dateTo),
|
||||
getExpensesByCategory(dateFrom, dateTo),
|
||||
getCategoryOverTime(dateFrom, dateTo),
|
||||
getBudgetVsActualData(currentYear, currentMonth),
|
||||
getBudgetVsActualData(bYear!, bMonth!),
|
||||
]);
|
||||
|
||||
if (fetchId !== fetchIdRef.current) return;
|
||||
|
|
@ -156,8 +161,8 @@ export function useDashboard() {
|
|||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(state.period, state.customDateFrom, state.customDateTo);
|
||||
}, [state.period, state.customDateFrom, state.customDateTo, fetchData]);
|
||||
fetchData(state.period, state.customDateFrom, state.customDateTo, state.budgetYear, state.budgetMonth);
|
||||
}, [state.period, state.customDateFrom, state.customDateTo, state.budgetYear, state.budgetMonth, fetchData]);
|
||||
|
||||
const setPeriod = useCallback((period: DashboardPeriod) => {
|
||||
dispatch({ type: "SET_PERIOD", payload: period });
|
||||
|
|
@ -167,5 +172,9 @@ export function useDashboard() {
|
|||
dispatch({ type: "SET_CUSTOM_DATES", payload: { dateFrom, dateTo } });
|
||||
}, []);
|
||||
|
||||
return { state, setPeriod, setCustomDates };
|
||||
const setBudgetMonth = useCallback((year: number, month: number) => {
|
||||
dispatch({ type: "SET_BUDGET_MONTH", payload: { year, month } });
|
||||
}, []);
|
||||
|
||||
return { state, setPeriod, setCustomDates, setBudgetMonth };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Wallet, TrendingUp, TrendingDown } from "lucide-react";
|
||||
import { useDashboard } from "../hooks/useDashboard";
|
||||
|
|
@ -40,8 +40,8 @@ function computeDateRange(
|
|||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const { t } = useTranslation();
|
||||
const { state, setPeriod, setCustomDates } = useDashboard();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { state, setPeriod, setCustomDates, setBudgetMonth } = useDashboard();
|
||||
const { summary, categoryBreakdown, categoryOverTime, budgetVsActual, period, isLoading } = state;
|
||||
|
||||
const [hiddenCategories, setHiddenCategories] = useState<Set<string>>(new Set());
|
||||
|
|
@ -91,6 +91,19 @@ export default function DashboardPage() {
|
|||
},
|
||||
];
|
||||
|
||||
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 { dateFrom, dateTo } = computeDateRange(period, state.customDateFrom, state.customDateTo);
|
||||
|
||||
return (
|
||||
|
|
@ -138,7 +151,23 @@ export default function DashboardPage() {
|
|||
/>
|
||||
</div>
|
||||
<div className="lg:col-span-3">
|
||||
<h2 className="text-lg font-semibold mb-3">{t("dashboard.budgetVsActual")}</h2>
|
||||
<h2 className="text-lg font-semibold mb-3 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-base font-semibold bg-[var(--card)] border border-[var(--border)] rounded-lg px-2 py-0.5 cursor-pointer hover:bg-[var(--muted)] transition-colors"
|
||||
>
|
||||
{monthOptions.map((opt) => (
|
||||
<option key={opt.key} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</h2>
|
||||
<BudgetVsActualTable data={budgetVsActual} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export default function ReportsPage() {
|
|||
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"
|
||||
className="text-lg font-bold bg-[var(--card)] border border-[var(--border)] rounded-lg px-2 py-0.5 cursor-pointer hover:bg-[var(--muted)] transition-colors"
|
||||
>
|
||||
{monthOptions.map((opt) => (
|
||||
<option key={opt.key} value={opt.value}>
|
||||
|
|
|
|||
Loading…
Reference in a new issue