"use client"; import { useState, useMemo, useCallback } from "react"; import { PageLayout, LoadingState, PageHeader } from "@/components/layout"; import { RuleGroupCard, RuleCreateDialog, RulesSearchBar, } from "@/components/rules"; import { useBankingData } from "@/lib/hooks"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Sparkles, RefreshCw } from "lucide-react"; import { updateCategory, autoCategorize, updateTransaction } from "@/lib/store-db"; import { normalizeDescription, suggestKeyword, } from "@/components/rules/constants"; import type { Transaction, Category } from "@/lib/types"; interface TransactionGroup { key: string; displayName: string; transactions: Transaction[]; totalAmount: number; suggestedKeyword: string; } export default function RulesPage() { const { data, isLoading, refresh } = useBankingData(); const [searchQuery, setSearchQuery] = useState(""); const [sortBy, setSortBy] = useState<"count" | "amount" | "name">("count"); const [filterMinCount, setFilterMinCount] = useState(2); const [expandedGroups, setExpandedGroups] = useState>(new Set()); const [selectedGroup, setSelectedGroup] = useState( null ); const [isDialogOpen, setIsDialogOpen] = useState(false); const [isAutoCategorizing, setIsAutoCategorizing] = useState(false); // Group uncategorized transactions by normalized description const transactionGroups = useMemo(() => { if (!data?.transactions) return []; const uncategorized = data.transactions.filter((t) => !t.categoryId); const groups: Record = {}; uncategorized.forEach((transaction) => { const key = normalizeDescription(transaction.description); if (!groups[key]) { groups[key] = []; } groups[key].push(transaction); }); // Convert to array with metadata const groupArray: TransactionGroup[] = Object.entries(groups).map( ([key, transactions]) => { const descriptions = transactions.map((t) => t.description); return { key, displayName: transactions[0].description, // Use first transaction's description as display name transactions, totalAmount: transactions.reduce((sum, t) => sum + t.amount, 0), suggestedKeyword: suggestKeyword(descriptions), }; } ); // Filter by search query let filtered = groupArray; if (searchQuery.trim()) { const query = searchQuery.toLowerCase(); filtered = groupArray.filter( (g) => g.displayName.toLowerCase().includes(query) || g.key.includes(query) || g.suggestedKeyword.toLowerCase().includes(query) ); } // Filter by minimum count filtered = filtered.filter((g) => g.transactions.length >= filterMinCount); // Sort filtered.sort((a, b) => { switch (sortBy) { case "count": return b.transactions.length - a.transactions.length; case "amount": return Math.abs(b.totalAmount) - Math.abs(a.totalAmount); case "name": return a.displayName.localeCompare(b.displayName); default: return 0; } }); return filtered; }, [data?.transactions, searchQuery, sortBy, filterMinCount]); const uncategorizedCount = useMemo(() => { if (!data?.transactions) return 0; return data.transactions.filter((t) => !t.categoryId).length; }, [data?.transactions]); const formatCurrency = useCallback((amount: number) => { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", }).format(amount); }, []); const formatDate = useCallback((dateStr: string) => { return new Date(dateStr).toLocaleDateString("fr-FR", { day: "2-digit", month: "short", year: "numeric", }); }, []); const toggleExpand = useCallback((key: string) => { setExpandedGroups((prev) => { const next = new Set(prev); if (next.has(key)) { next.delete(key); } else { next.add(key); } return next; }); }, []); const handleCreateRule = useCallback((group: TransactionGroup) => { setSelectedGroup(group); setIsDialogOpen(true); }, []); 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(); }, [data, refresh] ); const handleAutoCategorize = useCallback(async () => { if (!data) return; setIsAutoCategorizing(true); try { const uncategorized = data.transactions.filter((t) => !t.categoryId); let categorizedCount = 0; for (const transaction of uncategorized) { const categoryId = autoCategorize( transaction.description + " " + (transaction.memo || ""), data.categories ); if (categoryId) { await updateTransaction({ ...transaction, categoryId }); categorizedCount++; } } refresh(); alert(`${categorizedCount} transaction(s) catégorisée(s) automatiquement`); } catch (error) { console.error("Error auto-categorizing:", error); alert("Erreur lors de la catégorisation automatique"); } finally { setIsAutoCategorizing(false); } }, [data, refresh]); if (isLoading || !data) { return ; } return ( {transactionGroups.length} groupe {transactionGroups.length > 1 ? "s" : ""} de transactions similaires {uncategorizedCount} non catégorisées } actions={ uncategorizedCount > 0 && ( ) } /> {transactionGroups.length === 0 ? (

{uncategorizedCount === 0 ? "Toutes les transactions sont catégorisées !" : "Aucun groupe trouvé"}

{uncategorizedCount === 0 ? "Continuez à importer des transactions pour voir les suggestions de règles." : filterMinCount > 1 ? `Essayez de réduire le filtre minimum à ${filterMinCount - 1}+ transactions.` : "Modifiez vos critères de recherche."}

) : (
{transactionGroups.map((group) => ( toggleExpand(group.key)} onCreateRule={() => handleCreateRule(group)} categories={data.categories} formatCurrency={formatCurrency} formatDate={formatDate} /> ))}
)}
); }