"use client"; import { useState, useMemo } from "react"; import { Sidebar } from "@/components/dashboard/sidebar"; import { useBankingData } from "@/lib/hooks"; 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 { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Plus, MoreVertical, Pencil, Trash2, RefreshCw, X, ChevronDown, ChevronRight, ChevronsUpDown, Search, } from "lucide-react"; import { CategoryIcon } from "@/components/ui/category-icon"; import { autoCategorize, addCategory, updateCategory, deleteCategory, } from "@/lib/store-db"; import type { Category } from "@/lib/types"; import { cn } from "@/lib/utils"; const categoryColors = [ "#22c55e", "#3b82f6", "#f59e0b", "#ec4899", "#ef4444", "#8b5cf6", "#06b6d4", "#84cc16", "#f97316", "#6366f1", "#14b8a6", "#f43f5e", "#64748b", "#0891b2", "#dc2626", ]; 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", keywords: [] as string[], parentId: null as string | null, }); const [newKeyword, setNewKeyword] = useState(""); 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[] = []; // Grouper les enfants par parent 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", keywords: [], parentId }); setIsDialogOpen(true); }; const handleEdit = (category: Category) => { setEditingCategory(category); setFormData({ name: category.name, color: category.color, keywords: [...category.keywords], parentId: category.parentId, }); setIsDialogOpen(true); }; const handleSave = async () => { try { if (editingCategory) { await updateCategory({ ...editingCategory, name: formData.name, color: formData.color, keywords: formData.keywords, parentId: formData.parentId, }); } else { await addCategory({ name: formData.name, color: formData.color, keywords: formData.keywords, icon: "tag", 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 addKeyword = () => { if ( newKeyword.trim() && !formData.keywords.includes(newKeyword.trim().toLowerCase()) ) { setFormData({ ...formData, keywords: [...formData.keywords, newKeyword.trim().toLowerCase()], }); setNewKeyword(""); } }; const removeKeyword = (keyword: string) => { setFormData({ ...formData, keywords: formData.keywords.filter((k) => k !== keyword), }); }; 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; // Composant pour une carte de catégorie enfant const ChildCategoryCard = ({ category }: { category: Category }) => { const stats = getCategoryStats(category.id); return (
{category.name} {stats.count} opération{stats.count > 1 ? "s" : ""} •{" "} {formatCurrency(stats.total)} {category.keywords.length > 0 && ( {category.keywords.length} )}
); }; return (
{/* Header */}

Catégories

{parentCategories.length} catégories principales •{" "} {data.categories.length - parentCategories.length}{" "} sous-catégories

{uncategorizedCount > 0 && ( )}
{/* Barre de recherche et contrôles */}
setSearchQuery(e.target.value)} className="pl-9" /> {searchQuery && ( )}
{/* Liste des catégories par parent */}
{parentCategories .filter((parent) => { if (!searchQuery.trim()) return true; const query = searchQuery.toLowerCase(); // Afficher si le parent matche if (parent.name.toLowerCase().includes(query)) return true; if ( parent.keywords.some((k) => k.toLowerCase().includes(query)) ) return true; // Ou si un enfant matche const children = childrenByParent[parent.id] || []; return children.some( (c) => c.name.toLowerCase().includes(query) || c.keywords.some((k) => k.toLowerCase().includes(query)), ); }) .map((parent) => { const allChildren = childrenByParent[parent.id] || []; // Filtrer les enfants aussi si recherche active const children = searchQuery.trim() ? allChildren.filter( (c) => c.name .toLowerCase() .includes(searchQuery.toLowerCase()) || c.keywords.some((k) => k.toLowerCase().includes(searchQuery.toLowerCase()), ) || // Garder tous les enfants si le parent matche 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)} >
handleEdit(parent)} > Modifier handleDelete(parent.id)} className="text-red-600" > Supprimer
{children.length > 0 ? (
{children.map((child) => ( ))}
) : (
Aucune sous-catégorie
)}
); })} {/* Catégories orphelines (sans parent valide) */} {orphanCategories.length > 0 && (
Catégories non classées ({orphanCategories.length})
{orphanCategories.map((category) => ( ))}
)}
{/* Dialog de création/édition */} {editingCategory ? "Modifier la catégorie" : "Nouvelle catégorie"}
{/* Catégorie parente */}
{/* Nom */}
setFormData({ ...formData, name: e.target.value }) } placeholder="Ex: Alimentation" />
{/* Couleur */}
{categoryColors.map((color) => (
{/* Mots-clés */}
setNewKeyword(e.target.value)} placeholder="Ajouter un mot-clé" onKeyDown={(e) => e.key === "Enter" && (e.preventDefault(), addKeyword()) } />
{formData.keywords.map((keyword) => ( {keyword} ))}
{/* Actions */}
); }