diff --git a/components/rules/rule-create-dialog.tsx b/components/rules/rule-create-dialog.tsx index 55022fb..62b4a9c 100644 --- a/components/rules/rule-create-dialog.tsx +++ b/components/rules/rule-create-dialog.tsx @@ -13,15 +13,8 @@ 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 { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; -import { CategoryIcon } from "@/components/ui/category-icon"; +import { CategoryCombobox } from "@/components/ui/category-combobox"; import { Tag, AlertCircle, CheckCircle2 } from "lucide-react"; import type { Category, Transaction } from "@/lib/types"; @@ -54,7 +47,7 @@ export function RuleCreateDialog({ onSave, }: RuleCreateDialogProps) { const [keyword, setKeyword] = useState(""); - const [categoryId, setCategoryId] = useState(""); + const [categoryId, setCategoryId] = useState(null); const [applyToExisting, setApplyToExisting] = useState(true); const [isLoading, setIsLoading] = useState(false); @@ -62,28 +55,11 @@ export function RuleCreateDialog({ useEffect(() => { if (group) { setKeyword(group.suggestedKeyword); - setCategoryId(""); + setCategoryId(null); setApplyToExisting(true); } }, [group]); - // Organize categories by parent - const { parentCategories, childrenByParent } = useMemo(() => { - const parents = categories.filter((c) => c.parentId === null); - const children: Record = {}; - - categories - .filter((c) => c.parentId !== null) - .forEach((child) => { - if (!children[child.parentId!]) { - children[child.parentId!] = []; - } - children[child.parentId!].push(child); - }); - - return { parentCategories: parents, childrenByParent: children }; - }, [categories]); - // Check if keyword already exists in any category const existingCategory = useMemo(() => { if (!keyword) return null; @@ -166,42 +142,16 @@ export function RuleCreateDialog({ )} - {/* Category select */} + {/* Category select with search */}
- - + + {selectedCategory && (
diff --git a/components/transactions/transaction-table.tsx b/components/transactions/transaction-table.tsx index 5c00ac6..2d95241 100644 --- a/components/transactions/transaction-table.tsx +++ b/components/transactions/transaction-table.tsx @@ -4,21 +4,18 @@ import { useEffect, useRef, useState, useCallback } from "react"; 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 { CategoryCombobox } from "@/components/ui/category-combobox"; import { CheckCircle2, Circle, MoreVertical, ArrowUpDown, - Check, } from "lucide-react"; import { cn } from "@/lib/utils"; import type { Transaction, Account, Category } from "@/lib/types"; @@ -110,11 +107,6 @@ export function TransactionTable({ useEffect(() => { setFocusedIndex(null); }, [transactions.length]); - 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); }; @@ -181,7 +173,6 @@ export function TransactionTable({ {transactions.map((transaction, index) => { - const category = getCategory(transaction.categoryId); const account = getAccount(transaction.accountId); const isFocused = focusedIndex === index; @@ -222,64 +213,16 @@ export function TransactionTable({ {account?.name || "-"} - - - - - - - onSetCategory(transaction.id, null)} - > - Aucune catégorie - - - {categories.map((cat) => ( - - onSetCategory(transaction.id, cat.id) - } - > - - {cat.name} - {transaction.categoryId === cat.id && ( - - )} - - ))} - - + e.stopPropagation()}> + + onSetCategory(transaction.id, categoryId) + } + showBadge + align="start" + /> void; + placeholder?: string; + showBadge?: boolean; + align?: "start" | "center" | "end"; + width?: string; +} + +export function CategoryCombobox({ + categories, + value, + onChange, + placeholder = "Sélectionner...", + showBadge = false, + align = "start", + width = "w-[300px]", +}: CategoryComboboxProps) { + const [open, setOpen] = useState(false); + + // Organize categories by parent + const { parentCategories, childrenByParent } = useMemo(() => { + const parents = categories.filter((c) => c.parentId === null); + const children: Record = {}; + + categories + .filter((c) => c.parentId !== null) + .forEach((child) => { + if (!children[child.parentId!]) { + children[child.parentId!] = []; + } + children[child.parentId!].push(child); + }); + + return { parentCategories: parents, childrenByParent: children }; + }, [categories]); + + const selectedCategory = categories.find((c) => c.id === value); + + const handleSelect = (categoryId: string | null) => { + onChange(categoryId); + setOpen(false); + }; + + // Badge style trigger + if (showBadge) { + return ( + + + + + e.preventDefault()} + > + + + + Aucune catégorie trouvée. + + handleSelect(null)} + > + + Aucune catégorie + + + + + {parentCategories.map((parent) => ( +
+ handleSelect(parent.id)} + > + + {parent.name} + + + {childrenByParent[parent.id]?.map((child) => ( + handleSelect(child.id)} + className="pl-8" + > + + {child.name} + + + ))} +
+ ))} +
+
+
+
+
+ ); + } + + // Button style trigger (default) + return ( + + + + + e.preventDefault()} + > + + + + Aucune catégorie trouvée. + + {parentCategories.map((parent) => ( +
+ handleSelect(parent.id)} + > + + {parent.name} + + + {childrenByParent[parent.id]?.map((child) => ( + handleSelect(child.id)} + className="pl-8" + > + + {child.name} + + + ))} +
+ ))} +
+
+
+
+
+ ); +} +