"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 */}
) }