diff --git a/app/categories/page.tsx b/app/categories/page.tsx index 8da0b93..e1c46ff 100644 --- a/app/categories/page.tsx +++ b/app/categories/page.tsx @@ -29,6 +29,7 @@ export default function CategoriesPage() { const [formData, setFormData] = useState({ name: "", color: "#22c55e", + icon: "tag", keywords: [] as string[], parentId: null as string | null, }); @@ -132,7 +133,7 @@ export default function CategoriesPage() { const handleNewCategory = (parentId: string | null = null) => { setEditingCategory(null); - setFormData({ name: "", color: "#22c55e", keywords: [], parentId }); + setFormData({ name: "", color: "#22c55e", icon: "tag", keywords: [], parentId }); setIsDialogOpen(true); }; @@ -141,6 +142,7 @@ export default function CategoriesPage() { setFormData({ name: category.name, color: category.color, + icon: category.icon, keywords: [...category.keywords], parentId: category.parentId, }); @@ -154,6 +156,7 @@ export default function CategoriesPage() { ...editingCategory, name: formData.name, color: formData.color, + icon: formData.icon, keywords: formData.keywords, parentId: formData.parentId, }); @@ -161,8 +164,8 @@ export default function CategoriesPage() { await addCategory({ name: formData.name, color: formData.color, + icon: formData.icon, keywords: formData.keywords, - icon: "tag", parentId: formData.parentId, }); } diff --git a/app/transactions/page.tsx b/app/transactions/page.tsx index bb53eb8..6d9e9c9 100644 --- a/app/transactions/page.tsx +++ b/app/transactions/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState, useMemo, useEffect } from "react"; +import { useState, useMemo, useEffect, useCallback } from "react"; import { useSearchParams } from "next/navigation"; import { PageLayout, LoadingState, PageHeader } from "@/components/layout"; import { @@ -8,10 +8,17 @@ import { TransactionBulkActions, TransactionTable, } from "@/components/transactions"; +import { RuleCreateDialog } from "@/components/rules"; import { OFXImportDialog } from "@/components/import/ofx-import-dialog"; import { useBankingData } from "@/lib/hooks"; +import { updateCategory, updateTransaction } from "@/lib/store-db"; import { Button } from "@/components/ui/button"; import { Upload } from "lucide-react"; +import type { Transaction } from "@/lib/types"; +import { + normalizeDescription, + suggestKeyword, +} from "@/components/rules/constants"; type SortField = "date" | "amount" | "description"; type SortOrder = "asc" | "desc"; @@ -36,6 +43,8 @@ export default function TransactionsPage() { const [selectedTransactions, setSelectedTransactions] = useState>( new Set() ); + const [ruleDialogOpen, setRuleDialogOpen] = useState(false); + const [ruleTransaction, setRuleTransaction] = useState(null); const filteredTransactions = useMemo(() => { if (!data) return []; @@ -101,6 +110,76 @@ export default function TransactionsPage() { sortOrder, ]); + const handleCreateRule = useCallback((transaction: Transaction) => { + setRuleTransaction(transaction); + setRuleDialogOpen(true); + }, []); + + // Create a virtual group for the rule dialog based on selected transaction + const ruleGroup = useMemo(() => { + if (!ruleTransaction || !data) return null; + + // Find similar transactions (same normalized description) + const normalizedDesc = normalizeDescription(ruleTransaction.description); + const similarTransactions = data.transactions.filter( + (t) => normalizeDescription(t.description) === normalizedDesc + ); + + return { + key: normalizedDesc, + displayName: ruleTransaction.description, + transactions: similarTransactions, + totalAmount: similarTransactions.reduce((sum, t) => sum + t.amount, 0), + suggestedKeyword: suggestKeyword(similarTransactions.map((t) => t.description)), + }; + }, [ruleTransaction, data]); + + const handleSaveRule = useCallback( + async (ruleData: { + keyword: string; + categoryId: string; + applyToExisting: boolean; + transactionIds: string[]; + }) => { + if (!data) return; + + // 1. Add keyword to category + const category = data.categories.find((c) => c.id === ruleData.categoryId); + if (!category) { + throw new Error("Category not found"); + } + + // Check if keyword already exists + const keywordExists = category.keywords.some( + (k) => k.toLowerCase() === ruleData.keyword.toLowerCase() + ); + + if (!keywordExists) { + await updateCategory({ + ...category, + keywords: [...category.keywords, ruleData.keyword], + }); + } + + // 2. Apply to existing transactions if requested + if (ruleData.applyToExisting) { + const transactions = data.transactions.filter((t) => + ruleData.transactionIds.includes(t.id) + ); + + await Promise.all( + transactions.map((t) => + updateTransaction({ ...t, categoryId: ruleData.categoryId }) + ) + ); + } + + refresh(); + setRuleDialogOpen(false); + }, + [data, refresh] + ); + if (isLoading || !data) { return ; } @@ -327,9 +406,18 @@ export default function TransactionsPage() { onToggleReconciled={toggleReconciled} onMarkReconciled={markReconciled} onSetCategory={setCategory} + onCreateRule={handleCreateRule} formatCurrency={formatCurrency} formatDate={formatDate} /> + + ); } diff --git a/components/categories/category-edit-dialog.tsx b/components/categories/category-edit-dialog.tsx index c311507..372b366 100644 --- a/components/categories/category-edit-dialog.tsx +++ b/components/categories/category-edit-dialog.tsx @@ -18,6 +18,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { IconPicker } from "@/components/ui/icon-picker"; import { Plus, X } from "lucide-react"; import { cn } from "@/lib/utils"; import type { Category } from "@/lib/types"; @@ -26,6 +27,7 @@ import { categoryColors } from "./constants"; interface CategoryFormData { name: string; color: string; + icon: string; keywords: string[]; parentId: string | null; } @@ -128,22 +130,32 @@ export function CategoryEditDialog({ /> - {/* Couleur */} -
- -
- {categoryColors.map((color) => ( -
+
+
+ + onFormDataChange({ ...formData, icon })} + color={formData.color} + />
diff --git a/components/transactions/transaction-filters.tsx b/components/transactions/transaction-filters.tsx index 2c02b46..31af09c 100644 --- a/components/transactions/transaction-filters.tsx +++ b/components/transactions/transaction-filters.tsx @@ -9,6 +9,7 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { CategoryFilterCombobox } from "@/components/ui/category-filter-combobox"; import { Search } from "lucide-react"; import type { Account, Category } from "@/lib/types"; @@ -67,20 +68,12 @@ export function TransactionFilters({ - +