Merge pull request 'feat: add month dropdown to dashboard Budget vs Actual (#31)' (#32) from fix/simpl-resultat-31-dashboard-month-dropdown into main
This commit is contained in:
commit
f8b44ebb6e
7 changed files with 130 additions and 168 deletions
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
## [Non publié]
|
## [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]
|
## [0.6.4]
|
||||||
|
|
||||||
### Ajouté
|
### Ajouté
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,12 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [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]
|
## [0.6.4]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
} from "../services/dashboardService";
|
} from "../services/dashboardService";
|
||||||
import { getCategoryOverTime } from "../services/reportService";
|
import { getCategoryOverTime } from "../services/reportService";
|
||||||
import { getBudgetVsActualData } from "../services/budgetService";
|
import { getBudgetVsActualData } from "../services/budgetService";
|
||||||
|
import { computeDateRange } from "../utils/dateRange";
|
||||||
|
|
||||||
interface DashboardState {
|
interface DashboardState {
|
||||||
summary: DashboardSummary;
|
summary: DashboardSummary;
|
||||||
|
|
@ -19,6 +20,8 @@ interface DashboardState {
|
||||||
categoryOverTime: CategoryOverTimeData;
|
categoryOverTime: CategoryOverTimeData;
|
||||||
budgetVsActual: BudgetVsActualRow[];
|
budgetVsActual: BudgetVsActualRow[];
|
||||||
period: DashboardPeriod;
|
period: DashboardPeriod;
|
||||||
|
budgetYear: number;
|
||||||
|
budgetMonth: number;
|
||||||
customDateFrom: string;
|
customDateFrom: string;
|
||||||
customDateTo: string;
|
customDateTo: string;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
|
@ -38,6 +41,7 @@ type DashboardAction =
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
| { type: "SET_PERIOD"; payload: DashboardPeriod }
|
| { type: "SET_PERIOD"; payload: DashboardPeriod }
|
||||||
|
| { type: "SET_BUDGET_MONTH"; payload: { year: number; month: number } }
|
||||||
| { type: "SET_CUSTOM_DATES"; payload: { dateFrom: string; dateTo: string } };
|
| { type: "SET_CUSTOM_DATES"; payload: { dateFrom: string; dateTo: string } };
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
@ -50,6 +54,8 @@ const initialState: DashboardState = {
|
||||||
categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {} },
|
categoryOverTime: { categories: [], data: [], colors: {}, categoryIds: {} },
|
||||||
budgetVsActual: [],
|
budgetVsActual: [],
|
||||||
period: "year",
|
period: "year",
|
||||||
|
budgetYear: now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(),
|
||||||
|
budgetMonth: now.getMonth() === 0 ? 12 : now.getMonth(),
|
||||||
customDateFrom: yearStartStr,
|
customDateFrom: yearStartStr,
|
||||||
customDateTo: todayStr,
|
customDateTo: todayStr,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
|
@ -73,6 +79,8 @@ function reducer(state: DashboardState, action: DashboardAction): DashboardState
|
||||||
};
|
};
|
||||||
case "SET_PERIOD":
|
case "SET_PERIOD":
|
||||||
return { ...state, period: action.payload };
|
return { ...state, period: action.payload };
|
||||||
|
case "SET_BUDGET_MONTH":
|
||||||
|
return { ...state, budgetYear: action.payload.year, budgetMonth: action.payload.month };
|
||||||
case "SET_CUSTOM_DATES":
|
case "SET_CUSTOM_DATES":
|
||||||
return { ...state, period: "custom" as DashboardPeriod, customDateFrom: action.payload.dateFrom, customDateTo: action.payload.dateTo };
|
return { ...state, period: "custom" as DashboardPeriod, customDateFrom: action.payload.dateFrom, customDateTo: action.payload.dateTo };
|
||||||
default:
|
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() {
|
export function useDashboard() {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const fetchIdRef = useRef(0);
|
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;
|
const fetchId = ++fetchIdRef.current;
|
||||||
dispatch({ type: "SET_LOADING", payload: true });
|
dispatch({ type: "SET_LOADING", payload: true });
|
||||||
dispatch({ type: "SET_ERROR", payload: null });
|
dispatch({ type: "SET_ERROR", payload: null });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { dateFrom, dateTo } = computeDateRange(period, customFrom, customTo);
|
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([
|
const [summary, categoryBreakdown, categoryOverTime, budgetVsActual] = await Promise.all([
|
||||||
getDashboardSummary(dateFrom, dateTo),
|
getDashboardSummary(dateFrom, dateTo),
|
||||||
getExpensesByCategory(dateFrom, dateTo),
|
getExpensesByCategory(dateFrom, dateTo),
|
||||||
getCategoryOverTime(dateFrom, dateTo),
|
getCategoryOverTime(dateFrom, dateTo),
|
||||||
getBudgetVsActualData(currentYear, currentMonth),
|
getBudgetVsActualData(bYear, bMonth),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (fetchId !== fetchIdRef.current) return;
|
if (fetchId !== fetchIdRef.current) return;
|
||||||
|
|
@ -156,8 +124,8 @@ export function useDashboard() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData(state.period, state.customDateFrom, state.customDateTo);
|
fetchData(state.period, state.customDateFrom, state.customDateTo, state.budgetYear, state.budgetMonth);
|
||||||
}, [state.period, state.customDateFrom, state.customDateTo, fetchData]);
|
}, [state.period, state.customDateFrom, state.customDateTo, state.budgetYear, state.budgetMonth, fetchData]);
|
||||||
|
|
||||||
const setPeriod = useCallback((period: DashboardPeriod) => {
|
const setPeriod = useCallback((period: DashboardPeriod) => {
|
||||||
dispatch({ type: "SET_PERIOD", payload: period });
|
dispatch({ type: "SET_PERIOD", payload: period });
|
||||||
|
|
@ -167,5 +135,9 @@ export function useDashboard() {
|
||||||
dispatch({ type: "SET_CUSTOM_DATES", payload: { dateFrom, dateTo } });
|
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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import type {
|
||||||
import { getMonthlyTrends, getCategoryOverTime, getDynamicReportData } from "../services/reportService";
|
import { getMonthlyTrends, getCategoryOverTime, getDynamicReportData } from "../services/reportService";
|
||||||
import { getExpensesByCategory } from "../services/dashboardService";
|
import { getExpensesByCategory } from "../services/dashboardService";
|
||||||
import { getBudgetVsActualData } from "../services/budgetService";
|
import { getBudgetVsActualData } from "../services/budgetService";
|
||||||
|
import { computeDateRange } from "../utils/dateRange";
|
||||||
|
|
||||||
interface ReportsState {
|
interface ReportsState {
|
||||||
tab: ReportTab;
|
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() {
|
export function useReports() {
|
||||||
const [state, dispatch] = useReducer(reducer, initialState);
|
const [state, dispatch] = useReducer(reducer, initialState);
|
||||||
const fetchIdRef = useRef(0);
|
const fetchIdRef = useRef(0);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useCallback } from "react";
|
import { useState, useCallback, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Wallet, TrendingUp, TrendingDown } from "lucide-react";
|
import { Wallet, TrendingUp, TrendingDown } from "lucide-react";
|
||||||
import { useDashboard } from "../hooks/useDashboard";
|
import { useDashboard } from "../hooks/useDashboard";
|
||||||
|
|
@ -8,40 +8,14 @@ import CategoryPieChart from "../components/dashboard/CategoryPieChart";
|
||||||
import CategoryOverTimeChart from "../components/reports/CategoryOverTimeChart";
|
import CategoryOverTimeChart from "../components/reports/CategoryOverTimeChart";
|
||||||
import BudgetVsActualTable from "../components/reports/BudgetVsActualTable";
|
import BudgetVsActualTable from "../components/reports/BudgetVsActualTable";
|
||||||
import TransactionDetailModal from "../components/shared/TransactionDetailModal";
|
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" });
|
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() {
|
export default function DashboardPage() {
|
||||||
const { t } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { state, setPeriod, setCustomDates } = useDashboard();
|
const { state, setPeriod, setCustomDates, setBudgetMonth } = useDashboard();
|
||||||
const { summary, categoryBreakdown, categoryOverTime, budgetVsActual, period, isLoading } = state;
|
const { summary, categoryBreakdown, categoryOverTime, budgetVsActual, period, isLoading } = state;
|
||||||
|
|
||||||
const [hiddenCategories, setHiddenCategories] = useState<Set<string>>(new Set());
|
const [hiddenCategories, setHiddenCategories] = useState<Set<string>>(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);
|
const { dateFrom, dateTo } = computeDateRange(period, state.customDateFrom, state.customDateTo);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -138,7 +114,23 @@ export default function DashboardPage() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="lg:col-span-3">
|
<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} />
|
<BudgetVsActualTable data={budgetVsActual} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||||
import { Hash, Table, BarChart3 } from "lucide-react";
|
import { Hash, Table, BarChart3 } from "lucide-react";
|
||||||
import { useReports } from "../hooks/useReports";
|
import { useReports } from "../hooks/useReports";
|
||||||
import { PageHelp } from "../components/shared/PageHelp";
|
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 { getAllSources } from "../services/importSourceService";
|
||||||
import PeriodSelector from "../components/dashboard/PeriodSelector";
|
import PeriodSelector from "../components/dashboard/PeriodSelector";
|
||||||
import MonthlyTrendsChart from "../components/reports/MonthlyTrendsChart";
|
import MonthlyTrendsChart from "../components/reports/MonthlyTrendsChart";
|
||||||
|
|
@ -16,36 +16,10 @@ import BudgetVsActualTable from "../components/reports/BudgetVsActualTable";
|
||||||
import DynamicReport from "../components/reports/DynamicReport";
|
import DynamicReport from "../components/reports/DynamicReport";
|
||||||
import ReportFilterPanel from "../components/reports/ReportFilterPanel";
|
import ReportFilterPanel from "../components/reports/ReportFilterPanel";
|
||||||
import TransactionDetailModal from "../components/shared/TransactionDetailModal";
|
import TransactionDetailModal from "../components/shared/TransactionDetailModal";
|
||||||
|
import { computeDateRange, buildMonthOptions } from "../utils/dateRange";
|
||||||
|
|
||||||
const TABS: ReportTab[] = ["trends", "byCategory", "overTime", "budgetVsActual", "dynamic"];
|
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() {
|
export default function ReportsPage() {
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const { state, setTab, setPeriod, setCustomDates, setBudgetMonth, setPivotConfig, setSourceId } = useReports();
|
const { state, setTab, setPeriod, setCustomDates, setBudgetMonth, setPivotConfig, setSourceId } = useReports();
|
||||||
|
|
@ -92,18 +66,7 @@ export default function ReportsPage() {
|
||||||
return [];
|
return [];
|
||||||
}, [state.tab, state.categorySpending, state.categoryOverTime]);
|
}, [state.tab, state.categorySpending, state.categoryOverTime]);
|
||||||
|
|
||||||
const monthOptions = useMemo(() => {
|
const monthOptions = useMemo(() => buildMonthOptions(i18n.language), [i18n.language]);
|
||||||
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);
|
||||||
|
|
@ -121,7 +84,7 @@ export default function ReportsPage() {
|
||||||
const [y, m] = e.target.value.split("-").map(Number);
|
const [y, m] = e.target.value.split("-").map(Number);
|
||||||
setBudgetMonth(y, m);
|
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) => (
|
{monthOptions.map((opt) => (
|
||||||
<option key={opt.key} value={opt.value}>
|
<option key={opt.key} value={opt.value}>
|
||||||
|
|
|
||||||
66
src/utils/dateRange.ts
Normal file
66
src/utils/dateRange.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
import type { DashboardPeriod } from "../shared/types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a date range (dateFrom / dateTo) based on the selected period.
|
||||||
|
* Shared between useDashboard, useReports, DashboardPage and ReportsPage.
|
||||||
|
*/
|
||||||
|
export 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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build an array of month options for the budget month dropdown.
|
||||||
|
* Returns the last 24 months with localized labels.
|
||||||
|
*/
|
||||||
|
export function buildMonthOptions(language: string): Array<{ key: string; value: string; label: string }> {
|
||||||
|
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(language, { month: "long", year: "numeric" }).format(d);
|
||||||
|
return { key: `${y}-${m}`, value: `${y}-${m}`, label: label.charAt(0).toUpperCase() + label.slice(1) };
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue