diff --git a/app/statistics/page.tsx b/app/statistics/page.tsx index 0e48d39..b199bf3 100644 --- a/app/statistics/page.tsx +++ b/app/statistics/page.tsx @@ -33,25 +33,55 @@ export default function StatisticsPage() { const [selectedAccounts, setSelectedAccounts] = useState(["all"]); const [selectedCategories, setSelectedCategories] = useState(["all"]); - const stats = useMemo(() => { - if (!data) return null; - + // Get start date based on period + const startDate = useMemo(() => { const now = new Date(); - let startDate: Date; - switch (period) { case "3months": - startDate = new Date(now.getFullYear(), now.getMonth() - 3, 1); - break; + return new Date(now.getFullYear(), now.getMonth() - 3, 1); case "6months": - startDate = new Date(now.getFullYear(), now.getMonth() - 6, 1); - break; + return new Date(now.getFullYear(), now.getMonth() - 6, 1); case "12months": - startDate = new Date(now.getFullYear(), now.getMonth() - 12, 1); - break; + return new Date(now.getFullYear(), now.getMonth() - 12, 1); default: - startDate = new Date(0); + return new Date(0); } + }, [period]); + + // 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) => { + if (!selectedCategories.includes("all")) { + if (selectedCategories.includes("uncategorized")) { + return !t.categoryId; + } else { + return t.categoryId && selectedCategories.includes(t.categoryId); + } + } + return true; + }); + }, [data, startDate, 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) => { + if (!selectedAccounts.includes("all")) { + return selectedAccounts.includes(t.accountId); + } + return true; + }); + }, [data, startDate, selectedAccounts]); + + const stats = useMemo(() => { + if (!data) return null; let transactions = data.transactions.filter( (t) => new Date(t.date) >= startDate @@ -226,7 +256,7 @@ export default function StatisticsPage() { perAccountBalanceData, transactionCount: transactions.length, }; - }, [data, period, selectedAccounts, selectedCategories]); + }, [data, startDate, selectedAccounts, selectedCategories]); const formatCurrency = (amount: number) => { return new Intl.NumberFormat("fr-FR", { @@ -255,6 +285,7 @@ export default function StatisticsPage() { value={selectedAccounts} onChange={setSelectedAccounts} className="w-[200px]" + filteredTransactions={transactionsForAccountFilter} /> diff --git a/components/ui/account-filter-combobox.tsx b/components/ui/account-filter-combobox.tsx index 9b4dfe8..5b42675 100644 --- a/components/ui/account-filter-combobox.tsx +++ b/components/ui/account-filter-combobox.tsx @@ -18,7 +18,7 @@ import { import { CategoryIcon } from "@/components/ui/category-icon"; import { ChevronsUpDown, Check, Wallet, X } from "lucide-react"; import { cn } from "@/lib/utils"; -import type { Account, Folder } from "@/lib/types"; +import type { Account, Folder, Transaction } from "@/lib/types"; interface AccountFilterComboboxProps { accounts: Account[]; @@ -26,6 +26,7 @@ interface AccountFilterComboboxProps { value: string[]; // ["all"] | [accountId, accountId, ...] onChange: (value: string[]) => void; className?: string; + filteredTransactions?: Transaction[]; // Transactions filtered by other filters (categories, period, etc.) } export function AccountFilterCombobox({ @@ -34,9 +35,31 @@ export function AccountFilterCombobox({ value, onChange, className, + filteredTransactions, }: AccountFilterComboboxProps) { const [open, setOpen] = useState(false); + // Calculate total amount per account based on filtered transactions + const accountTotals = useMemo(() => { + if (!filteredTransactions) return {}; + + const totals: Record = {}; + filteredTransactions.forEach((t) => { + totals[t.accountId] = (totals[t.accountId] || 0) + t.amount; + }); + + return totals; + }, [filteredTransactions]); + + const formatCurrency = (amount: number) => { + return new Intl.NumberFormat("fr-FR", { + style: "currency", + currency: "EUR", + minimumFractionDigits: 0, + maximumFractionDigits: 0, + }).format(amount); + }; + // Get root folders (folders without parent) - same as folders/page.tsx const rootFolders = useMemo( () => folders.filter((f) => f.parentId === null), @@ -166,23 +189,31 @@ export function AccountFilterCombobox({ {/* Accounts in this folder */} - {folderAccounts.map((account) => ( - handleSelect(account.id)} - style={{ paddingLeft: `${paddingLeft + 16}px` }} - > - - {account.name} - { + const total = accountTotals[account.id]; + return ( + handleSelect(account.id)} + style={{ paddingLeft: `${paddingLeft + 16}px` }} + > + + {account.name} + {total !== undefined && ( + + ({formatCurrency(total)}) + )} - /> - - ))} + + + ); + })} {/* Child folders - recursive */} {childFoldersList.map((childFolder) => @@ -251,6 +282,13 @@ export function AccountFilterCombobox({ handleSelect("all")}> Tous les comptes + {filteredTransactions && ( + + ({formatCurrency( + filteredTransactions.reduce((sum, t) => sum + t.amount, 0) + )}) + + )} 0 && ( - {orphanAccounts.map((account) => ( - handleSelect(account.id)} - > - - {account.name} - { + const total = accountTotals[account.id]; + return ( + handleSelect(account.id)} + > + + {account.name} + {total !== undefined && ( + + ({formatCurrency(total)}) + )} - /> - - ))} + + + ); + })} )} diff --git a/components/ui/category-filter-combobox.tsx b/components/ui/category-filter-combobox.tsx index 78026fe..824f906 100644 --- a/components/ui/category-filter-combobox.tsx +++ b/components/ui/category-filter-combobox.tsx @@ -18,13 +18,14 @@ import { import { CategoryIcon } from "@/components/ui/category-icon"; import { ChevronsUpDown, Check, Tags, CircleSlash, X } from "lucide-react"; import { cn } from "@/lib/utils"; -import type { Category } from "@/lib/types"; +import type { Category, Transaction } from "@/lib/types"; interface CategoryFilterComboboxProps { categories: Category[]; value: string[]; // ["all"] | ["uncategorized"] | [categoryId, categoryId, ...] onChange: (value: string[]) => void; className?: string; + filteredTransactions?: Transaction[]; // Transactions filtered by other filters (accounts, period, etc.) } export function CategoryFilterCombobox({ @@ -32,9 +33,23 @@ export function CategoryFilterCombobox({ value, onChange, className, + filteredTransactions, }: CategoryFilterComboboxProps) { const [open, setOpen] = useState(false); + // Calculate transaction counts per category based on filtered transactions + const categoryCounts = useMemo(() => { + if (!filteredTransactions) return {}; + + const counts: Record = {}; + filteredTransactions.forEach((t) => { + const catId = t.categoryId || "uncategorized"; + counts[catId] = (counts[catId] || 0) + 1; + }); + + return counts; + }, [filteredTransactions]); + // Organize categories by parent const { parentCategories, childrenByParent } = useMemo(() => { const parents = categories.filter((c) => c.parentId === null); @@ -178,6 +193,11 @@ export function CategoryFilterCombobox({ handleSelect("all")}> Toutes catégories + {filteredTransactions && ( + + ({filteredTransactions.length}) + + )} Non catégorisé + {categoryCounts["uncategorized"] !== undefined && ( + + ({categoryCounts["uncategorized"]}) + + )} {parent.name} + {categoryCounts[parent.id] !== undefined && ( + + ({categoryCounts[parent.id]}) + + )} {child.name} + {categoryCounts[child.id] !== undefined && ( + + ({categoryCounts[child.id]}) + + )}