diff --git a/app/page.tsx b/app/page.tsx index 29d4365..5fb55d3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,19 +1,47 @@ "use client"; +import { useState, useMemo } from "react"; import { PageLayout, LoadingState, PageHeader } from "@/components/layout"; import { OverviewCards } from "@/components/dashboard/overview-cards"; import { RecentTransactions } from "@/components/dashboard/recent-transactions"; import { AccountsSummary } from "@/components/dashboard/accounts-summary"; import { CategoryBreakdown } from "@/components/dashboard/category-breakdown"; import { OFXImportDialog } from "@/components/import/ofx-import-dialog"; +import { AccountFilterCombobox } from "@/components/ui/account-filter-combobox"; +import { Card, CardContent } from "@/components/ui/card"; import { useBankingData } from "@/lib/hooks"; import { Button } from "@/components/ui/button"; import { Upload } from "lucide-react"; +import type { BankingData } from "@/lib/types"; export default function DashboardPage() { const { data, isLoading, refresh } = useBankingData(); + const [selectedAccounts, setSelectedAccounts] = useState(["all"]); - if (isLoading || !data) { + // Filter data based on selected accounts + const filteredData = useMemo(() => { + if (!data) return null; + + if (selectedAccounts.includes("all") || selectedAccounts.length === 0) { + return data; + } + + const filteredAccounts = data.accounts.filter((a) => + selectedAccounts.includes(a.id) + ); + const filteredAccountIds = new Set(filteredAccounts.map((a) => a.id)); + const filteredTransactions = data.transactions.filter((t) => + filteredAccountIds.has(t.accountId) + ); + + return { + ...data, + accounts: filteredAccounts, + transactions: filteredTransactions, + }; + }, [data, selectedAccounts]); + + if (isLoading || !data || !filteredData) { return ; } @@ -32,13 +60,28 @@ export default function DashboardPage() { } /> - + + +
+ +
+
+
+ +
- +
- - + +
diff --git a/components/dashboard/accounts-summary.tsx b/components/dashboard/accounts-summary.tsx index bd9eb24..1ce71c0 100644 --- a/components/dashboard/accounts-summary.tsx +++ b/components/dashboard/accounts-summary.tsx @@ -1,10 +1,11 @@ "use client"; +import { useMemo } from "react"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; -import type { BankingData } from "@/lib/types"; +import type { BankingData, Account, Folder } from "@/lib/types"; import { cn } from "@/lib/utils"; -import { Building2 } from "lucide-react"; +import { Building2, Folder as FolderIcon } from "lucide-react"; import { getAccountBalance } from "@/lib/account-utils"; interface AccountsSummaryProps { @@ -19,9 +20,132 @@ export function AccountsSummary({ data }: AccountsSummaryProps) { }).format(amount); }; - const totalPositive = data.accounts - .filter((a) => getAccountBalance(a) > 0) - .reduce((sum, a) => sum + getAccountBalance(a), 0); + // Group accounts by folder + const accountsByFolder = useMemo(() => { + const grouped: Record = {}; + + data.accounts.forEach((account) => { + const folderId = account.folderId || "no-folder"; + if (!grouped[folderId]) { + grouped[folderId] = []; + } + grouped[folderId].push(account); + }); + + // Sort accounts within each folder by name + Object.keys(grouped).forEach((folderId) => { + grouped[folderId].sort((a, b) => a.name.localeCompare(b.name)); + }); + + return grouped; + }, [data.accounts]); + + // Get root folders (folders without parent) sorted by name + const rootFolders = useMemo(() => { + return data.folders + .filter((f) => !f.parentId) + .sort((a, b) => a.name.localeCompare(b.name)); + }, [data.folders]); + + // Helper to get child folders recursively + const getChildFolders = (parentId: string): Folder[] => { + return data.folders + .filter((f) => f.parentId === parentId) + .sort((a, b) => a.name.localeCompare(b.name)); + }; + + // Render folder section recursively + const renderFolderSection = (folder: Folder, level: number = 0) => { + const folderAccounts = accountsByFolder[folder.id] || []; + const childFolders = getChildFolders(folder.id); + const folderTotal = folderAccounts.reduce( + (sum, a) => sum + getAccountBalance(a), + 0, + ); + + if (folderAccounts.length === 0 && childFolders.length === 0) { + return null; + } + + return ( +
0 ? "mt-4" : ""}> + {/* Folder header */} +
+ +

0 && "text-muted-foreground")}> + {folder.name} +

+ {folderAccounts.length > 0 && ( + + ({folderAccounts.length}) + + )} + {folderTotal !== 0 && ( + = 0 ? "text-emerald-600" : "text-red-600", + )} + > + {formatCurrency(folderTotal)} + + )} +
+ + {/* Accounts in this folder */} + {folderAccounts.length > 0 && ( +
0 && "ml-4")}> + {folderAccounts.map((account) => { + const realBalance = getAccountBalance(account); + const totalPositive = data.accounts + .filter((a) => getAccountBalance(a) > 0) + .reduce((sum, a) => sum + getAccountBalance(a), 0); + const percentage = + totalPositive > 0 + ? Math.max(0, (realBalance / totalPositive) * 100) + : 0; + + return ( +
+
+
+
+ +
+
+

{account.name}

+

+ {account.accountNumber} +

+
+
+ = 0 + ? "text-emerald-600" + : "text-red-600", + )} + > + {formatCurrency(realBalance)} + +
+ {realBalance > 0 && ( + + )} +
+ ); + })} +
+ )} + + {/* Child folders */} + {childFolders.map((childFolder) => + renderFolderSection(childFolder, level + 1), + )} +
+ ); + }; if (data.accounts.length === 0) { return ( @@ -42,51 +166,87 @@ export function AccountsSummary({ data }: AccountsSummaryProps) { ); } + const orphanAccounts = accountsByFolder["no-folder"] || []; + const orphanTotal = orphanAccounts.reduce( + (sum, a) => sum + getAccountBalance(a), + 0, + ); + return ( Mes Comptes -
- {data.accounts.map((account) => { - const realBalance = getAccountBalance(account); - const percentage = - totalPositive > 0 - ? Math.max(0, (realBalance / totalPositive) * 100) - : 0; - - return ( -
-
-
-
- -
-
-

{account.name}

-

- {account.accountNumber} -

-
-
+
+ {/* Accounts without folder */} + {orphanAccounts.length > 0 && ( +
+
+ +

Sans dossier

+ + ({orphanAccounts.length}) + + {orphanTotal !== 0 && ( = 0 - ? "text-emerald-600" - : "text-red-600", + "text-xs font-semibold tabular-nums ml-auto", + orphanTotal >= 0 ? "text-emerald-600" : "text-red-600", )} > - {formatCurrency(realBalance)} + {formatCurrency(orphanTotal)} -
- {realBalance > 0 && ( - )}
- ); - })} +
+ {orphanAccounts.map((account) => { + const realBalance = getAccountBalance(account); + const totalPositive = data.accounts + .filter((a) => getAccountBalance(a) > 0) + .reduce((sum, a) => sum + getAccountBalance(a), 0); + const percentage = + totalPositive > 0 + ? Math.max(0, (realBalance / totalPositive) * 100) + : 0; + + return ( +
+
+
+
+ +
+
+

{account.name}

+

+ {account.accountNumber} +

+
+
+ = 0 + ? "text-emerald-600" + : "text-red-600", + )} + > + {formatCurrency(realBalance)} + +
+ {realBalance > 0 && ( + + )} +
+ ); + })} +
+
+ )} + + {/* Folders */} + {rootFolders.map((folder) => renderFolderSection(folder))}