From f366ea02c5b203d6b66b313ffbc0f98d6cdf53a6 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Sun, 30 Nov 2025 16:48:55 +0100 Subject: [PATCH] feat: add categorization feature for transaction groups with UI enhancements for category selection --- app/rules/page.tsx | 22 ++++++++ components/rules/rule-group-card.tsx | 45 +++++++++++---- .../transactions/transaction-bulk-actions.tsx | 55 ++++++------------- components/ui/category-combobox.tsx | 22 +++++++- 4 files changed, 95 insertions(+), 49 deletions(-) diff --git a/app/rules/page.tsx b/app/rules/page.tsx index fc14d76..914f13b 100644 --- a/app/rules/page.tsx +++ b/app/rules/page.tsx @@ -210,6 +210,25 @@ export default function RulesPage() { } }, [data, refresh]); + const handleCategorizeGroup = useCallback( + async (group: TransactionGroup, categoryId: string | null) => { + if (!data) return; + + try { + await Promise.all( + group.transactions.map((t) => + updateTransaction({ ...t, categoryId }) + ) + ); + refresh(); + } catch (error) { + console.error("Error categorizing group:", error); + alert("Erreur lors de la catégorisation"); + } + }, + [data, refresh] + ); + if (isLoading || !data) { return ; } @@ -277,6 +296,9 @@ export default function RulesPage() { isExpanded={expandedGroups.has(group.key)} onToggleExpand={() => toggleExpand(group.key)} onCreateRule={() => handleCreateRule(group)} + onCategorize={(categoryId) => + handleCategorizeGroup(group, categoryId) + } categories={data.categories} formatCurrency={formatCurrency} formatDate={formatDate} diff --git a/components/rules/rule-group-card.tsx b/components/rules/rule-group-card.tsx index d2ba39b..b5024d2 100644 --- a/components/rules/rule-group-card.tsx +++ b/components/rules/rule-group-card.tsx @@ -1,8 +1,10 @@ "use client"; +import { useState } from "react"; import { ChevronDown, ChevronRight, Plus, Tag } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; +import { CategoryCombobox } from "@/components/ui/category-combobox"; import { cn } from "@/lib/utils"; import type { Transaction, Category } from "@/lib/types"; @@ -19,6 +21,7 @@ interface RuleGroupCardProps { isExpanded: boolean; onToggleExpand: () => void; onCreateRule: () => void; + onCategorize: (categoryId: string | null) => void; categories: Category[]; formatCurrency: (amount: number) => string; formatDate: (date: string) => string; @@ -29,14 +32,23 @@ export function RuleGroupCard({ isExpanded, onToggleExpand, onCreateRule, + onCategorize, + categories, formatCurrency, formatDate, }: RuleGroupCardProps) { + const [selectedCategoryId, setSelectedCategoryId] = useState(null); + const avgAmount = group.transactions.reduce((sum, t) => sum + t.amount, 0) / group.transactions.length; const isDebit = avgAmount < 0; + const handleCategorySelect = (categoryId: string | null) => { + setSelectedCategoryId(null); // Reset après sélection + onCategorize(categoryId); + }; + return (
{/* Header */} @@ -85,17 +97,28 @@ export function RuleGroupCard({
- +
+
e.stopPropagation()}> + +
+ +
diff --git a/components/transactions/transaction-bulk-actions.tsx b/components/transactions/transaction-bulk-actions.tsx index deec1e6..39dc9dc 100644 --- a/components/transactions/transaction-bulk-actions.tsx +++ b/components/transactions/transaction-bulk-actions.tsx @@ -1,16 +1,10 @@ "use client"; +import { useState } from "react"; 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 { CategoryCombobox } from "@/components/ui/category-combobox"; +import { CheckCircle2, Circle } from "lucide-react"; import type { Category } from "@/lib/types"; interface TransactionBulkActionsProps { @@ -26,8 +20,15 @@ export function TransactionBulkActions({ onReconcile, onSetCategory, }: TransactionBulkActionsProps) { + const [selectedCategoryId, setSelectedCategoryId] = useState(null); + if (selectedCount === 0) return null; + const handleCategorySelect = (categoryId: string | null) => { + setSelectedCategoryId(null); // Reset après sélection + onSetCategory(categoryId); + }; + return ( @@ -47,34 +48,14 @@ export function TransactionBulkActions({ Dépointer - - - - - - onSetCategory(null)}> - Aucune catégorie - - - {categories.map((cat) => ( - onSetCategory(cat.id)} - > - - {cat.name} - - ))} - - + diff --git a/components/ui/category-combobox.tsx b/components/ui/category-combobox.tsx index b97e9a4..6f3dea3 100644 --- a/components/ui/category-combobox.tsx +++ b/components/ui/category-combobox.tsx @@ -29,6 +29,7 @@ interface CategoryComboboxProps { showBadge?: boolean; align?: "start" | "center" | "end"; width?: string; + buttonWidth?: string; } export function CategoryCombobox({ @@ -39,6 +40,7 @@ export function CategoryCombobox({ showBadge = false, align = "start", width = "w-[300px]", + buttonWidth, }: CategoryComboboxProps) { const [open, setOpen] = useState(false); @@ -181,7 +183,10 @@ export function CategoryCombobox({ variant="outline" role="combobox" aria-expanded={open} - className="w-full justify-between" + className={cn( + "justify-between", + buttonWidth || "w-full" + )} > {selectedCategory ? (
@@ -207,6 +212,21 @@ export function CategoryCombobox({ Aucune catégorie trouvée. + + handleSelect(null)} + > + + Aucune catégorie + + + {parentCategories.map((parent) => (