diff --git a/CHANGELOG.fr.md b/CHANGELOG.fr.md index 2351156..bad30ca 100644 --- a/CHANGELOG.fr.md +++ b/CHANGELOG.fr.md @@ -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é diff --git a/CHANGELOG.md b/CHANGELOG.md index a1c9954..bd8b72b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/hooks/useDashboard.ts b/src/hooks/useDashboard.ts index 00dccd1..1611d98 100644 --- a/src/hooks/useDashboard.ts +++ b/src/hooks/useDashboard.ts @@ -12,6 +12,7 @@ import { } from "../services/dashboardService"; import { getCategoryOverTime } from "../services/reportService"; import { getBudgetVsActualData } from "../services/budgetService"; +import { computeDateRange } from "../utils/dateRange"; interface DashboardState { summary: DashboardSummary; @@ -19,6 +20,8 @@ interface DashboardState { categoryOverTime: CategoryOverTimeData; budgetVsActual: BudgetVsActualRow[]; period: DashboardPeriod; + budgetYear: number; + budgetMonth: number; customDateFrom: string; customDateTo: string; isLoading: boolean; @@ -38,6 +41,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 +54,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 +79,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: @@ -80,68 +88,28 @@ function reducer(state: DashboardState, action: DashboardAction): DashboardState } } -function computeDateRange( - period: DashboardPeriod, - customDateFrom?: string, - customDateTo?: string, -): { dateFrom?: string; dateTo?: string } { - if (period === "all") return {}; - if (period === "custom" && customDateFrom && customDateTo) { - return { dateFrom: customDateFrom, dateTo: customDateTo }; - } - - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth(); - const day = now.getDate(); - - const dateTo = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; - - let from: Date; - switch (period) { - case "month": - from = new Date(year, month, 1); - break; - case "3months": - from = new Date(year, month - 2, 1); - break; - case "6months": - from = new Date(year, month - 5, 1); - break; - case "year": - from = new Date(year, 0, 1); - break; - case "12months": - from = new Date(year, month - 11, 1); - break; - default: - from = new Date(year, month, 1); - break; - } - - const dateFrom = `${from.getFullYear()}-${String(from.getMonth() + 1).padStart(2, "0")}-${String(from.getDate()).padStart(2, "0")}`; - - return { dateFrom, dateTo }; -} - 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 | undefined, + customTo: string | undefined, + 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 +124,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 +135,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 }; } diff --git a/src/hooks/useReports.ts b/src/hooks/useReports.ts index 0cbfc5a..b13f101 100644 --- a/src/hooks/useReports.ts +++ b/src/hooks/useReports.ts @@ -12,6 +12,7 @@ import type { import { getMonthlyTrends, getCategoryOverTime, getDynamicReportData } from "../services/reportService"; import { getExpensesByCategory } from "../services/dashboardService"; import { getBudgetVsActualData } from "../services/budgetService"; +import { computeDateRange } from "../utils/dateRange"; interface ReportsState { tab: ReportTab; @@ -101,50 +102,6 @@ function reducer(state: ReportsState, action: ReportsAction): ReportsState { } } -function computeDateRange( - period: DashboardPeriod, - customDateFrom?: string, - customDateTo?: string, -): { dateFrom?: string; dateTo?: string } { - if (period === "all") return {}; - if (period === "custom" && customDateFrom && customDateTo) { - return { dateFrom: customDateFrom, dateTo: customDateTo }; - } - - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth(); - const day = now.getDate(); - - const dateTo = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; - - let from: Date; - switch (period) { - case "month": - from = new Date(year, month, 1); - break; - case "3months": - from = new Date(year, month - 2, 1); - break; - case "6months": - from = new Date(year, month - 5, 1); - break; - case "year": - from = new Date(year, 0, 1); - break; - case "12months": - from = new Date(year, month - 11, 1); - break; - default: - from = new Date(year, month, 1); - break; - } - - const dateFrom = `${from.getFullYear()}-${String(from.getMonth() + 1).padStart(2, "0")}-${String(from.getDate()).padStart(2, "0")}`; - - return { dateFrom, dateTo }; -} - export function useReports() { const [state, dispatch] = useReducer(reducer, initialState); const fetchIdRef = useRef(0); diff --git a/src/pages/DashboardPage.tsx b/src/pages/DashboardPage.tsx index d3b1fd0..e5aae8c 100644 --- a/src/pages/DashboardPage.tsx +++ b/src/pages/DashboardPage.tsx @@ -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"; @@ -8,40 +8,14 @@ import CategoryPieChart from "../components/dashboard/CategoryPieChart"; import CategoryOverTimeChart from "../components/reports/CategoryOverTimeChart"; import BudgetVsActualTable from "../components/reports/BudgetVsActualTable"; import TransactionDetailModal from "../components/shared/TransactionDetailModal"; -import type { CategoryBreakdownItem, DashboardPeriod } from "../shared/types"; +import type { CategoryBreakdownItem } from "../shared/types"; +import { computeDateRange, buildMonthOptions } from "../utils/dateRange"; const fmt = new Intl.NumberFormat("en-CA", { style: "currency", currency: "CAD" }); -function computeDateRange( - period: DashboardPeriod, - customDateFrom?: string, - customDateTo?: string, -): { dateFrom?: string; dateTo?: string } { - if (period === "all") return {}; - if (period === "custom" && customDateFrom && customDateTo) { - return { dateFrom: customDateFrom, dateTo: customDateTo }; - } - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth(); - const day = now.getDate(); - const dateTo = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; - let from: Date; - switch (period) { - case "month": from = new Date(year, month, 1); break; - case "3months": from = new Date(year, month - 2, 1); break; - case "6months": from = new Date(year, month - 5, 1); break; - case "year": from = new Date(year, 0, 1); break; - case "12months": from = new Date(year, month - 11, 1); break; - default: from = new Date(year, month, 1); break; - } - const dateFrom = `${from.getFullYear()}-${String(from.getMonth() + 1).padStart(2, "0")}-${String(from.getDate()).padStart(2, "0")}`; - return { dateFrom, dateTo }; -} - 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>(new Set()); @@ -91,6 +65,8 @@ export default function DashboardPage() { }, ]; + const monthOptions = useMemo(() => buildMonthOptions(i18n.language), [i18n.language]); + const { dateFrom, dateTo } = computeDateRange(period, state.customDateFrom, state.customDateTo); return ( @@ -138,7 +114,23 @@ export default function DashboardPage() { />
-

{t("dashboard.budgetVsActual")}

+

+ {t("reports.bva.titlePrefix")} + +

diff --git a/src/pages/ReportsPage.tsx b/src/pages/ReportsPage.tsx index 0102b19..da975bd 100644 --- a/src/pages/ReportsPage.tsx +++ b/src/pages/ReportsPage.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { Hash, Table, BarChart3 } from "lucide-react"; import { useReports } from "../hooks/useReports"; import { PageHelp } from "../components/shared/PageHelp"; -import type { ReportTab, CategoryBreakdownItem, DashboardPeriod, ImportSource } from "../shared/types"; +import type { ReportTab, CategoryBreakdownItem, ImportSource } from "../shared/types"; import { getAllSources } from "../services/importSourceService"; import PeriodSelector from "../components/dashboard/PeriodSelector"; import MonthlyTrendsChart from "../components/reports/MonthlyTrendsChart"; @@ -16,36 +16,10 @@ import BudgetVsActualTable from "../components/reports/BudgetVsActualTable"; import DynamicReport from "../components/reports/DynamicReport"; import ReportFilterPanel from "../components/reports/ReportFilterPanel"; import TransactionDetailModal from "../components/shared/TransactionDetailModal"; +import { computeDateRange, buildMonthOptions } from "../utils/dateRange"; const TABS: ReportTab[] = ["trends", "byCategory", "overTime", "budgetVsActual", "dynamic"]; -function computeDateRange( - period: DashboardPeriod, - customDateFrom?: string, - customDateTo?: string, -): { dateFrom?: string; dateTo?: string } { - if (period === "all") return {}; - if (period === "custom" && customDateFrom && customDateTo) { - return { dateFrom: customDateFrom, dateTo: customDateTo }; - } - const now = new Date(); - const year = now.getFullYear(); - const month = now.getMonth(); - const day = now.getDate(); - const dateTo = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart(2, "0")}`; - let from: Date; - switch (period) { - case "month": from = new Date(year, month, 1); break; - case "3months": from = new Date(year, month - 2, 1); break; - case "6months": from = new Date(year, month - 5, 1); break; - case "year": from = new Date(year, 0, 1); break; - case "12months": from = new Date(year, month - 11, 1); break; - default: from = new Date(year, month, 1); break; - } - const dateFrom = `${from.getFullYear()}-${String(from.getMonth() + 1).padStart(2, "0")}-${String(from.getDate()).padStart(2, "0")}`; - return { dateFrom, dateTo }; -} - export default function ReportsPage() { const { t, i18n } = useTranslation(); const { state, setTab, setPeriod, setCustomDates, setBudgetMonth, setPivotConfig, setSourceId } = useReports(); @@ -92,18 +66,7 @@ export default function ReportsPage() { return []; }, [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 monthOptions = useMemo(() => buildMonthOptions(i18n.language), [i18n.language]); const hasCategories = ["byCategory", "overTime"].includes(state.tab) && filterCategories.length > 0; const showFilterPanel = hasCategories || (state.tab === "trends" && sources.length > 1); @@ -121,7 +84,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) => (