"use client"; import { useState, useMemo } from "react"; 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 { Plus } from "lucide-react"; import { autoCategorize, addCategory, updateCategory, deleteCategory, } from "@/lib/store-db"; import type { Category } from "@/lib/types"; 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() ); const [formData, setFormData] = useState({ name: "", color: "#22c55e", icon: "tag", keywords: [] as string[], parentId: null as string | null, }); const [searchQuery, setSearchQuery] = useState(""); // Organiser les catégories par parent const { parentCategories, childrenByParent, orphanCategories } = useMemo(() => { if (!data?.categories) return { parentCategories: [], childrenByParent: {}, orphanCategories: [], }; const parents = data.categories.filter((c) => c.parentId === null); const children: Record = {}; const orphans: Category[] = []; data.categories .filter((c) => c.parentId !== null) .forEach((child) => { const parentExists = parents.some((p) => p.id === child.parentId); if (parentExists) { if (!children[child.parentId!]) { children[child.parentId!] = []; } children[child.parentId!].push(child); } else { orphans.push(child); } }); return { parentCategories: parents, childrenByParent: children, orphanCategories: orphans, }; }, [data?.categories]); // Initialiser tous les parents comme ouverts useState(() => { if (parentCategories.length > 0 && expandedParents.size === 0) { setExpandedParents(new Set(parentCategories.map((p) => p.id))); } }); if (isLoading || !data) { return ; } const formatCurrency = (amount: number) => { return new Intl.NumberFormat("fr-FR", { style: "currency", currency: "EUR", }).format(amount); }; const getCategoryStats = (categoryId: string, includeChildren = false) => { let categoryIds = [categoryId]; if (includeChildren && childrenByParent[categoryId]) { categoryIds = [ ...categoryIds, ...childrenByParent[categoryId].map((c) => c.id), ]; } const categoryTransactions = data.transactions.filter((t) => categoryIds.includes(t.categoryId || "") ); const total = categoryTransactions.reduce( (sum, t) => sum + Math.abs(t.amount), 0 ); const count = categoryTransactions.length; return { total, count }; }; const toggleExpanded = (parentId: string) => { const newExpanded = new Set(expandedParents); if (newExpanded.has(parentId)) { newExpanded.delete(parentId); } else { newExpanded.add(parentId); } setExpandedParents(newExpanded); }; const expandAll = () => { setExpandedParents(new Set(parentCategories.map((p) => p.id))); }; const collapseAll = () => { setExpandedParents(new Set()); }; const allExpanded = parentCategories.length > 0 && expandedParents.size === parentCategories.length; const handleNewCategory = (parentId: string | null = null) => { setEditingCategory(null); setFormData({ name: "", color: "#22c55e", icon: "tag", keywords: [], parentId }); setIsDialogOpen(true); }; const handleEdit = (category: Category) => { setEditingCategory(category); setFormData({ name: category.name, color: category.color, icon: category.icon, keywords: [...category.keywords], parentId: category.parentId, }); setIsDialogOpen(true); }; const handleSave = async () => { try { if (editingCategory) { await updateCategory({ ...editingCategory, name: formData.name, color: formData.color, icon: formData.icon, keywords: formData.keywords, parentId: formData.parentId, }); } else { await addCategory({ name: formData.name, color: formData.color, icon: formData.icon, keywords: formData.keywords, parentId: formData.parentId, }); } refresh(); setIsDialogOpen(false); } catch (error) { console.error("Error saving category:", error); alert("Erreur lors de la sauvegarde de la catégorie"); } }; const handleDelete = async (categoryId: string) => { const hasChildren = childrenByParent[categoryId]?.length > 0; const message = hasChildren ? "Cette catégorie a des sous-catégories. Supprimer quand même ?" : "Supprimer cette catégorie ?"; if (!confirm(message)) return; try { await deleteCategory(categoryId); refresh(); } catch (error) { console.error("Error deleting category:", error); alert("Erreur lors de la suppression de la catégorie"); } }; const reApplyAutoCategories = async () => { if ( !confirm( "Recatégoriser automatiquement les transactions non catégorisées ?" ) ) return; try { const { updateTransaction } = await import("@/lib/store-db"); const uncategorized = data.transactions.filter((t) => !t.categoryId); for (const transaction of uncategorized) { const categoryId = autoCategorize( transaction.description + " " + (transaction.memo || ""), data.categories ); if (categoryId) { await updateTransaction({ ...transaction, categoryId }); } } refresh(); } catch (error) { console.error("Error re-categorizing:", error); alert("Erreur lors de la recatégorisation"); } }; const uncategorizedCount = data.transactions.filter( (t) => !t.categoryId ).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 ( {uncategorizedCount > 0 && ( )} } />
{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})
{orphanCategories.map((category) => ( ))}
)}
); }