import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { categoryService } from '../services/api'; import { useToast } from '../components/Toast'; import { ConfirmModal } from '../components/Modal'; import IconSelector from '../components/IconSelector'; const Categories = () => { const { t } = useTranslation(); const toast = useToast(); const [categories, setCategories] = useState([]); const [flatCategories, setFlatCategories] = useState([]); const [loading, setLoading] = useState(true); const [showModal, setShowModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false); const [selectedItem, setSelectedItem] = useState(null); const [saving, setSaving] = useState(false); const [newKeyword, setNewKeyword] = useState(''); const [filter, setFilter] = useState({ type: '' }); const [expandedCategories, setExpandedCategories] = useState({}); // Estados para categorização em lote const [showBatchModal, setShowBatchModal] = useState(false); const [batchPreview, setBatchPreview] = useState(null); const [loadingBatch, setLoadingBatch] = useState(false); const [executingBatch, setExecutingBatch] = useState(false); const [formData, setFormData] = useState({ name: '', parent_id: '', type: 'expense', description: '', color: '#3B82F6', icon: 'bi-tag', is_active: true, keywords: [], }); const categoryTypes = categoryService.types; useEffect(() => { loadCategories(); }, [filter]); const loadCategories = async () => { try { setLoading(true); const params = {}; if (filter.type) params.type = filter.type; // Carregar hierárquicas const response = await categoryService.getAll(params); if (response.success) { setCategories(response.data); } // Carregar flat para o select de parent const flatResponse = await categoryService.getAll({ flat: true }); if (flatResponse.success) { setFlatCategories(flatResponse.data.filter(c => !c.parent_id)); // Apenas categorias raiz } } catch (error) { toast.error(t('categories.loadError')); } finally { setLoading(false); } }; const toggleExpand = (categoryId) => { setExpandedCategories(prev => ({ ...prev, [categoryId]: !prev[categoryId], })); }; const handleOpenModal = (item = null, parentId = null) => { if (item) { setSelectedItem(item); setFormData({ name: item.name || '', parent_id: item.parent_id?.toString() || '', type: item.type || 'expense', description: item.description || '', color: item.color || '#3B82F6', icon: item.icon || 'bi-tag', is_active: item.is_active ?? true, keywords: item.keywords?.map(k => k.keyword) || [], }); } else { setSelectedItem(null); setFormData({ name: '', parent_id: parentId?.toString() || '', type: 'expense', description: '', color: '#3B82F6', icon: 'bi-tag', is_active: true, keywords: [], }); } setNewKeyword(''); setShowModal(true); }; const handleCloseModal = () => { setShowModal(false); setSelectedItem(null); setNewKeyword(''); }; // Funções de categorização em lote const handleOpenBatchModal = async () => { setShowBatchModal(true); setLoadingBatch(true); try { const response = await categoryService.categorizeBatchPreview(true, 50); if (response.success) { setBatchPreview(response.data); } } catch (error) { toast.error(t('categories.batchPreviewError') || 'Erro ao carregar preview'); } finally { setLoadingBatch(false); } }; const handleCloseBatchModal = () => { setShowBatchModal(false); setBatchPreview(null); }; const handleExecuteBatch = async () => { setExecutingBatch(true); try { const response = await categoryService.categorizeBatch(true); if (response.success) { toast.success( `${t('categories.batchSuccess') || 'Categorização concluída'}: ${response.data.categorized} ${t('categories.categorized') || 'categorizadas'}` ); handleCloseBatchModal(); } } catch (error) { toast.error(t('categories.batchError') || 'Erro ao categorizar'); } finally { setExecutingBatch(false); } }; const handleChange = (e) => { const { name, value, type, checked } = e.target; setFormData(prev => ({ ...prev, [name]: type === 'checkbox' ? checked : value, })); }; const handleAddKeyword = () => { const keyword = newKeyword.trim(); if (keyword && !formData.keywords.includes(keyword)) { setFormData(prev => ({ ...prev, keywords: [...prev.keywords, keyword], })); setNewKeyword(''); } }; const handleRemoveKeyword = (keyword) => { setFormData(prev => ({ ...prev, keywords: prev.keywords.filter(k => k !== keyword), })); }; const handleKeywordKeyPress = (e) => { if (e.key === 'Enter') { e.preventDefault(); handleAddKeyword(); } }; const handleSubmit = async (e) => { e.preventDefault(); if (!formData.name.trim()) { toast.error(t('validation.required')); return; } setSaving(true); try { const data = { ...formData, parent_id: formData.parent_id ? parseInt(formData.parent_id) : null, }; let response; if (selectedItem) { response = await categoryService.update(selectedItem.id, data); } else { response = await categoryService.create(data); } if (response.success) { toast.success(selectedItem ? t('categories.updateSuccess') : t('categories.createSuccess')); handleCloseModal(); loadCategories(); } } catch (error) { toast.error(error.response?.data?.message || t('categories.createError')); } finally { setSaving(false); } }; const handleDeleteClick = (item) => { setSelectedItem(item); setShowDeleteModal(true); }; const handleDeleteConfirm = async () => { if (!selectedItem) return; setSaving(true); try { const response = await categoryService.delete(selectedItem.id); if (response.success) { toast.success(t('categories.deleteSuccess')); setShowDeleteModal(false); setSelectedItem(null); loadCategories(); } } catch (error) { toast.error(error.response?.data?.message || t('categories.deleteError')); } finally { setSaving(false); } }; const getTypeColor = (type) => { switch (type) { case 'income': return 'success'; case 'expense': return 'danger'; case 'both': return 'info'; default: return 'secondary'; } }; const renderCategory = (category, level = 0, parentColor = null) => { const hasChildren = category.children && category.children.length > 0; const isExpanded = expandedCategories[category.id]; // Subcategorias herdam a cor da categoria pai - garantir que sempre use a cor do pai se disponível const displayColor = level > 0 && parentColor ? parentColor : (category.color || '#94a3b8'); return (
0 ? 'ms-4' : ''}`} style={{ borderBottom: '1px solid #334155', backgroundColor: level > 0 ? '#1a2332' : 'transparent', }} > {/* Expand Button */}
{hasChildren && ( )}
{/* Icon & Name */}
{category.name}
{category.keywords && category.keywords.length > 0 && ( {category.keywords.length} {t('categories.keywords')} )}
{/* Type Badge */}
{categoryTypes[category.type]}
{/* Status */}
{category.is_active ? ( {t('common.active')} ) : ( {t('common.inactive')} )}
{/* Actions */}
{!category.is_system && ( )}
{/* Children */} {hasChildren && isExpanded && (
{category.children.map(child => renderCategory(child, level + 1, displayColor))}
)}
); }; return (
{/* Header */}

{t('nav.categories')}

{t('categories.title')}

{/* Summary Cards */}

{t('categories.types.expense')}

{flatCategories.filter(c => c.type === 'expense' || c.type === 'both').length}

{t('categories.types.income')}

{flatCategories.filter(c => c.type === 'income' || c.type === 'both').length}

{t('common.total')}

{flatCategories.length}

{/* Filters */}
{/* Categories List */}
{loading ? (
{t('common.loading')}
) : categories.length === 0 ? (

{t('categories.noCategories')}

) : (
{categories.map(category => renderCategory(category))}
)}
{/* Modal de Criar/Editar - Design Elegante */} {showModal && (
{/* Header elegante */}
{selectedItem ? t('categories.editCategory') : formData.parent_id ? t('categories.createSubcategory') : t('categories.newCategory')}

{formData.parent_id ? `${t('categories.parentCategory')}: ${flatCategories.find(c => c.id == formData.parent_id)?.name || ''}` : t('categories.title') }

{/* Preview Card */}
{formData.name || t('categories.newCategory')}
{formData.description || t('common.description')}
{categoryTypes[formData.type]}
{/* Nome e Tipo - Linha principal */}
{/* Visual - Cor e Ícone */}
setFormData(prev => ({ ...prev, icon }))} type="category" />
{/* Categoria Pai (se não for edição de uma categoria raiz) */} {!selectedItem?.children?.length && (
setFormData({...formData, parent_id: ''})} className="p-2 rounded text-center h-100 d-flex flex-column justify-content-center" style={{ background: !formData.parent_id ? 'rgba(59, 130, 246, 0.15)' : '#0f172a', cursor: 'pointer', border: !formData.parent_id ? '2px solid #3b82f6' : '2px solid transparent', minHeight: 60 }} > {t('categories.noParent')}
{flatCategories .filter(c => c.id !== selectedItem?.id && (c.type === formData.type || c.type === 'both')) .slice(0, 7) .map(cat => (
setFormData({...formData, parent_id: cat.id.toString()})} className="p-2 rounded text-center h-100 d-flex flex-column justify-content-center" style={{ background: formData.parent_id == cat.id ? 'rgba(59, 130, 246, 0.15)' : '#0f172a', cursor: 'pointer', border: formData.parent_id == cat.id ? '2px solid #3b82f6' : '2px solid transparent', minHeight: 60 }} > {cat.name}
))}
{flatCategories.filter(c => c.id !== selectedItem?.id && (c.type === formData.type || c.type === 'both')).length > 7 && ( )}
)} {/* Descrição */}
{/* Palavras-chave - Seção destacada */}
setNewKeyword(e.target.value)} onKeyPress={handleKeywordKeyPress} placeholder={t('categories.keywordPlaceholder') || 'Digite e pressione Enter...'} />
{formData.keywords.map((keyword, index) => ( {keyword} ))} {formData.keywords.length === 0 && ( {t('categories.noKeywords') || 'Nenhuma palavra-chave. Transações serão categorizadas manualmente.'} )}
{t('categories.keywordHelp') || 'Ex: "RESTAURANTE", "PIZZA" - Transações com essas palavras serão categorizadas automaticamente'}
{/* Status */}
{/* Footer elegante */}
)} {/* Modal de Confirmação de Exclusão */} setShowDeleteModal(false)} onConfirm={handleDeleteConfirm} title={t('categories.deleteCategory')} message={t('categories.deleteConfirm')} confirmText={t('common.delete')} loading={saving} /> {/* Modal de Categorização em Lote - Design Elegante */} {showBatchModal && (
{/* Header elegante */}
{t('categories.batchCategorize') || 'Categorização Automática'}

{t('categories.batchDescription') || 'Categorize transações automaticamente usando palavras-chave'}

{loadingBatch ? (
Loading...

{t('categories.analyzingTransactions') || 'Analisando transações...'}

) : batchPreview ? ( <> {/* Cards de Resumo */}

{batchPreview.total_uncategorized}

{t('categories.uncategorized') || 'Sem categoria'}

{batchPreview.would_categorize}

{t('categories.willCategorize') || 'Serão categorizadas'}

{batchPreview.would_skip}

{t('categories.willSkip') || 'Sem correspondência'}

{batchPreview.total_keywords}

{t('categories.totalKeywords') || 'Palavras-chave'}
{/* Preview Table */} {batchPreview.preview.length > 0 ? ( <>
{t('categories.previewTitle') || 'Preview das categorizações'}
{batchPreview.preview.length} {t('common.items') || 'itens'}
{batchPreview.preview.map((item, index) => ( ))}
{t('transactions.description') || 'Descrição'} {t('categories.matchedKeyword') || 'Keyword'} {t('categories.category') || 'Categoria'}
{item.description} {item.matched_keyword} {item.category_name}
) : (
{t('categories.noMatchesFoundTitle') || 'Nenhuma correspondência encontrada'}

{t('categories.noMatchesFound') || 'Adicione palavras-chave às categorias para permitir categorização automática'}

)} ) : (

{t('categories.previewError') || 'Erro ao carregar preview'}

)}
{/* Footer elegante */}
)}
); }; export default Categories;