From f17b83fb95d506ec3b62ae5cb615ebc6f00821e7 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sat, 29 Nov 2025 19:22:13 +0100 Subject: [PATCH] feat: add custom date range selection to statistics page, enhancing transaction filtering capabilities and user experience --- app/statistics/page.tsx | 196 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 180 insertions(+), 16 deletions(-) diff --git a/app/statistics/page.tsx b/app/statistics/page.tsx index b199bf3..863f355 100644 --- a/app/statistics/page.tsx +++ b/app/statistics/page.tsx @@ -23,38 +23,72 @@ import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { CategoryIcon } from "@/components/ui/category-icon"; import { Filter, X, Wallet, CircleSlash, Calendar } from "lucide-react"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { Calendar as CalendarComponent } from "@/components/ui/calendar"; +import { Button } from "@/components/ui/button"; +import { format } from "date-fns"; +import { fr } from "date-fns/locale"; import type { Account, Category } from "@/lib/types"; -type Period = "3months" | "6months" | "12months" | "all"; +type Period = "1month" | "3months" | "6months" | "12months" | "custom" | "all"; export default function StatisticsPage() { const { data, isLoading } = useBankingData(); const [period, setPeriod] = useState("6months"); const [selectedAccounts, setSelectedAccounts] = useState(["all"]); const [selectedCategories, setSelectedCategories] = useState(["all"]); + const [customStartDate, setCustomStartDate] = useState(undefined); + const [customEndDate, setCustomEndDate] = useState(undefined); + const [isCustomDatePickerOpen, setIsCustomDatePickerOpen] = useState(false); // Get start date based on period const startDate = useMemo(() => { const now = new Date(); switch (period) { + case "1month": + return new Date(now.getFullYear(), now.getMonth() - 1, 1); case "3months": return new Date(now.getFullYear(), now.getMonth() - 3, 1); case "6months": return new Date(now.getFullYear(), now.getMonth() - 6, 1); case "12months": return new Date(now.getFullYear(), now.getMonth() - 12, 1); + case "custom": + return customStartDate || new Date(0); default: return new Date(0); } - }, [period]); + }, [period, customStartDate]); + + // Get end date (only for custom period) + const endDate = useMemo(() => { + if (period === "custom" && customEndDate) { + return customEndDate; + } + return undefined; + }, [period, customEndDate]); // Transactions filtered for account filter (by categories, period - not accounts) const transactionsForAccountFilter = useMemo(() => { if (!data) return []; - return data.transactions.filter( - (t) => new Date(t.date) >= startDate - ).filter((t) => { + return data.transactions.filter((t) => { + const transactionDate = new Date(t.date); + if (endDate) { + // Custom date range + const endOfDay = new Date(endDate); + endOfDay.setHours(23, 59, 59, 999); + if (transactionDate < startDate || transactionDate > endOfDay) { + return false; + } + } else { + // Standard period + if (transactionDate < startDate) { + return false; + } + } + return true; + }).filter((t) => { if (!selectedCategories.includes("all")) { if (selectedCategories.includes("uncategorized")) { return !t.categoryId; @@ -64,28 +98,51 @@ export default function StatisticsPage() { } return true; }); - }, [data, startDate, selectedCategories]); + }, [data, startDate, endDate, selectedCategories]); // Transactions filtered for category filter (by accounts, period - not categories) const transactionsForCategoryFilter = useMemo(() => { if (!data) return []; - return data.transactions.filter( - (t) => new Date(t.date) >= startDate - ).filter((t) => { + return data.transactions.filter((t) => { + const transactionDate = new Date(t.date); + if (endDate) { + // Custom date range + const endOfDay = new Date(endDate); + endOfDay.setHours(23, 59, 59, 999); + if (transactionDate < startDate || transactionDate > endOfDay) { + return false; + } + } else { + // Standard period + if (transactionDate < startDate) { + return false; + } + } + return true; + }).filter((t) => { if (!selectedAccounts.includes("all")) { return selectedAccounts.includes(t.accountId); } return true; }); - }, [data, startDate, selectedAccounts]); + }, [data, startDate, endDate, selectedAccounts]); const stats = useMemo(() => { if (!data) return null; - let transactions = data.transactions.filter( - (t) => new Date(t.date) >= startDate - ); + let transactions = data.transactions.filter((t) => { + const transactionDate = new Date(t.date); + if (endDate) { + // Custom date range + const endOfDay = new Date(endDate); + endOfDay.setHours(23, 59, 59, 999); + return transactionDate >= startDate && transactionDate <= endOfDay; + } else { + // Standard period + return transactionDate >= startDate; + } + }); // Filter by accounts if (!selectedAccounts.includes("all")) { @@ -256,7 +313,7 @@ export default function StatisticsPage() { perAccountBalanceData, transactionCount: transactions.length, }; - }, [data, startDate, selectedAccounts, selectedCategories]); + }, [data, startDate, endDate, selectedAccounts, selectedCategories]); const formatCurrency = (amount: number) => { return new Intl.NumberFormat("fr-FR", { @@ -298,18 +355,108 @@ export default function StatisticsPage() { + + {period === "custom" && ( + + + + + +
+
+ + { + setCustomStartDate(date); + if (date && customEndDate && date > customEndDate) { + setCustomEndDate(undefined); + } + }} + locale={fr} + /> +
+
+ + { + if (date && customStartDate && date < customStartDate) { + return; + } + setCustomEndDate(date); + if (date && customStartDate) { + setIsCustomDatePickerOpen(false); + } + }} + disabled={(date) => { + if (!customStartDate) return true; + return date < customStartDate; + }} + locale={fr} + /> +
+ {customStartDate && customEndDate && ( +
+ + +
+ )} +
+
+
+ )} setSelectedCategories(["all"])} period={period} - onClearPeriod={() => setPeriod("all")} + onClearPeriod={() => { + setPeriod("all"); + setCustomStartDate(undefined); + setCustomEndDate(undefined); + }} accounts={data.accounts} categories={data.categories} + customStartDate={customStartDate} + customEndDate={customEndDate} /> @@ -376,6 +529,8 @@ function ActiveFilters({ onClearPeriod, accounts, categories, + customStartDate, + customEndDate, }: { selectedAccounts: string[]; onRemoveAccount: (id: string) => void; @@ -387,6 +542,8 @@ function ActiveFilters({ onClearPeriod: () => void; accounts: Account[]; categories: Category[]; + customStartDate?: Date; + customEndDate?: Date; }) { const hasAccounts = !selectedAccounts.includes("all"); const hasCategories = !selectedCategories.includes("all"); @@ -402,12 +559,19 @@ function ActiveFilters({ const getPeriodLabel = (p: Period) => { switch (p) { + case "1month": + return "1 mois"; case "3months": return "3 mois"; case "6months": return "6 mois"; case "12months": return "12 mois"; + case "custom": + if (customStartDate && customEndDate) { + return `${format(customStartDate, "d MMM", { locale: fr })} - ${format(customEndDate, "d MMM yyyy", { locale: fr })}`; + } + return "Personnalisé"; default: return "Tout"; }