diff --git a/app/accounts/page.tsx b/app/accounts/page.tsx index 089f22f..b064b94 100644 --- a/app/accounts/page.tsx +++ b/app/accounts/page.tsx @@ -1,60 +1,14 @@ "use client"; import { useState } from "react"; -import { Sidebar } from "@/components/dashboard/sidebar"; +import { PageLayout, LoadingState, PageHeader } from "@/components/layout"; +import { AccountCard, AccountEditDialog } from "@/components/accounts"; import { useBankingData } from "@/lib/hooks"; import { updateAccount, deleteAccount } from "@/lib/store-db"; -import { Button } from "@/components/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - MoreVertical, - Pencil, - Trash2, - Building2, - CreditCard, - Wallet, - PiggyBank, - RefreshCw, - ExternalLink, -} from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { Building2 } from "lucide-react"; import type { Account } from "@/lib/types"; import { cn } from "@/lib/utils"; -import Link from "next/link"; - -const accountTypeIcons = { - CHECKING: Wallet, - SAVINGS: PiggyBank, - CREDIT_CARD: CreditCard, - OTHER: Building2, -}; - -const accountTypeLabels = { - CHECKING: "Compte courant", - SAVINGS: "Épargne", - CREDIT_CARD: "Carte de crédit", - OTHER: "Autre", -}; export default function AccountsPage() { const { data, isLoading, refresh } = useBankingData(); @@ -68,14 +22,7 @@ export default function AccountsPage() { }); if (isLoading || !data) { - return ( -
- -
- -
-
- ); + return ; } const formatCurrency = (amount: number) => { @@ -136,217 +83,64 @@ export default function AccountsPage() { const totalBalance = data.accounts.reduce((sum, a) => sum + a.balance, 0); return ( -
- -
-
-
-
-

Comptes

-

- Gérez vos comptes bancaires -

-
-
-

Solde total

-

= 0 ? "text-emerald-600" : "text-red-600", - )} - > - {formatCurrency(totalBalance)} -

-
+ + +

Solde total

+

= 0 ? "text-emerald-600" : "text-red-600" + )} + > + {formatCurrency(totalBalance)} +

+ } + /> - {data.accounts.length === 0 ? ( - - - -

Aucun compte

-

- Importez un fichier OFX depuis le tableau de bord pour ajouter - votre premier compte. -

-
-
- ) : ( -
- {data.accounts.map((account) => { - const Icon = accountTypeIcons[account.type]; - const folder = data.folders.find( - (f) => f.id === account.folderId, - ); + {data.accounts.length === 0 ? ( + + + +

Aucun compte

+

+ Importez un fichier OFX depuis le tableau de bord pour ajouter + votre premier compte. +

+
+
+ ) : ( +
+ {data.accounts.map((account) => { + const folder = data.folders.find((f) => f.id === account.folderId); - return ( - - -
-
-
- -
-
- - {account.name} - -

- {accountTypeLabels[account.type]} -

-
-
- - - - - - handleEdit(account)} - > - - Modifier - - handleDelete(account.id)} - className="text-red-600" - > - - Supprimer - - - -
-
- -
= 0 - ? "text-emerald-600" - : "text-red-600", - )} - > - {formatCurrency(account.balance)} -
-
- - {getTransactionCount(account.id)} transactions - - {folder && {folder.name}} -
- {account.lastImport && ( -

- Dernier import:{" "} - {new Date(account.lastImport).toLocaleDateString( - "fr-FR", - )} -

- )} - {account.externalUrl && ( - - - Accéder au portail banque - - )} -
-
- ); - })} -
- )} + return ( + + ); + })}
-
+ )} - - - - Modifier le compte - -
-
- - - setFormData({ ...formData, name: e.target.value }) - } - /> -
-
- - -
-
- - -
-
- - - setFormData({ ...formData, externalUrl: e.target.value }) - } - placeholder="https://..." - /> -

- URL personnalisée vers le portail de votre banque -

-
-
- - -
-
-
-
-
+ + ); } diff --git a/app/categories/page.tsx b/app/categories/page.tsx index de26631..8da0b93 100644 --- a/app/categories/page.tsx +++ b/app/categories/page.tsx @@ -1,49 +1,16 @@ "use client"; import { useState, useMemo } from "react"; -import { Sidebar } from "@/components/dashboard/sidebar"; +import { PageLayout, LoadingState, PageHeader } from "@/components/layout"; +import { + CategoryCard, + CategoryEditDialog, + ParentCategoryRow, + CategorySearchBar, +} from "@/components/categories"; import { useBankingData } from "@/lib/hooks"; import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Badge } from "@/components/ui/badge"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from "@/components/ui/collapsible"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - Plus, - MoreVertical, - Pencil, - Trash2, - RefreshCw, - X, - ChevronDown, - ChevronRight, - ChevronsUpDown, - Search, -} from "lucide-react"; -import { CategoryIcon } from "@/components/ui/category-icon"; +import { Plus } from "lucide-react"; import { autoCategorize, addCategory, @@ -51,32 +18,13 @@ import { deleteCategory, } from "@/lib/store-db"; import type { Category } from "@/lib/types"; -import { cn } from "@/lib/utils"; - -const categoryColors = [ - "#22c55e", - "#3b82f6", - "#f59e0b", - "#ec4899", - "#ef4444", - "#8b5cf6", - "#06b6d4", - "#84cc16", - "#f97316", - "#6366f1", - "#14b8a6", - "#f43f5e", - "#64748b", - "#0891b2", - "#dc2626", -]; export default function CategoriesPage() { const { data, isLoading, refresh } = useBankingData(); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingCategory, setEditingCategory] = useState(null); const [expandedParents, setExpandedParents] = useState>( - new Set(), + new Set() ); const [formData, setFormData] = useState({ name: "", @@ -84,7 +32,6 @@ export default function CategoriesPage() { keywords: [] as string[], parentId: null as string | null, }); - const [newKeyword, setNewKeyword] = useState(""); const [searchQuery, setSearchQuery] = useState(""); // Organiser les catégories par parent @@ -101,7 +48,6 @@ export default function CategoriesPage() { const children: Record = {}; const orphans: Category[] = []; - // Grouper les enfants par parent data.categories .filter((c) => c.parentId !== null) .forEach((child) => { @@ -131,14 +77,7 @@ export default function CategoriesPage() { }); if (isLoading || !data) { - return ( -
- -
- -
-
- ); + return ; } const formatCurrency = (amount: number) => { @@ -159,11 +98,11 @@ export default function CategoriesPage() { } const categoryTransactions = data.transactions.filter((t) => - categoryIds.includes(t.categoryId || ""), + categoryIds.includes(t.categoryId || "") ); const total = categoryTransactions.reduce( (sum, t) => sum + Math.abs(t.amount), - 0, + 0 ); const count = categoryTransactions.length; return { total, count }; @@ -252,30 +191,10 @@ export default function CategoriesPage() { } }; - const addKeyword = () => { - if ( - newKeyword.trim() && - !formData.keywords.includes(newKeyword.trim().toLowerCase()) - ) { - setFormData({ - ...formData, - keywords: [...formData.keywords, newKeyword.trim().toLowerCase()], - }); - setNewKeyword(""); - } - }; - - const removeKeyword = (keyword: string) => { - setFormData({ - ...formData, - keywords: formData.keywords.filter((k) => k !== keyword), - }); - }; - const reApplyAutoCategories = async () => { if ( !confirm( - "Recatégoriser automatiquement les transactions non catégorisées ?", + "Recatégoriser automatiquement les transactions non catégorisées ?" ) ) return; @@ -287,7 +206,7 @@ export default function CategoriesPage() { for (const transaction of uncategorized) { const categoryId = autoCategorize( transaction.description + " " + (transaction.memo || ""), - data.categories, + data.categories ); if (categoryId) { await updateTransaction({ ...transaction, categoryId }); @@ -301,395 +220,118 @@ export default function CategoriesPage() { }; const uncategorizedCount = data.transactions.filter( - (t) => !t.categoryId, + (t) => !t.categoryId ).length; - // Composant pour une carte de catégorie enfant - const ChildCategoryCard = ({ category }: { category: Category }) => { - const stats = getCategoryStats(category.id); - - return ( -
-
-
- -
- {category.name} - - {stats.count} opération{stats.count > 1 ? "s" : ""} •{" "} - {formatCurrency(stats.total)} - - {category.keywords.length > 0 && ( - - {category.keywords.length} - - )} -
- -
- - -
-
+ // Filtrer les catégories selon la recherche + const filteredParentCategories = parentCategories.filter((parent) => { + if (!searchQuery.trim()) return true; + const query = searchQuery.toLowerCase(); + if (parent.name.toLowerCase().includes(query)) return true; + if (parent.keywords.some((k) => k.toLowerCase().includes(query))) + return true; + const children = childrenByParent[parent.id] || []; + return children.some( + (c) => + c.name.toLowerCase().includes(query) || + c.keywords.some((k) => k.toLowerCase().includes(query)) ); - }; + }); return ( -
- -
-
- {/* Header */} -
-
-

Catégories

-

- {parentCategories.length} catégories principales •{" "} - {data.categories.length - parentCategories.length}{" "} - sous-catégories -

-
-
- {uncategorizedCount > 0 && ( - - )} - -
-
- - {/* Barre de recherche et contrôles */} -
-
- - setSearchQuery(e.target.value)} - className="pl-9" - /> - {searchQuery && ( - - )} -
- -
- - {/* Liste des catégories par parent */} -
- {parentCategories - .filter((parent) => { - if (!searchQuery.trim()) return true; - const query = searchQuery.toLowerCase(); - // Afficher si le parent matche - if (parent.name.toLowerCase().includes(query)) return true; - if ( - parent.keywords.some((k) => k.toLowerCase().includes(query)) - ) - return true; - // Ou si un enfant matche - const children = childrenByParent[parent.id] || []; - return children.some( - (c) => - c.name.toLowerCase().includes(query) || - c.keywords.some((k) => k.toLowerCase().includes(query)), - ); - }) - .map((parent) => { - const allChildren = childrenByParent[parent.id] || []; - // Filtrer les enfants aussi si recherche active - const children = searchQuery.trim() - ? allChildren.filter( - (c) => - c.name - .toLowerCase() - .includes(searchQuery.toLowerCase()) || - c.keywords.some((k) => - k.toLowerCase().includes(searchQuery.toLowerCase()), - ) || - // Garder tous les enfants si le parent matche - parent.name - .toLowerCase() - .includes(searchQuery.toLowerCase()), - ) - : allChildren; - const stats = getCategoryStats(parent.id, true); - const isExpanded = - expandedParents.has(parent.id) || - (searchQuery.trim() !== "" && children.length > 0); - - return ( -
- toggleExpanded(parent.id)} - > -
- - - - -
- - - - - - - handleEdit(parent)} - > - - Modifier - - handleDelete(parent.id)} - className="text-red-600" - > - - Supprimer - - - -
-
- - - {children.length > 0 ? ( -
- {children.map((child) => ( - - ))} -
- ) : ( -
- Aucune sous-catégorie -
- )} -
-
-
- ); - })} - - {/* Catégories orphelines (sans parent valide) */} - {orphanCategories.length > 0 && ( -
-
- - Catégories non classées ({orphanCategories.length}) - -
-
- {orphanCategories.map((category) => ( - - ))} -
-
)} -
-
-
+ + + } + /> - {/* Dialog de création/édition */} - - - - - {editingCategory ? "Modifier la catégorie" : "Nouvelle catégorie"} - - -
- {/* Catégorie parente */} -
- - + + +
+ {filteredParentCategories.map((parent) => { + const allChildren = childrenByParent[parent.id] || []; + const children = searchQuery.trim() + ? allChildren.filter( + (c) => + c.name.toLowerCase().includes(searchQuery.toLowerCase()) || + c.keywords.some((k) => + k.toLowerCase().includes(searchQuery.toLowerCase()) + ) || + parent.name.toLowerCase().includes(searchQuery.toLowerCase()) + ) + : allChildren; + const stats = getCategoryStats(parent.id, true); + const isExpanded = + expandedParents.has(parent.id) || + (searchQuery.trim() !== "" && children.length > 0); + + return ( + toggleExpanded(parent.id)} + formatCurrency={formatCurrency} + getCategoryStats={(id) => getCategoryStats(id)} + onEdit={handleEdit} + onDelete={handleDelete} + onNewCategory={handleNewCategory} + /> + ); + })} + + {orphanCategories.length > 0 && ( +
+
+ + Catégories non classées ({orphanCategories.length}) +
- - {/* Nom */} -
- - - setFormData({ ...formData, name: e.target.value }) - } - placeholder="Ex: Alimentation" - /> -
- - {/* Couleur */} -
- -
- {categoryColors.map((color) => ( -
-
- - {/* Mots-clés */} -
- -
- setNewKeyword(e.target.value)} - placeholder="Ajouter un mot-clé" - onKeyDown={(e) => - e.key === "Enter" && (e.preventDefault(), addKeyword()) - } +
+ {orphanCategories.map((category) => ( + - -
-
- {formData.keywords.map((keyword) => ( - - {keyword} - - - ))} -
-
- - {/* Actions */} -
- - + ))}
- -
-
+ )} + + + + ); } diff --git a/app/folders/page.tsx b/app/folders/page.tsx index 26ce9c4..cff54ef 100644 --- a/app/folders/page.tsx +++ b/app/folders/page.tsx @@ -1,43 +1,16 @@ "use client"; import { useState } from "react"; -import { Sidebar } from "@/components/dashboard/sidebar"; +import { PageLayout, LoadingState, PageHeader } from "@/components/layout"; +import { + FolderTreeItem, + FolderEditDialog, + AccountFolderDialog, +} from "@/components/folders"; import { useBankingData } from "@/lib/hooks"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - Plus, - MoreVertical, - Pencil, - Trash2, - Folder, - FolderOpen, - ChevronRight, - ChevronDown, - Building2, - RefreshCw, -} from "lucide-react"; +import { Plus } from "lucide-react"; import { addFolder, updateFolder, @@ -45,189 +18,6 @@ import { updateAccount, } from "@/lib/store-db"; import type { Folder as FolderType, Account } from "@/lib/types"; -import { cn } from "@/lib/utils"; -import Link from "next/link"; - -const folderColors = [ - { value: "#6366f1", label: "Indigo" }, - { value: "#22c55e", label: "Vert" }, - { value: "#f59e0b", label: "Orange" }, - { value: "#ec4899", label: "Rose" }, - { value: "#3b82f6", label: "Bleu" }, - { value: "#ef4444", label: "Rouge" }, -]; - -interface FolderTreeItemProps { - folder: FolderType; - accounts: Account[]; - allFolders: FolderType[]; - level: number; - onEdit: (folder: FolderType) => void; - onDelete: (folderId: string) => void; - onEditAccount: (account: Account) => void; - formatCurrency: (amount: number) => string; -} - -function FolderTreeItem({ - folder, - accounts, - allFolders, - level, - onEdit, - onDelete, - onEditAccount, - formatCurrency, -}: FolderTreeItemProps) { - const [isExpanded, setIsExpanded] = useState(true); - - // Pour le dossier "Mes Comptes" (folder-root), inclure aussi les comptes sans dossier - const folderAccounts = accounts.filter( - (a) => - a.folderId === folder.id || - (folder.id === "folder-root" && a.folderId === null), - ); - const childFolders = allFolders.filter((f) => f.parentId === folder.id); - const hasChildren = childFolders.length > 0 || folderAccounts.length > 0; - - const folderTotal = folderAccounts.reduce((sum, a) => sum + a.balance, 0); - - return ( -
-
0 && "ml-6", - )} - > - - -
- {isExpanded ? ( - - ) : ( - - )} -
- - {folder.name} - - {folderAccounts.length > 0 && ( - = 0 ? "text-emerald-600" : "text-red-600", - )} - > - {formatCurrency(folderTotal)} - - )} - - - - - - - onEdit(folder)}> - - Modifier - - {folder.id !== "folder-root" && ( - onDelete(folder.id)} - className="text-red-600" - > - - Supprimer - - )} - - -
- - {isExpanded && ( -
- {folderAccounts.map((account) => ( -
- - - {account.name} - - = 0 ? "text-emerald-600" : "text-red-600", - )} - > - {formatCurrency(account.balance)} - - -
- ))} - - {childFolders.map((child) => ( - - ))} -
- )} -
- ); -} - -const accountTypeLabels = { - CHECKING: "Compte courant", - SAVINGS: "Épargne", - CREDIT_CARD: "Carte de crédit", - OTHER: "Autre", -}; export default function FoldersPage() { const { data, isLoading, refresh } = useBankingData(); @@ -249,14 +39,7 @@ export default function FoldersPage() { }); if (isLoading || !data) { - return ( -
- -
- -
-
- ); + return ; } const formatCurrency = (amount: number) => { @@ -315,7 +98,7 @@ export default function FoldersPage() { const handleDelete = async (folderId: string) => { if ( !confirm( - "Supprimer ce dossier ? Les comptes seront déplacés à la racine.", + "Supprimer ce dossier ? Les comptes seront déplacés à la racine." ) ) return; @@ -362,196 +145,59 @@ export default function FoldersPage() { }; return ( -
- -
-
-
-
-

- Organisation -

-

- Organisez vos comptes en dossiers -

-
- -
+ + + + Nouveau dossier + + } + /> - - - Arborescence des comptes - - -
- {rootFolders.map((folder) => ( - - ))} -
-
-
-
-
- - - - - - {editingFolder ? "Modifier le dossier" : "Nouveau dossier"} - - -
-
- - - setFormData({ ...formData, name: e.target.value }) - } - placeholder="Ex: Comptes personnels" + + + Arborescence des comptes + + +
+ {rootFolders.map((folder) => ( + -
-
- - -
-
- -
- {folderColors.map(({ value }) => ( -
-
-
- - -
+ ))}
- -
+ + - - - - Modifier le compte - -
-
- - - setAccountFormData({ - ...accountFormData, - name: e.target.value, - }) - } - /> -
-
- - -
-
- - -
-
- - -
-
-
-
-
+ + + + ); } diff --git a/app/page.tsx b/app/page.tsx index 2553b4f..29d4365 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { Sidebar } from "@/components/dashboard/sidebar"; +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"; @@ -8,55 +8,39 @@ import { CategoryBreakdown } from "@/components/dashboard/category-breakdown"; import { OFXImportDialog } from "@/components/import/ofx-import-dialog"; import { useBankingData } from "@/lib/hooks"; import { Button } from "@/components/ui/button"; -import { Upload, RefreshCw } from "lucide-react"; +import { Upload } from "lucide-react"; export default function DashboardPage() { const { data, isLoading, refresh } = useBankingData(); if (isLoading || !data) { - return ( -
- -
- -
-
- ); + return ; } return ( -
- -
-
-
-
-

- Tableau de bord -

-

- Vue d'ensemble de vos finances -

-
- - - -
+ + + + + } + /> - + -
- -
- - -
-
+
+ +
+ +
-
-
+ + ); } diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 7947fc2..fc81269 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -1,36 +1,9 @@ "use client"; import { useState } from "react"; -import { Sidebar } from "@/components/dashboard/sidebar"; +import { PageLayout, LoadingState, PageHeader } from "@/components/layout"; +import { DataCard, DangerZoneCard, OFXInfoCard } from "@/components/settings"; import { useBankingData } from "@/lib/hooks"; -import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, - AlertDialogTrigger, -} from "@/components/ui/alert-dialog"; -import { - Download, - Trash2, - Upload, - RefreshCw, - Database, - FileJson, - Tags, -} from "lucide-react"; import type { BankingData } from "@/lib/types"; export default function SettingsPage() { @@ -38,14 +11,7 @@ export default function SettingsPage() { const [importing, setImporting] = useState(false); if (isLoading || !data) { - return ( -
- -
- -
-
- ); + return ; } const exportData = () => { @@ -74,7 +40,6 @@ export default function SettingsPage() { const content = await file.text(); const importedData = JSON.parse(content) as BankingData; - // Validate structure if ( !importedData.accounts || !importedData.transactions || @@ -107,7 +72,7 @@ export default function SettingsPage() { "/api/banking/transactions/clear-categories", { method: "POST", - }, + } ); if (!response.ok) throw new Error("Erreur"); refresh(); @@ -121,173 +86,28 @@ export default function SettingsPage() { const categorizedCount = data.transactions.filter((t) => t.categoryId).length; return ( -
- -
-
-
-

Paramètres

-

- Gérez vos données et préférences -

-
+ +
+ - - - - - Données - - - Exportez ou importez vos données pour les sauvegarder ou les - transférer - - - -
-
-

Statistiques

-

- {data.accounts.length} comptes, {data.transactions.length}{" "} - transactions, {data.categories.length} catégories -

-
-
+ -
- - -
-
-
+ - - - - - Zone dangereuse - - - Actions irréversibles - procédez avec prudence - - - - {/* Supprimer catégories des opérations */} - - - - - - - - Supprimer toutes les catégories ? - - - Cette action va retirer la catégorie de {categorizedCount}{" "} - opération{categorizedCount > 1 ? "s" : ""}. Les catégories - elles-mêmes ne seront pas supprimées, seulement leur - affectation aux opérations. - - - - Annuler - - Supprimer les affectations - - - - - - {/* Réinitialiser toutes les données */} - - - - - - - Êtes-vous sûr ? - - Cette action supprimera définitivement tous vos comptes, - transactions, catégories et dossiers. Cette action est - irréversible. - - - - Annuler - - Supprimer tout - - - - - - - - - - - - Format OFX - - - Informations sur l'import de fichiers - - - -
-

- L'application accepte les fichiers au format OFX (Open - Financial Exchange) ou QFX. Ces fichiers sont généralement - disponibles depuis l'espace client de votre banque. -

-

- Lors de l'import, les transactions sont automatiquement - catégorisées selon les mots-clés définis. Les doublons sont - détectés et ignorés automatiquement. -

-
-
-
-
-
-
+ + + ); } diff --git a/app/statistics/page.tsx b/app/statistics/page.tsx index 31e8678..d9d0c8b 100644 --- a/app/statistics/page.tsx +++ b/app/statistics/page.tsx @@ -1,9 +1,15 @@ "use client"; import { useState, useMemo } from "react"; -import { Sidebar } from "@/components/dashboard/sidebar"; +import { PageLayout, LoadingState, PageHeader } from "@/components/layout"; +import { + StatsSummaryCards, + MonthlyChart, + CategoryPieChart, + BalanceLineChart, + TopExpensesList, +} from "@/components/statistics"; import { useBankingData } from "@/lib/hooks"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Select, SelectContent, @@ -11,24 +17,6 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { RefreshCw, TrendingUp, TrendingDown, ArrowRight } from "lucide-react"; -import { CategoryIcon } from "@/components/ui/category-icon"; -import { - BarChart, - Bar, - XAxis, - YAxis, - CartesianGrid, - Tooltip, - ResponsiveContainer, - PieChart, - Pie, - Cell, - LineChart, - Line, - Legend, -} from "recharts"; -import { cn } from "@/lib/utils"; type Period = "3months" | "6months" | "12months" | "all"; @@ -58,12 +46,12 @@ export default function StatisticsPage() { } let transactions = data.transactions.filter( - (t) => new Date(t.date) >= startDate, + (t) => new Date(t.date) >= startDate ); if (selectedAccount !== "all") { transactions = transactions.filter( - (t) => t.accountId === selectedAccount, + (t) => t.accountId === selectedAccount ); } @@ -132,7 +120,7 @@ export default function StatisticsPage() { // Balance evolution const sortedTransactions = [...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() ); let runningBalance = 0; @@ -149,7 +137,7 @@ export default function StatisticsPage() { month: "short", }), solde: Math.round(balance), - }), + }) ); return { @@ -164,17 +152,6 @@ export default function StatisticsPage() { }; }, [data, period, selectedAccount]); - if (isLoading || !data || !stats) { - return ( -
- -
- -
-
- ); - } - const formatCurrency = (amount: number) => { return new Intl.NumberFormat("fr-FR", { style: "currency", @@ -182,333 +159,74 @@ export default function StatisticsPage() { }).format(amount); }; + if (isLoading || !data || !stats) { + return ; + } + return ( -
- -
-
-
-
-

- Statistiques -

-

- Analysez vos dépenses et revenus -

-
-
- - -
-
+ + + + + + } + /> - {/* Summary Cards */} -
- - - - - Total Revenus - - - -
- {formatCurrency(stats.totalIncome)} -
-
-
+ - - - - - Total Dépenses - - - -
- {formatCurrency(stats.totalExpenses)} -
-
-
- - - - - - Moyenne mensuelle - - - -
- {formatCurrency(stats.avgMonthlyExpenses)} -
-
-
- - - - - Économies - - - -
= 0 - ? "text-emerald-600" - : "text-red-600", - )} - > - {formatCurrency(stats.totalIncome - stats.totalExpenses)} -
-
-
-
- - {/* Charts */} -
- {/* Monthly Income vs Expenses */} - - - Revenus vs Dépenses par mois - - - {stats.monthlyChartData.length > 0 ? ( -
- - - - - `${v}€`} - /> - formatCurrency(value)} - contentStyle={{ - backgroundColor: "hsl(var(--card))", - border: "1px solid hsl(var(--border))", - borderRadius: "8px", - }} - /> - - - - - -
- ) : ( -
- Pas de données pour cette période -
- )} -
-
- - {/* Category Breakdown */} - - - Répartition par catégorie - - - {stats.categoryChartData.length > 0 ? ( -
- - - - {stats.categoryChartData.map((entry, index) => ( - - ))} - - formatCurrency(value)} - contentStyle={{ - backgroundColor: "hsl(var(--card))", - border: "1px solid hsl(var(--border))", - borderRadius: "8px", - }} - /> - ( - - {value} - - )} - /> - - -
- ) : ( -
- Pas de données pour cette période -
- )} -
-
- - {/* Balance Evolution */} - - - Évolution du solde - - - {stats.balanceChartData.length > 0 ? ( -
- - - - - `${v}€`} - /> - formatCurrency(value)} - contentStyle={{ - backgroundColor: "hsl(var(--card))", - border: "1px solid hsl(var(--border))", - borderRadius: "8px", - }} - /> - - - -
- ) : ( -
- Pas de données pour cette période -
- )} -
-
- - {/* Top Expenses */} - - - Top 5 dépenses - - - {stats.topExpenses.length > 0 ? ( -
- {stats.topExpenses.map((expense, index) => { - const category = data.categories.find( - (c) => c.id === expense.categoryId, - ); - return ( -
-
- {index + 1} -
-
-

- {expense.description} -

-
- - {new Date(expense.date).toLocaleDateString( - "fr-FR", - )} - - {category && ( - - - {category.name} - - )} -
-
-
- {formatCurrency(expense.amount)} -
-
- ); - })} -
- ) : ( -
- Pas de dépenses pour cette période -
- )} -
-
-
-
-
-
+
+ + + + +
+ ); } diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index 1ffd426..6befa30 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -2,41 +2,16 @@ import { useState, useMemo, useEffect } from "react"; import { useSearchParams } from "next/navigation"; -import { Sidebar } from "@/components/dashboard/sidebar"; +import { PageLayout, LoadingState, PageHeader } from "@/components/layout"; +import { + TransactionFilters, + TransactionBulkActions, + TransactionTable, +} from "@/components/transactions"; +import { OFXImportDialog } from "@/components/import/ofx-import-dialog"; import { useBankingData } from "@/lib/hooks"; import { Button } from "@/components/ui/button"; -import { Card, CardContent } from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { Badge } from "@/components/ui/badge"; -import { Checkbox } from "@/components/ui/checkbox"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, - DropdownMenuSeparator, -} from "@/components/ui/dropdown-menu"; -import { OFXImportDialog } from "@/components/import/ofx-import-dialog"; -import { CategoryIcon } from "@/components/ui/category-icon"; -import { - Search, - CheckCircle2, - Circle, - MoreVertical, - Tags, - Upload, - RefreshCw, - ArrowUpDown, - Check, -} from "lucide-react"; -import { cn } from "@/lib/utils"; +import { Upload } from "lucide-react"; type SortField = "date" | "amount" | "description"; type SortOrder = "asc" | "desc"; @@ -47,19 +22,19 @@ export default function TransactionsPage() { const [searchQuery, setSearchQuery] = useState(""); const [selectedAccount, setSelectedAccount] = useState("all"); - // Initialize account filter from URL params useEffect(() => { const accountId = searchParams.get("accountId"); if (accountId) { setSelectedAccount(accountId); } }, [searchParams]); + const [selectedCategory, setSelectedCategory] = useState("all"); const [showReconciled, setShowReconciled] = useState("all"); const [sortField, setSortField] = useState("date"); const [sortOrder, setSortOrder] = useState("desc"); const [selectedTransactions, setSelectedTransactions] = useState>( - new Set(), + new Set() ); const filteredTransactions = useMemo(() => { @@ -67,43 +42,38 @@ export default function TransactionsPage() { let transactions = [...data.transactions]; - // Filter by search if (searchQuery) { const query = searchQuery.toLowerCase(); transactions = transactions.filter( (t) => t.description.toLowerCase().includes(query) || - t.memo?.toLowerCase().includes(query), + t.memo?.toLowerCase().includes(query) ); } - // Filter by account if (selectedAccount !== "all") { transactions = transactions.filter( - (t) => t.accountId === selectedAccount, + (t) => t.accountId === selectedAccount ); } - // Filter by category if (selectedCategory !== "all") { if (selectedCategory === "uncategorized") { transactions = transactions.filter((t) => !t.categoryId); } else { transactions = transactions.filter( - (t) => t.categoryId === selectedCategory, + (t) => t.categoryId === selectedCategory ); } } - // Filter by reconciliation status if (showReconciled !== "all") { const isReconciled = showReconciled === "reconciled"; transactions = transactions.filter( - (t) => t.isReconciled === isReconciled, + (t) => t.isReconciled === isReconciled ); } - // Sort transactions.sort((a, b) => { let comparison = 0; switch (sortField) { @@ -132,14 +102,7 @@ export default function TransactionsPage() { ]); if (isLoading || !data) { - return ( -
- -
- -
-
- ); + return ; } const formatCurrency = (amount: number) => { @@ -161,15 +124,16 @@ export default function TransactionsPage() { const transaction = data.transactions.find((t) => t.id === transactionId); if (!transaction) return; - const updatedTransaction = { ...transaction, isReconciled: !transaction.isReconciled }; - - // Optimistic update + const updatedTransaction = { + ...transaction, + isReconciled: !transaction.isReconciled, + }; + const updatedTransactions = data.transactions.map((t) => - t.id === transactionId ? updatedTransaction : t, + t.id === transactionId ? updatedTransaction : t ); update({ ...data, transactions: updatedTransactions }); - // Persist to database try { await fetch("/api/banking/transactions", { method: "PUT", @@ -178,24 +142,24 @@ export default function TransactionsPage() { }); } catch (error) { console.error("Failed to update transaction:", error); - // Revert on error refresh(); } }; - const setCategory = async (transactionId: string, categoryId: string | null) => { + const setCategory = async ( + transactionId: string, + categoryId: string | null + ) => { const transaction = data.transactions.find((t) => t.id === transactionId); if (!transaction) return; const updatedTransaction = { ...transaction, categoryId }; - - // Optimistic update + const updatedTransactions = data.transactions.map((t) => - t.id === transactionId ? updatedTransaction : t, + t.id === transactionId ? updatedTransaction : t ); update({ ...data, transactions: updatedTransactions }); - // Persist to database try { await fetch("/api/banking/transactions", { method: "PUT", @@ -212,15 +176,13 @@ export default function TransactionsPage() { const transactionsToUpdate = data.transactions.filter((t) => selectedTransactions.has(t.id) ); - - // Optimistic update + const updatedTransactions = data.transactions.map((t) => - selectedTransactions.has(t.id) ? { ...t, isReconciled: reconciled } : t, + selectedTransactions.has(t.id) ? { ...t, isReconciled: reconciled } : t ); update({ ...data, transactions: updatedTransactions }); setSelectedTransactions(new Set()); - // Persist to database try { await Promise.all( transactionsToUpdate.map((t) => @@ -241,15 +203,13 @@ export default function TransactionsPage() { const transactionsToUpdate = data.transactions.filter((t) => selectedTransactions.has(t.id) ); - - // Optimistic update + const updatedTransactions = data.transactions.map((t) => - selectedTransactions.has(t.id) ? { ...t, categoryId } : t, + selectedTransactions.has(t.id) ? { ...t, categoryId } : t ); update({ ...data, transactions: updatedTransactions }); setSelectedTransactions(new Set()); - // Persist to database try { await Promise.all( transactionsToUpdate.map((t) => @@ -284,411 +244,65 @@ export default function TransactionsPage() { setSelectedTransactions(newSelected); }; - const getCategory = (categoryId: string | null) => { - if (!categoryId) return null; - return data.categories.find((c) => c.id === categoryId); - }; - - const getAccount = (accountId: string) => { - return data.accounts.find((a) => a.id === accountId); + const handleSortChange = (field: SortField) => { + if (sortField === field) { + setSortOrder(sortOrder === "asc" ? "desc" : "asc"); + } else { + setSortField(field); + setSortOrder(field === "date" ? "desc" : "asc"); + } }; return ( -
- -
-
-
-
-

- Transactions -

-

- {filteredTransactions.length} transaction - {filteredTransactions.length > 1 ? "s" : ""} -

-
- - - -
+ + 1 ? "s" : ""}`} + actions={ + + + + } + /> - {/* Filters */} - - -
-
-
- - setSearchQuery(e.target.value)} - className="pl-9" - /> -
-
+ - + - - - -
-
-
- - {/* Bulk actions */} - {selectedTransactions.size > 0 && ( - - -
- - {selectedTransactions.size} sélectionnée - {selectedTransactions.size > 1 ? "s" : ""} - - - - - - - - - bulkSetCategory(null)}> - Aucune catégorie - - - {data.categories.map((cat) => ( - bulkSetCategory(cat.id)} - > - - {cat.name} - - ))} - - -
-
-
- )} - - {/* Transactions list */} - - - {filteredTransactions.length === 0 ? ( -
-

- Aucune transaction trouvée -

-
- ) : ( -
- - - - - - - - - - - - - - - {filteredTransactions.map((transaction) => { - const category = getCategory(transaction.categoryId); - const account = getAccount(transaction.accountId); - - return ( - - - - - - - - - - - ); - })} - -
- 0 - } - onCheckedChange={toggleSelectAll} - /> - - - - - - Compte - - Catégorie - - - - Pointé -
- - toggleSelectTransaction(transaction.id) - } - /> - - {formatDate(transaction.date)} - -

- {transaction.description} -

- {transaction.memo && ( -

- {transaction.memo} -

- )} -
- {account?.name || "-"} - - - - - - - - setCategory(transaction.id, null) - } - > - Aucune catégorie - - - {data.categories.map((cat) => ( - - setCategory(transaction.id, cat.id) - } - > - - {cat.name} - {transaction.categoryId === cat.id && ( - - )} - - ))} - - - = 0 - ? "text-emerald-600" - : "text-red-600", - )} - > - {transaction.amount >= 0 ? "+" : ""} - {formatCurrency(transaction.amount)} - - - - - - - - - - toggleReconciled(transaction.id) - } - > - {transaction.isReconciled - ? "Dépointer" - : "Pointer"} - - - -
-
- )} -
-
-
-
-
+ + ); } diff --git a/components/accounts/account-card.tsx b/components/accounts/account-card.tsx new file mode 100644 index 0000000..406afb7 --- /dev/null +++ b/components/accounts/account-card.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { MoreVertical, Pencil, Trash2, ExternalLink } from "lucide-react"; +import { cn } from "@/lib/utils"; +import Link from "next/link"; +import type { Account, Folder } from "@/lib/types"; +import { accountTypeIcons, accountTypeLabels } from "./constants"; + +interface AccountCardProps { + account: Account; + folder?: Folder; + transactionCount: number; + onEdit: (account: Account) => void; + onDelete: (accountId: string) => void; + formatCurrency: (amount: number) => string; +} + +export function AccountCard({ + account, + folder, + transactionCount, + onEdit, + onDelete, + formatCurrency, +}: AccountCardProps) { + const Icon = accountTypeIcons[account.type]; + + return ( + + +
+
+
+ +
+
+ {account.name} +

+ {accountTypeLabels[account.type]} +

+
+
+ + + + + + onEdit(account)}> + + Modifier + + onDelete(account.id)} + className="text-red-600" + > + + Supprimer + + + +
+
+ +
= 0 ? "text-emerald-600" : "text-red-600" + )} + > + {formatCurrency(account.balance)} +
+
+ + {transactionCount} transactions + + {folder && {folder.name}} +
+ {account.lastImport && ( +

+ Dernier import:{" "} + {new Date(account.lastImport).toLocaleDateString("fr-FR")} +

+ )} + {account.externalUrl && ( + + + Accéder au portail banque + + )} +
+
+ ); +} + diff --git a/components/accounts/account-edit-dialog.tsx b/components/accounts/account-edit-dialog.tsx new file mode 100644 index 0000000..2055082 --- /dev/null +++ b/components/accounts/account-edit-dialog.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { Account, Folder } from "@/lib/types"; +import { accountTypeLabels } from "./constants"; + +interface AccountFormData { + name: string; + type: Account["type"]; + folderId: string; + externalUrl: string; +} + +interface AccountEditDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + formData: AccountFormData; + onFormDataChange: (data: AccountFormData) => void; + folders: Folder[]; + onSave: () => void; +} + +export function AccountEditDialog({ + open, + onOpenChange, + formData, + onFormDataChange, + folders, + onSave, +}: AccountEditDialogProps) { + return ( + + + + Modifier le compte + +
+
+ + + onFormDataChange({ ...formData, name: e.target.value }) + } + /> +
+
+ + +
+
+ + +
+
+ + + onFormDataChange({ ...formData, externalUrl: e.target.value }) + } + placeholder="https://..." + /> +

+ URL personnalisée vers le portail de votre banque +

+
+
+ + +
+
+
+
+ ); +} + diff --git a/components/accounts/constants.ts b/components/accounts/constants.ts new file mode 100644 index 0000000..a3c8151 --- /dev/null +++ b/components/accounts/constants.ts @@ -0,0 +1,16 @@ +import { Wallet, PiggyBank, CreditCard, Building2 } from "lucide-react"; + +export const accountTypeIcons = { + CHECKING: Wallet, + SAVINGS: PiggyBank, + CREDIT_CARD: CreditCard, + OTHER: Building2, +}; + +export const accountTypeLabels = { + CHECKING: "Compte courant", + SAVINGS: "Épargne", + CREDIT_CARD: "Carte de crédit", + OTHER: "Autre", +}; + diff --git a/components/accounts/index.ts b/components/accounts/index.ts new file mode 100644 index 0000000..360f02f --- /dev/null +++ b/components/accounts/index.ts @@ -0,0 +1,4 @@ +export { AccountCard } from "./account-card"; +export { AccountEditDialog } from "./account-edit-dialog"; +export { accountTypeIcons, accountTypeLabels } from "./constants"; + diff --git a/components/categories/category-card.tsx b/components/categories/category-card.tsx new file mode 100644 index 0000000..94ea533 --- /dev/null +++ b/components/categories/category-card.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { CategoryIcon } from "@/components/ui/category-icon"; +import { Pencil, Trash2 } from "lucide-react"; +import type { Category } from "@/lib/types"; + +interface CategoryCardProps { + category: Category; + stats: { total: number; count: number }; + formatCurrency: (amount: number) => string; + onEdit: (category: Category) => void; + onDelete: (categoryId: string) => void; +} + +export function CategoryCard({ + category, + stats, + formatCurrency, + onEdit, + onDelete, +}: CategoryCardProps) { + return ( +
+
+
+ +
+ {category.name} + + {stats.count} opération{stats.count > 1 ? "s" : ""} •{" "} + {formatCurrency(stats.total)} + + {category.keywords.length > 0 && ( + + {category.keywords.length} + + )} +
+ +
+ + +
+
+ ); +} + diff --git a/components/categories/category-edit-dialog.tsx b/components/categories/category-edit-dialog.tsx new file mode 100644 index 0000000..c311507 --- /dev/null +++ b/components/categories/category-edit-dialog.tsx @@ -0,0 +1,192 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Badge } from "@/components/ui/badge"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Plus, X } from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { Category } from "@/lib/types"; +import { categoryColors } from "./constants"; + +interface CategoryFormData { + name: string; + color: string; + keywords: string[]; + parentId: string | null; +} + +interface CategoryEditDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + editingCategory: Category | null; + formData: CategoryFormData; + onFormDataChange: (data: CategoryFormData) => void; + parentCategories: Category[]; + onSave: () => void; +} + +export function CategoryEditDialog({ + open, + onOpenChange, + editingCategory, + formData, + onFormDataChange, + parentCategories, + onSave, +}: CategoryEditDialogProps) { + const [newKeyword, setNewKeyword] = useState(""); + + const addKeyword = () => { + if ( + newKeyword.trim() && + !formData.keywords.includes(newKeyword.trim().toLowerCase()) + ) { + onFormDataChange({ + ...formData, + keywords: [...formData.keywords, newKeyword.trim().toLowerCase()], + }); + setNewKeyword(""); + } + }; + + const removeKeyword = (keyword: string) => { + onFormDataChange({ + ...formData, + keywords: formData.keywords.filter((k) => k !== keyword), + }); + }; + + return ( + + + + + {editingCategory ? "Modifier la catégorie" : "Nouvelle catégorie"} + + +
+ {/* Catégorie parente */} +
+ + +
+ + {/* Nom */} +
+ + + onFormDataChange({ ...formData, name: e.target.value }) + } + placeholder="Ex: Alimentation" + /> +
+ + {/* Couleur */} +
+ +
+ {categoryColors.map((color) => ( +
+
+ + {/* Mots-clés */} +
+ +
+ setNewKeyword(e.target.value)} + placeholder="Ajouter un mot-clé" + onKeyDown={(e) => + e.key === "Enter" && (e.preventDefault(), addKeyword()) + } + /> + +
+
+ {formData.keywords.map((keyword) => ( + + {keyword} + + + ))} +
+
+ + {/* Actions */} +
+ + +
+
+
+
+ ); +} + diff --git a/components/categories/category-search-bar.tsx b/components/categories/category-search-bar.tsx new file mode 100644 index 0000000..d9244d1 --- /dev/null +++ b/components/categories/category-search-bar.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Search, X, ChevronsUpDown } from "lucide-react"; + +interface CategorySearchBarProps { + searchQuery: string; + onSearchChange: (query: string) => void; + allExpanded: boolean; + onToggleAll: () => void; +} + +export function CategorySearchBar({ + searchQuery, + onSearchChange, + allExpanded, + onToggleAll, +}: CategorySearchBarProps) { + return ( +
+
+ + onSearchChange(e.target.value)} + className="pl-9" + /> + {searchQuery && ( + + )} +
+ +
+ ); +} + diff --git a/components/categories/constants.ts b/components/categories/constants.ts new file mode 100644 index 0000000..af21a52 --- /dev/null +++ b/components/categories/constants.ts @@ -0,0 +1,18 @@ +export const categoryColors = [ + "#22c55e", + "#3b82f6", + "#f59e0b", + "#ec4899", + "#ef4444", + "#8b5cf6", + "#06b6d4", + "#84cc16", + "#f97316", + "#6366f1", + "#14b8a6", + "#f43f5e", + "#64748b", + "#0891b2", + "#dc2626", +]; + diff --git a/components/categories/index.ts b/components/categories/index.ts new file mode 100644 index 0000000..8dec9d0 --- /dev/null +++ b/components/categories/index.ts @@ -0,0 +1,6 @@ +export { CategoryCard } from "./category-card"; +export { CategoryEditDialog } from "./category-edit-dialog"; +export { ParentCategoryRow } from "./parent-category-row"; +export { CategorySearchBar } from "./category-search-bar"; +export { categoryColors } from "./constants"; + diff --git a/components/categories/parent-category-row.tsx b/components/categories/parent-category-row.tsx new file mode 100644 index 0000000..bede9d2 --- /dev/null +++ b/components/categories/parent-category-row.tsx @@ -0,0 +1,140 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; +import { CategoryIcon } from "@/components/ui/category-icon"; +import { + Plus, + MoreVertical, + Pencil, + Trash2, + ChevronDown, + ChevronRight, +} from "lucide-react"; +import { CategoryCard } from "./category-card"; +import type { Category } from "@/lib/types"; + +interface ParentCategoryRowProps { + parent: Category; + children: Category[]; + stats: { total: number; count: number }; + isExpanded: boolean; + onToggleExpanded: () => void; + formatCurrency: (amount: number) => string; + getCategoryStats: (categoryId: string) => { total: number; count: number }; + onEdit: (category: Category) => void; + onDelete: (categoryId: string) => void; + onNewCategory: (parentId: string) => void; +} + +export function ParentCategoryRow({ + parent, + children, + stats, + isExpanded, + onToggleExpanded, + formatCurrency, + getCategoryStats, + onEdit, + onDelete, + onNewCategory, +}: ParentCategoryRowProps) { + return ( +
+ +
+ + + + +
+ + + + + + + onEdit(parent)}> + + Modifier + + onDelete(parent.id)} + className="text-red-600" + > + + Supprimer + + + +
+
+ + + {children.length > 0 ? ( +
+ {children.map((child) => ( + + ))} +
+ ) : ( +
+ Aucune sous-catégorie +
+ )} +
+
+
+ ); +} + diff --git a/components/folders/account-folder-dialog.tsx b/components/folders/account-folder-dialog.tsx new file mode 100644 index 0000000..431d964 --- /dev/null +++ b/components/folders/account-folder-dialog.tsx @@ -0,0 +1,118 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import type { Account, Folder } from "@/lib/types"; +import { accountTypeLabels } from "./constants"; + +interface AccountFormData { + name: string; + type: Account["type"]; + folderId: string; +} + +interface AccountFolderDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + formData: AccountFormData; + onFormDataChange: (data: AccountFormData) => void; + folders: Folder[]; + onSave: () => void; +} + +export function AccountFolderDialog({ + open, + onOpenChange, + formData, + onFormDataChange, + folders, + onSave, +}: AccountFolderDialogProps) { + return ( + + + + Modifier le compte + +
+
+ + + onFormDataChange({ + ...formData, + name: e.target.value, + }) + } + /> +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ ); +} + diff --git a/components/folders/constants.ts b/components/folders/constants.ts new file mode 100644 index 0000000..62d8c54 --- /dev/null +++ b/components/folders/constants.ts @@ -0,0 +1,16 @@ +export const folderColors = [ + { value: "#6366f1", label: "Indigo" }, + { value: "#22c55e", label: "Vert" }, + { value: "#f59e0b", label: "Orange" }, + { value: "#ec4899", label: "Rose" }, + { value: "#3b82f6", label: "Bleu" }, + { value: "#ef4444", label: "Rouge" }, +]; + +export const accountTypeLabels = { + CHECKING: "Compte courant", + SAVINGS: "Épargne", + CREDIT_CARD: "Carte de crédit", + OTHER: "Autre", +}; + diff --git a/components/folders/folder-edit-dialog.tsx b/components/folders/folder-edit-dialog.tsx new file mode 100644 index 0000000..304bfe5 --- /dev/null +++ b/components/folders/folder-edit-dialog.tsx @@ -0,0 +1,123 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import type { Folder } from "@/lib/types"; +import { folderColors } from "./constants"; + +interface FolderFormData { + name: string; + parentId: string | null; + color: string; +} + +interface FolderEditDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + editingFolder: Folder | null; + formData: FolderFormData; + onFormDataChange: (data: FolderFormData) => void; + folders: Folder[]; + onSave: () => void; +} + +export function FolderEditDialog({ + open, + onOpenChange, + editingFolder, + formData, + onFormDataChange, + folders, + onSave, +}: FolderEditDialogProps) { + return ( + + + + + {editingFolder ? "Modifier le dossier" : "Nouveau dossier"} + + +
+
+ + + onFormDataChange({ ...formData, name: e.target.value }) + } + placeholder="Ex: Comptes personnels" + /> +
+
+ + +
+
+ +
+ {folderColors.map(({ value }) => ( +
+
+
+ + +
+
+
+
+ ); +} + diff --git a/components/folders/folder-tree-item.tsx b/components/folders/folder-tree-item.tsx new file mode 100644 index 0000000..715353b --- /dev/null +++ b/components/folders/folder-tree-item.tsx @@ -0,0 +1,189 @@ +"use client"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { + MoreVertical, + Pencil, + Trash2, + Folder, + FolderOpen, + ChevronRight, + ChevronDown, + Building2, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import Link from "next/link"; +import type { Folder as FolderType, Account } from "@/lib/types"; + +interface FolderTreeItemProps { + folder: FolderType; + accounts: Account[]; + allFolders: FolderType[]; + level: number; + onEdit: (folder: FolderType) => void; + onDelete: (folderId: string) => void; + onEditAccount: (account: Account) => void; + formatCurrency: (amount: number) => string; +} + +export function FolderTreeItem({ + folder, + accounts, + allFolders, + level, + onEdit, + onDelete, + onEditAccount, + formatCurrency, +}: FolderTreeItemProps) { + const [isExpanded, setIsExpanded] = useState(true); + + // Pour le dossier "Mes Comptes" (folder-root), inclure aussi les comptes sans dossier + const folderAccounts = accounts.filter( + (a) => + a.folderId === folder.id || + (folder.id === "folder-root" && a.folderId === null) + ); + const childFolders = allFolders.filter((f) => f.parentId === folder.id); + const hasChildren = childFolders.length > 0 || folderAccounts.length > 0; + + const folderTotal = folderAccounts.reduce((sum, a) => sum + a.balance, 0); + + return ( +
+
0 && "ml-6" + )} + > + + +
+ {isExpanded ? ( + + ) : ( + + )} +
+ + {folder.name} + + {folderAccounts.length > 0 && ( + = 0 ? "text-emerald-600" : "text-red-600" + )} + > + {formatCurrency(folderTotal)} + + )} + + + + + + + onEdit(folder)}> + + Modifier + + {folder.id !== "folder-root" && ( + onDelete(folder.id)} + className="text-red-600" + > + + Supprimer + + )} + + +
+ + {isExpanded && ( +
+ {folderAccounts.map((account) => ( +
+ + + {account.name} + + = 0 ? "text-emerald-600" : "text-red-600" + )} + > + {formatCurrency(account.balance)} + + +
+ ))} + + {childFolders.map((child) => ( + + ))} +
+ )} +
+ ); +} + diff --git a/components/folders/index.ts b/components/folders/index.ts new file mode 100644 index 0000000..427d1b5 --- /dev/null +++ b/components/folders/index.ts @@ -0,0 +1,5 @@ +export { FolderTreeItem } from "./folder-tree-item"; +export { FolderEditDialog } from "./folder-edit-dialog"; +export { AccountFolderDialog } from "./account-folder-dialog"; +export { folderColors, accountTypeLabels } from "./constants"; + diff --git a/components/layout/index.ts b/components/layout/index.ts new file mode 100644 index 0000000..5b4682e --- /dev/null +++ b/components/layout/index.ts @@ -0,0 +1,4 @@ +export { PageLayout } from "./page-layout"; +export { LoadingState } from "./loading-state"; +export { PageHeader } from "./page-header"; + diff --git a/components/layout/loading-state.tsx b/components/layout/loading-state.tsx new file mode 100644 index 0000000..5b4e7a9 --- /dev/null +++ b/components/layout/loading-state.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { Sidebar } from "@/components/dashboard/sidebar"; +import { RefreshCw } from "lucide-react"; + +export function LoadingState() { + return ( +
+ +
+ +
+
+ ); +} + diff --git a/components/layout/page-header.tsx b/components/layout/page-header.tsx new file mode 100644 index 0000000..98f80f6 --- /dev/null +++ b/components/layout/page-header.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { ReactNode } from "react"; + +interface PageHeaderProps { + title: string; + description?: string; + actions?: ReactNode; + rightContent?: ReactNode; +} + +export function PageHeader({ + title, + description, + actions, + rightContent, +}: PageHeaderProps) { + return ( +
+
+

{title}

+ {description && ( +

{description}

+ )} +
+ {rightContent} + {actions &&
{actions}
} +
+ ); +} + diff --git a/components/layout/page-layout.tsx b/components/layout/page-layout.tsx new file mode 100644 index 0000000..db4bb6d --- /dev/null +++ b/components/layout/page-layout.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { Sidebar } from "@/components/dashboard/sidebar"; +import { ReactNode } from "react"; + +interface PageLayoutProps { + children: ReactNode; +} + +export function PageLayout({ children }: PageLayoutProps) { + return ( +
+ +
+
{children}
+
+
+ ); +} + diff --git a/components/settings/danger-zone-card.tsx b/components/settings/danger-zone-card.tsx new file mode 100644 index 0000000..e4d4066 --- /dev/null +++ b/components/settings/danger-zone-card.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Trash2, Tags } from "lucide-react"; + +interface DangerZoneCardProps { + categorizedCount: number; + onClearCategories: () => void; + onResetData: () => void; +} + +export function DangerZoneCard({ + categorizedCount, + onClearCategories, + onResetData, +}: DangerZoneCardProps) { + return ( + + + + + Zone dangereuse + + + Actions irréversibles - procédez avec prudence + + + + {/* Supprimer catégories des opérations */} + + + + + + + + Supprimer toutes les catégories ? + + + Cette action va retirer la catégorie de {categorizedCount}{" "} + opération{categorizedCount > 1 ? "s" : ""}. Les catégories + elles-mêmes ne seront pas supprimées, seulement leur + affectation aux opérations. + + + + Annuler + + Supprimer les affectations + + + + + + {/* Réinitialiser toutes les données */} + + + + + + + Êtes-vous sûr ? + + Cette action supprimera définitivement tous vos comptes, + transactions, catégories et dossiers. Cette action est + irréversible. + + + + Annuler + + Supprimer tout + + + + + + + ); +} + diff --git a/components/settings/data-card.tsx b/components/settings/data-card.tsx new file mode 100644 index 0000000..53fdc04 --- /dev/null +++ b/components/settings/data-card.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Download, Upload, Database } from "lucide-react"; +import type { BankingData } from "@/lib/types"; + +interface DataCardProps { + data: BankingData; + importing: boolean; + onExport: () => void; + onImport: () => void; +} + +export function DataCard({ + data, + importing, + onExport, + onImport, +}: DataCardProps) { + return ( + + + + + Données + + + Exportez ou importez vos données pour les sauvegarder ou les + transférer + + + +
+
+

Statistiques

+

+ {data.accounts.length} comptes, {data.transactions.length}{" "} + transactions, {data.categories.length} catégories +

+
+
+ +
+ + +
+
+
+ ); +} + diff --git a/components/settings/index.ts b/components/settings/index.ts new file mode 100644 index 0000000..36af500 --- /dev/null +++ b/components/settings/index.ts @@ -0,0 +1,4 @@ +export { DataCard } from "./data-card"; +export { DangerZoneCard } from "./danger-zone-card"; +export { OFXInfoCard } from "./ofx-info-card"; + diff --git a/components/settings/ofx-info-card.tsx b/components/settings/ofx-info-card.tsx new file mode 100644 index 0000000..1b127e2 --- /dev/null +++ b/components/settings/ofx-info-card.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { FileJson } from "lucide-react"; + +export function OFXInfoCard() { + return ( + + + + + Format OFX + + + Informations sur l'import de fichiers + + + +
+

+ L'application accepte les fichiers au format OFX (Open Financial + Exchange) ou QFX. Ces fichiers sont généralement disponibles depuis + l'espace client de votre banque. +

+

+ Lors de l'import, les transactions sont automatiquement + catégorisées selon les mots-clés définis. Les doublons sont détectés + et ignorés automatiquement. +

+
+
+
+ ); +} + diff --git a/components/statistics/balance-line-chart.tsx b/components/statistics/balance-line-chart.tsx new file mode 100644 index 0000000..b3bfe7f --- /dev/null +++ b/components/statistics/balance-line-chart.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; + +interface BalanceChartData { + date: string; + solde: number; +} + +interface BalanceLineChartProps { + data: BalanceChartData[]; + formatCurrency: (amount: number) => string; +} + +export function BalanceLineChart({ + data, + formatCurrency, +}: BalanceLineChartProps) { + return ( + + + Évolution du solde + + + {data.length > 0 ? ( +
+ + + + + `${v}€`} /> + formatCurrency(value)} + contentStyle={{ + backgroundColor: "hsl(var(--card))", + border: "1px solid hsl(var(--border))", + borderRadius: "8px", + }} + /> + + + +
+ ) : ( +
+ Pas de données pour cette période +
+ )} +
+
+ ); +} + diff --git a/components/statistics/category-pie-chart.tsx b/components/statistics/category-pie-chart.tsx new file mode 100644 index 0000000..e472e7f --- /dev/null +++ b/components/statistics/category-pie-chart.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + PieChart, + Pie, + Cell, + Tooltip, + ResponsiveContainer, + Legend, +} from "recharts"; + +interface CategoryChartData { + name: string; + value: number; + color: string; +} + +interface CategoryPieChartProps { + data: CategoryChartData[]; + formatCurrency: (amount: number) => string; +} + +export function CategoryPieChart({ + data, + formatCurrency, +}: CategoryPieChartProps) { + return ( + + + Répartition par catégorie + + + {data.length > 0 ? ( +
+ + + + {data.map((entry, index) => ( + + ))} + + formatCurrency(value)} + contentStyle={{ + backgroundColor: "hsl(var(--card))", + border: "1px solid hsl(var(--border))", + borderRadius: "8px", + }} + /> + ( + {value} + )} + /> + + +
+ ) : ( +
+ Pas de données pour cette période +
+ )} +
+
+ ); +} + diff --git a/components/statistics/index.ts b/components/statistics/index.ts new file mode 100644 index 0000000..0f4e42e --- /dev/null +++ b/components/statistics/index.ts @@ -0,0 +1,6 @@ +export { StatsSummaryCards } from "./stats-summary-cards"; +export { MonthlyChart } from "./monthly-chart"; +export { CategoryPieChart } from "./category-pie-chart"; +export { BalanceLineChart } from "./balance-line-chart"; +export { TopExpensesList } from "./top-expenses-list"; + diff --git a/components/statistics/monthly-chart.tsx b/components/statistics/monthly-chart.tsx new file mode 100644 index 0000000..9020e64 --- /dev/null +++ b/components/statistics/monthly-chart.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Legend, +} from "recharts"; + +interface MonthlyChartData { + month: string; + revenus: number; + depenses: number; + solde: number; +} + +interface MonthlyChartProps { + data: MonthlyChartData[]; + formatCurrency: (amount: number) => string; +} + +export function MonthlyChart({ data, formatCurrency }: MonthlyChartProps) { + return ( + + + Revenus vs Dépenses par mois + + + {data.length > 0 ? ( +
+ + + + + `${v}€`} /> + formatCurrency(value)} + contentStyle={{ + backgroundColor: "hsl(var(--card))", + border: "1px solid hsl(var(--border))", + borderRadius: "8px", + }} + /> + + + + + +
+ ) : ( +
+ Pas de données pour cette période +
+ )} +
+
+ ); +} + diff --git a/components/statistics/stats-summary-cards.tsx b/components/statistics/stats-summary-cards.tsx new file mode 100644 index 0000000..9597042 --- /dev/null +++ b/components/statistics/stats-summary-cards.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { TrendingUp, TrendingDown, ArrowRight } from "lucide-react"; +import { cn } from "@/lib/utils"; + +interface StatsSummaryCardsProps { + totalIncome: number; + totalExpenses: number; + avgMonthlyExpenses: number; + formatCurrency: (amount: number) => string; +} + +export function StatsSummaryCards({ + totalIncome, + totalExpenses, + avgMonthlyExpenses, + formatCurrency, +}: StatsSummaryCardsProps) { + const savings = totalIncome - totalExpenses; + + return ( +
+ + + + + Total Revenus + + + +
+ {formatCurrency(totalIncome)} +
+
+
+ + + + + + Total Dépenses + + + +
+ {formatCurrency(totalExpenses)} +
+
+
+ + + + + + Moyenne mensuelle + + + +
+ {formatCurrency(avgMonthlyExpenses)} +
+
+
+ + + + + Économies + + + +
= 0 ? "text-emerald-600" : "text-red-600" + )} + > + {formatCurrency(savings)} +
+
+
+
+ ); +} + diff --git a/components/statistics/top-expenses-list.tsx b/components/statistics/top-expenses-list.tsx new file mode 100644 index 0000000..26dd3a7 --- /dev/null +++ b/components/statistics/top-expenses-list.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { CategoryIcon } from "@/components/ui/category-icon"; +import type { Transaction, Category } from "@/lib/types"; + +interface TopExpensesListProps { + expenses: Transaction[]; + categories: Category[]; + formatCurrency: (amount: number) => string; +} + +export function TopExpensesList({ + expenses, + categories, + formatCurrency, +}: TopExpensesListProps) { + return ( + + + Top 5 dépenses + + + {expenses.length > 0 ? ( +
+ {expenses.map((expense, index) => { + const category = categories.find( + (c) => c.id === expense.categoryId + ); + return ( +
+
+ {index + 1} +
+
+

+ {expense.description} +

+
+ + {new Date(expense.date).toLocaleDateString("fr-FR")} + + {category && ( + + + {category.name} + + )} +
+
+
+ {formatCurrency(expense.amount)} +
+
+ ); + })} +
+ ) : ( +
+ Pas de dépenses pour cette période +
+ )} +
+
+ ); +} + diff --git a/components/transactions/index.ts b/components/transactions/index.ts new file mode 100644 index 0000000..c31b2e0 --- /dev/null +++ b/components/transactions/index.ts @@ -0,0 +1,4 @@ +export { TransactionFilters } from "./transaction-filters"; +export { TransactionBulkActions } from "./transaction-bulk-actions"; +export { TransactionTable } from "./transaction-table"; + diff --git a/components/transactions/transaction-bulk-actions.tsx b/components/transactions/transaction-bulk-actions.tsx new file mode 100644 index 0000000..deec1e6 --- /dev/null +++ b/components/transactions/transaction-bulk-actions.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { CategoryIcon } from "@/components/ui/category-icon"; +import { CheckCircle2, Circle, Tags } from "lucide-react"; +import type { Category } from "@/lib/types"; + +interface TransactionBulkActionsProps { + selectedCount: number; + categories: Category[]; + onReconcile: (reconciled: boolean) => void; + onSetCategory: (categoryId: string | null) => void; +} + +export function TransactionBulkActions({ + selectedCount, + categories, + onReconcile, + onSetCategory, +}: TransactionBulkActionsProps) { + if (selectedCount === 0) return null; + + return ( + + +
+ + {selectedCount} sélectionnée{selectedCount > 1 ? "s" : ""} + + + + + + + + + onSetCategory(null)}> + Aucune catégorie + + + {categories.map((cat) => ( + onSetCategory(cat.id)} + > + + {cat.name} + + ))} + + +
+
+
+ ); +} + diff --git a/components/transactions/transaction-filters.tsx b/components/transactions/transaction-filters.tsx new file mode 100644 index 0000000..2c02b46 --- /dev/null +++ b/components/transactions/transaction-filters.tsx @@ -0,0 +1,100 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Search } from "lucide-react"; +import type { Account, Category } from "@/lib/types"; + +interface TransactionFiltersProps { + searchQuery: string; + onSearchChange: (query: string) => void; + selectedAccount: string; + onAccountChange: (account: string) => void; + selectedCategory: string; + onCategoryChange: (category: string) => void; + showReconciled: string; + onReconciledChange: (value: string) => void; + accounts: Account[]; + categories: Category[]; +} + +export function TransactionFilters({ + searchQuery, + onSearchChange, + selectedAccount, + onAccountChange, + selectedCategory, + onCategoryChange, + showReconciled, + onReconciledChange, + accounts, + categories, +}: TransactionFiltersProps) { + return ( + + +
+
+
+ + onSearchChange(e.target.value)} + className="pl-9" + /> +
+
+ + + + + + +
+
+
+ ); +} + diff --git a/components/transactions/transaction-table.tsx b/components/transactions/transaction-table.tsx new file mode 100644 index 0000000..435d940 --- /dev/null +++ b/components/transactions/transaction-table.tsx @@ -0,0 +1,277 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { Card, CardContent } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Badge } from "@/components/ui/badge"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + DropdownMenuSeparator, +} from "@/components/ui/dropdown-menu"; +import { CategoryIcon } from "@/components/ui/category-icon"; +import { + CheckCircle2, + Circle, + MoreVertical, + ArrowUpDown, + Check, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import type { Transaction, Account, Category } from "@/lib/types"; + +type SortField = "date" | "amount" | "description"; +type SortOrder = "asc" | "desc"; + +interface TransactionTableProps { + transactions: Transaction[]; + accounts: Account[]; + categories: Category[]; + selectedTransactions: Set; + sortField: SortField; + sortOrder: SortOrder; + onSortChange: (field: SortField) => void; + onToggleSelectAll: () => void; + onToggleSelectTransaction: (id: string) => void; + onToggleReconciled: (id: string) => void; + onSetCategory: (transactionId: string, categoryId: string | null) => void; + formatCurrency: (amount: number) => string; + formatDate: (dateStr: string) => string; +} + +export function TransactionTable({ + transactions, + accounts, + categories, + selectedTransactions, + sortField, + sortOrder, + onSortChange, + onToggleSelectAll, + onToggleSelectTransaction, + onToggleReconciled, + onSetCategory, + formatCurrency, + formatDate, +}: TransactionTableProps) { + const getCategory = (categoryId: string | null) => { + if (!categoryId) return null; + return categories.find((c) => c.id === categoryId); + }; + + const getAccount = (accountId: string) => { + return accounts.find((a) => a.id === accountId); + }; + + return ( + + + {transactions.length === 0 ? ( +
+

Aucune transaction trouvée

+
+ ) : ( +
+ + + + + + + + + + + + + + + {transactions.map((transaction) => { + const category = getCategory(transaction.categoryId); + const account = getAccount(transaction.accountId); + + return ( + + + + + + + + + + + ); + })} + +
+ 0 + } + onCheckedChange={onToggleSelectAll} + /> + + + + + + Compte + + Catégorie + + + + Pointé +
+ + onToggleSelectTransaction(transaction.id) + } + /> + + {formatDate(transaction.date)} + +

+ {transaction.description} +

+ {transaction.memo && ( +

+ {transaction.memo} +

+ )} +
+ {account?.name || "-"} + + + + + + + onSetCategory(transaction.id, null)} + > + Aucune catégorie + + + {categories.map((cat) => ( + + onSetCategory(transaction.id, cat.id) + } + > + + {cat.name} + {transaction.categoryId === cat.id && ( + + )} + + ))} + + + = 0 + ? "text-emerald-600" + : "text-red-600" + )} + > + {transaction.amount >= 0 ? "+" : ""} + {formatCurrency(transaction.amount)} + + + + + + + + + onToggleReconciled(transaction.id)} + > + {transaction.isReconciled + ? "Dépointer" + : "Pointer"} + + + +
+
+ )} +
+
+ ); +} +