diff --git a/app/statistics/page.tsx b/app/statistics/page.tsx index 0e022cf..037ec05 100644 --- a/app/statistics/page.tsx +++ b/app/statistics/page.tsx @@ -34,6 +34,13 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; import { Calendar as CalendarComponent } from "@/components/ui/calendar"; import { Button } from "@/components/ui/button"; import { format } from "date-fns"; @@ -45,6 +52,8 @@ type Period = "1month" | "3months" | "6months" | "12months" | "custom" | "all"; export default function StatisticsPage() { const { data, isLoading } = useBankingData(); + const isMobile = useIsMobile(); + const [sheetOpen, setSheetOpen] = useState(false); const [period, setPeriod] = useState("6months"); const [selectedAccounts, setSelectedAccounts] = useState(["all"]); const [selectedCategories, setSelectedCategories] = useState([ @@ -53,10 +62,10 @@ export default function StatisticsPage() { const [excludeInternalTransfers, setExcludeInternalTransfers] = useState(true); const [customStartDate, setCustomStartDate] = useState( - undefined, + undefined ); const [customEndDate, setCustomEndDate] = useState( - undefined, + undefined ); const [isCustomDatePickerOpen, setIsCustomDatePickerOpen] = useState(false); @@ -91,7 +100,7 @@ export default function StatisticsPage() { const internalTransferCategory = useMemo(() => { if (!data) return null; return data.categories.find( - (c) => c.name.toLowerCase() === "virement interne", + (c) => c.name.toLowerCase() === "virement interne" ); }, [data]); @@ -207,7 +216,7 @@ export default function StatisticsPage() { // Filter by accounts if (!selectedAccounts.includes("all")) { transactions = transactions.filter((t) => - selectedAccounts.includes(t.accountId), + selectedAccounts.includes(t.accountId) ); } @@ -217,7 +226,7 @@ export default function StatisticsPage() { transactions = transactions.filter((t) => !t.categoryId); } else { transactions = transactions.filter( - (t) => t.categoryId && selectedCategories.includes(t.categoryId), + (t) => t.categoryId && selectedCategories.includes(t.categoryId) ); } } @@ -225,7 +234,7 @@ export default function StatisticsPage() { // Exclude "Virement interne" category if checkbox is checked if (excludeInternalTransfers && internalTransferCategory) { transactions = transactions.filter( - (t) => t.categoryId !== internalTransferCategory.id, + (t) => t.categoryId !== internalTransferCategory.id ); } @@ -297,7 +306,7 @@ export default function StatisticsPage() { }); const categoryChartDataByParent = Array.from( - categoryTotalsByParent.entries(), + categoryTotalsByParent.entries() ) .map(([groupId, total]) => { const category = data.categories.find((c) => c.id === groupId); @@ -312,7 +321,7 @@ export default function StatisticsPage() { // Top expenses - deduplicate by ID and sort by amount (most negative first) const uniqueTransactions = Array.from( - new Map(transactions.map((t) => [t.id, t])).values(), + new Map(transactions.map((t) => [t.id, t])).values() ); const topExpenses = uniqueTransactions .filter((t) => t.amount < 0) @@ -338,7 +347,7 @@ export default function StatisticsPage() { // Balance evolution - Aggregated (using filtered transactions) const sortedFilteredTransactions = [...transactions].sort( - (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(), + (a, b) => new Date(a.date).getTime() - new Date(b.date).getTime() ); // Calculate starting balance: initialBalance + transactions before startDate @@ -350,7 +359,7 @@ export default function StatisticsPage() { // Start with initial balances runningBalance = accountsToUse.reduce( (sum, acc) => sum + (acc.initialBalance || 0), - 0, + 0 ); // Add all transactions before the start date for these accounts @@ -387,7 +396,7 @@ export default function StatisticsPage() { }); const aggregatedBalanceData = Array.from( - aggregatedBalanceByDate.entries(), + aggregatedBalanceByDate.entries() ).map(([date, balance]) => ({ date: new Date(date).toLocaleDateString("fr-FR", { day: "2-digit", @@ -643,170 +652,208 @@ export default function StatisticsPage() { description="Analysez vos dépenses et revenus" /> - - -
- - - - - - - {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 && ( -
- - -
- )} -
-
-
- )} - - {internalTransferCategory && ( -
- - setExcludeInternalTransfers(checked === true) - } + + Filtres + +
+ - -
- )} -
+ + + + + {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 && ( +
+ + +
+ )} +
+
+
+ )} + + {internalTransferCategory && ( +
+ + setExcludeInternalTransfers(checked === true) + } + /> + +
+ )} +
+ + { const newAccounts = selectedAccounts.filter((a) => a !== id); setSelectedAccounts( - newAccounts.length > 0 ? newAccounts : ["all"], + newAccounts.length > 0 ? newAccounts : ["all"] ); }} onClearAccounts={() => setSelectedAccounts(["all"])} @@ -814,7 +861,7 @@ export default function StatisticsPage() { onRemoveCategory={(id) => { const newCategories = selectedCategories.filter((c) => c !== id); setSelectedCategories( - newCategories.length > 0 ? newCategories : ["all"], + newCategories.length > 0 ? newCategories : ["all"] ); }} onClearCategories={() => setSelectedCategories(["all"])} @@ -829,8 +876,201 @@ export default function StatisticsPage() { customStartDate={customStartDate} customEndDate={customEndDate} /> -
-
+ + ) : ( + + +
+ + + + + + + {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 && ( +
+ + +
+ )} +
+
+
+ )} + + {internalTransferCategory && ( +
+ + setExcludeInternalTransfers(checked === true) + } + /> + +
+ )} +
+ + { + const newAccounts = selectedAccounts.filter((a) => a !== id); + setSelectedAccounts( + newAccounts.length > 0 ? newAccounts : ["all"] + ); + }} + onClearAccounts={() => setSelectedAccounts(["all"])} + selectedCategories={selectedCategories} + onRemoveCategory={(id) => { + const newCategories = selectedCategories.filter( + (c) => c !== id + ); + setSelectedCategories( + newCategories.length > 0 ? newCategories : ["all"] + ); + }} + onClearCategories={() => setSelectedCategories(["all"])} + period={period} + onClearPeriod={() => { + setPeriod("all"); + setCustomStartDate(undefined); + setCustomEndDate(undefined); + }} + accounts={data.accounts} + categories={data.categories} + customStartDate={customStartDate} + customEndDate={customEndDate} + /> +
+
+ )} {/* Vue d'ensemble */}
@@ -963,7 +1203,7 @@ function ActiveFilters({ const selectedAccs = accounts.filter((a) => selectedAccounts.includes(a.id)); const selectedCats = categories.filter((c) => - selectedCategories.includes(c.id), + selectedCategories.includes(c.id) ); const isUncategorized = selectedCategories.includes("uncategorized"); diff --git a/components/transactions/transaction-filters.tsx b/components/transactions/transaction-filters.tsx index c34b897..cf28ba5 100644 --- a/components/transactions/transaction-filters.tsx +++ b/components/transactions/transaction-filters.tsx @@ -1,5 +1,6 @@ "use client"; +import { useState } from "react"; import { Card, CardContent } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Badge } from "@/components/ui/badge"; @@ -20,9 +21,17 @@ import { } from "@/components/ui/popover"; import { Calendar as CalendarComponent } from "@/components/ui/calendar"; import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; import { Search, X, Filter, Wallet, Calendar } from "lucide-react"; import { format } from "date-fns"; import { fr } from "date-fns/locale"; +import { useIsMobile } from "@/hooks/use-mobile"; import type { Account, Category, Folder, Transaction } from "@/lib/types"; type Period = "1month" | "3months" | "6months" | "12months" | "custom" | "all"; @@ -74,195 +83,238 @@ export function TransactionFilters({ transactionsForAccountFilter, transactionsForCategoryFilter, }: TransactionFiltersProps) { - return ( - - -
-
-
- - onSearchChange(e.target.value)} - className="pl-9" - /> -
+ const isMobile = useIsMobile(); + const [sheetOpen, setSheetOpen] = useState(false); + + const filtersContent = ( + <> +
+
+
+ + onSearchChange(e.target.value)} + className="pl-9" + />
- - - - - - - - - - {period === "custom" && ( - - - - - -
-
- - { - onCustomStartDateChange(date); - if (date && customEndDate && date > customEndDate) { - onCustomEndDateChange(undefined); - } - }} - locale={fr} - /> -
-
- - { - if (date && customStartDate && date < customStartDate) { - return; - } - onCustomEndDateChange(date); - if (date && customStartDate) { - onCustomDatePickerOpenChange(false); - } - }} - disabled={(date) => { - if (!customStartDate) return true; - return date < customStartDate; - }} - locale={fr} - /> -
- {customStartDate && customEndDate && ( -
- - -
- )} -
-
-
- )}
- onSearchChange("")} - selectedAccounts={selectedAccounts} - onRemoveAccount={(id) => { - const newAccounts = selectedAccounts.filter((a) => a !== id); - onAccountsChange(newAccounts.length > 0 ? newAccounts : ["all"]); - }} - onClearAccounts={() => onAccountsChange(["all"])} - selectedCategories={selectedCategories} - onRemoveCategory={(id) => { - const newCategories = selectedCategories.filter((c) => c !== id); - onCategoriesChange( - newCategories.length > 0 ? newCategories : ["all"], - ); - }} - onClearCategories={() => onCategoriesChange(["all"])} - showReconciled={showReconciled} - onClearReconciled={() => onReconciledChange("all")} - period={period} - onClearPeriod={() => { - onPeriodChange("all"); - onCustomStartDateChange(undefined); - onCustomEndDateChange(undefined); - }} - customStartDate={customStartDate} - customEndDate={customEndDate} + - + + + + + + + + {period === "custom" && ( + + + + + +
+
+ + { + onCustomStartDateChange(date); + if (date && customEndDate && date > customEndDate) { + onCustomEndDateChange(undefined); + } + }} + locale={fr} + /> +
+
+ + { + if (date && customStartDate && date < customStartDate) { + return; + } + onCustomEndDateChange(date); + if (date && customStartDate) { + onCustomDatePickerOpenChange(false); + } + }} + disabled={(date) => { + if (!customStartDate) return true; + return date < customStartDate; + }} + locale={fr} + /> +
+ {customStartDate && customEndDate && ( +
+ + +
+ )} +
+
+
+ )} +
+ + onSearchChange("")} + selectedAccounts={selectedAccounts} + onRemoveAccount={(id) => { + const newAccounts = selectedAccounts.filter((a) => a !== id); + onAccountsChange(newAccounts.length > 0 ? newAccounts : ["all"]); + }} + onClearAccounts={() => onAccountsChange(["all"])} + selectedCategories={selectedCategories} + onRemoveCategory={(id) => { + const newCategories = selectedCategories.filter((c) => c !== id); + onCategoriesChange( + newCategories.length > 0 ? newCategories : ["all"] + ); + }} + onClearCategories={() => onCategoriesChange(["all"])} + showReconciled={showReconciled} + onClearReconciled={() => onReconciledChange("all")} + period={period} + onClearPeriod={() => { + onPeriodChange("all"); + onCustomStartDateChange(undefined); + onCustomEndDateChange(undefined); + }} + customStartDate={customStartDate} + customEndDate={customEndDate} + accounts={accounts} + categories={categories} + /> + + ); + + if (isMobile) { + const activeFiltersCount = + (searchQuery.trim() !== "" ? 1 : 0) + + (!selectedAccounts.includes("all") ? selectedAccounts.length : 0) + + (!selectedCategories.includes("all") ? selectedCategories.length : 0) + + (showReconciled !== "all" ? 1 : 0) + + (period !== "all" ? 1 : 0); + + return ( + <> + + + + + + + Filtres + +
{filtersContent}
+
+
+ + ); + } + + return ( + + {filtersContent} ); } @@ -315,7 +367,7 @@ function ActiveFilters({ const selectedAccs = accounts.filter((a) => selectedAccounts.includes(a.id)); const selectedCats = categories.filter((c) => - selectedCategories.includes(c.id), + selectedCategories.includes(c.id) ); const isUncategorized = selectedCategories.includes("uncategorized"); diff --git a/components/ui/sheet.tsx b/components/ui/sheet.tsx index d30779f..5dac8db 100644 --- a/components/ui/sheet.tsx +++ b/components/ui/sheet.tsx @@ -85,7 +85,7 @@ function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { return (
);