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 (
{t('categories.title')}
{t('categories.types.expense')}
{t('categories.types.income')}
{t('common.total')}
{t('categories.noCategories')}
{formData.parent_id ? `${t('categories.parentCategory')}: ${flatCategories.find(c => c.id == formData.parent_id)?.name || ''}` : t('categories.title') }
{t('categories.batchDescription') || 'Categorize transações automaticamente usando palavras-chave'}
{t('categories.analyzingTransactions') || 'Analisando transações...'}
| {t('transactions.description') || 'Descrição'} | {t('categories.matchedKeyword') || 'Keyword'} | {t('categories.category') || 'Categoria'} |
|---|---|---|
| {item.description} | {item.matched_keyword} | {item.category_name} |
{t('categories.noMatchesFound') || 'Adicione palavras-chave às categorias para permitir categorização automática'}
{t('categories.previewError') || 'Erro ao carregar preview'}