webmoney/frontend/src/pages/Categories.jsx
marcoitaloesp-ai 9c9d6443e7
v1.57.0: Redesign category modals + i18n updates + demo transactions fix
- Redesigned category create/edit modal with elegant wizard-style UI
- Redesigned batch categorization modal with visual cards and better preview
- Added missing i18n translations (common.continue, creating, remove)
- Added budgets.general and wizard translations for ES, PT-BR, EN
- Fixed 3 demo user transactions that were missing categories
2025-12-18 19:06:07 +00:00

962 lines
42 KiB
JavaScript

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 (
<div key={category.id}>
<div
className={`d-flex align-items-center p-3 ${level > 0 ? 'ms-4' : ''}`}
style={{
borderBottom: '1px solid #334155',
backgroundColor: level > 0 ? '#1a2332' : 'transparent',
}}
>
{/* Expand Button */}
<div style={{ width: '30px' }}>
{hasChildren && (
<button
className="btn btn-link text-slate-400 p-0"
onClick={() => toggleExpand(category.id)}
>
<i className={`bi ${isExpanded ? 'bi-chevron-down' : 'bi-chevron-right'}`}></i>
</button>
)}
</div>
{/* Icon & Name */}
<div className="d-flex align-items-center flex-grow-1">
<div
className="rounded-circle d-flex align-items-center justify-content-center me-3"
style={{
width: '35px',
height: '35px',
backgroundColor: displayColor + '25',
}}
>
<i className={`bi ${category.icon}`} style={{ color: displayColor }}></i>
</div>
<div>
<div className="text-white fw-medium">{category.name}</div>
{category.keywords && category.keywords.length > 0 && (
<small className="text-slate-500">
{category.keywords.length} {t('categories.keywords')}
</small>
)}
</div>
</div>
{/* Type Badge */}
<div className="me-3">
<span className={`badge bg-${getTypeColor(category.type)} bg-opacity-25 text-${getTypeColor(category.type)}`}>
{categoryTypes[category.type]}
</span>
</div>
{/* Status */}
<div className="me-3">
{category.is_active ? (
<span className="badge bg-success">{t('common.active')}</span>
) : (
<span className="badge bg-secondary">{t('common.inactive')}</span>
)}
</div>
{/* Actions */}
<div className="d-flex gap-1">
<button
className="btn btn-link text-success p-1"
onClick={() => handleOpenModal(null, category.id)}
title={t('categories.createSubcategory')}
>
<i className="bi bi-plus-circle"></i>
</button>
<button
className="btn btn-link text-info p-1"
onClick={() => handleOpenModal(category)}
title={t('common.edit')}
>
<i className="bi bi-pencil"></i>
</button>
{!category.is_system && (
<button
className="btn btn-link text-danger p-1"
onClick={() => handleDeleteClick(category)}
title={t('common.delete')}
>
<i className="bi bi-trash"></i>
</button>
)}
</div>
</div>
{/* Children */}
{hasChildren && isExpanded && (
<div>
{category.children.map(child => renderCategory(child, level + 1, displayColor))}
</div>
)}
</div>
);
};
return (
<div>
{/* Header */}
<div className="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 className="text-white mb-1">
<i className="bi bi-tags me-2 text-info"></i>
{t('nav.categories')}
</h2>
<p className="text-slate-400 mb-0">
{t('categories.title')}
</p>
</div>
<div className="d-flex gap-2">
<button className="btn btn-outline-warning" onClick={handleOpenBatchModal}>
<i className="bi bi-lightning-charge me-2"></i>
{t('categories.batchCategorize') || 'Categorizar em Lote'}
</button>
<button className="btn btn-info" onClick={() => handleOpenModal()}>
<i className="bi bi-plus-lg me-2"></i>
{t('categories.newCategory')}
</button>
</div>
</div>
{/* Summary Cards */}
<div className="row mb-4">
<div className="col-md-4">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body">
<div className="d-flex align-items-center">
<div className="rounded-circle bg-danger bg-opacity-25 p-3 me-3">
<i className="bi bi-arrow-down-circle text-danger fs-4"></i>
</div>
<div>
<p className="text-slate-400 mb-0 small">{t('categories.types.expense')}</p>
<h3 className="mb-0 text-white">
{flatCategories.filter(c => c.type === 'expense' || c.type === 'both').length}
</h3>
</div>
</div>
</div>
</div>
</div>
<div className="col-md-4">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body">
<div className="d-flex align-items-center">
<div className="rounded-circle bg-success bg-opacity-25 p-3 me-3">
<i className="bi bi-arrow-up-circle text-success fs-4"></i>
</div>
<div>
<p className="text-slate-400 mb-0 small">{t('categories.types.income')}</p>
<h3 className="mb-0 text-white">
{flatCategories.filter(c => c.type === 'income' || c.type === 'both').length}
</h3>
</div>
</div>
</div>
</div>
</div>
<div className="col-md-4">
<div className="card border-0" style={{ background: '#1e293b' }}>
<div className="card-body">
<div className="d-flex align-items-center">
<div className="rounded-circle bg-info bg-opacity-25 p-3 me-3">
<i className="bi bi-tags text-info fs-4"></i>
</div>
<div>
<p className="text-slate-400 mb-0 small">{t('common.total')}</p>
<h3 className="mb-0 text-white">{flatCategories.length}</h3>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="card border-0 mb-4" style={{ background: '#1e293b' }}>
<div className="card-body">
<div className="row g-3">
<div className="col-md-4">
<label className="form-label text-slate-400 small">{t('categories.filterByType')}</label>
<select
className="form-select bg-dark text-white border-secondary"
value={filter.type}
onChange={(e) => setFilter(prev => ({ ...prev, type: e.target.value }))}
>
<option value="">{t('common.all')}</option>
{Object.entries(categoryTypes).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
</div>
</div>
</div>
{/* Categories List */}
<div className="card border-0" style={{ background: '#1e293b' }}>
{loading ? (
<div className="card-body text-center py-5">
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">{t('common.loading')}</span>
</div>
</div>
) : categories.length === 0 ? (
<div className="card-body text-center py-5">
<i className="bi bi-tags display-1 text-slate-600"></i>
<p className="text-slate-400 mt-3">{t('categories.noCategories')}</p>
<button className="btn btn-info" onClick={() => handleOpenModal()}>
<i className="bi bi-plus-lg me-2"></i>
{t('categories.newCategory')}
</button>
</div>
) : (
<div className="card-body p-0">
{categories.map(category => renderCategory(category))}
</div>
)}
</div>
{/* Modal de Criar/Editar - Design Elegante */}
{showModal && (
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.8)' }}>
<div className="modal-dialog modal-dialog-centered modal-lg">
<div className="modal-content border-0" style={{ background: '#1e293b', maxHeight: '90vh' }}>
{/* Header elegante */}
<div className="modal-header border-0 pb-0">
<div>
<h5 className="modal-title text-white mb-1">
<i className={`bi ${selectedItem ? 'bi-pencil-square' : formData.parent_id ? 'bi-diagram-3' : 'bi-plus-circle-dotted'} me-2 text-info`}></i>
{selectedItem ? t('categories.editCategory') : formData.parent_id ? t('categories.createSubcategory') : t('categories.newCategory')}
</h5>
<p className="text-slate-400 mb-0 small">
{formData.parent_id
? `${t('categories.parentCategory')}: ${flatCategories.find(c => c.id == formData.parent_id)?.name || ''}`
: t('categories.title')
}
</p>
</div>
<button type="button" className="btn-close btn-close-white" onClick={handleCloseModal}></button>
</div>
<form onSubmit={handleSubmit}>
<div className="modal-body pt-3" style={{ maxHeight: '65vh', overflowY: 'auto' }}>
{/* Preview Card */}
<div className="mb-4 p-3 rounded-3" style={{ background: '#0f172a' }}>
<div className="d-flex align-items-center">
<div
className="rounded-circle d-flex align-items-center justify-content-center me-3"
style={{
width: 50,
height: 50,
background: `${formData.color}25`,
border: `2px solid ${formData.color}`,
}}
>
<i className={`bi ${formData.icon}`} style={{ fontSize: '1.3rem', color: formData.color }}></i>
</div>
<div>
<h6 className="text-white mb-0">{formData.name || t('categories.newCategory')}</h6>
<small className="text-slate-400">{formData.description || t('common.description')}</small>
</div>
<div className="ms-auto">
<span className={`badge bg-${getTypeColor(formData.type)} bg-opacity-25 text-${getTypeColor(formData.type)}`}>
{categoryTypes[formData.type]}
</span>
</div>
</div>
</div>
{/* Nome e Tipo - Linha principal */}
<div className="row g-3 mb-4">
<div className="col-md-8">
<label className="form-label text-white fw-medium mb-2">
<i className="bi bi-type me-2 text-primary"></i>
{t('common.name')} *
</label>
<input
type="text"
className="form-control bg-dark text-white border-0"
style={{ background: '#0f172a' }}
name="name"
value={formData.name}
onChange={handleChange}
placeholder={t('categories.namePlaceholder') || 'Ex: Alimentación, Transporte...'}
required
autoFocus
/>
</div>
<div className="col-md-4">
<label className="form-label text-white fw-medium mb-2">
<i className="bi bi-arrow-left-right me-2 text-warning"></i>
{t('common.type')} *
</label>
<select
className="form-select bg-dark text-white border-0"
style={{ background: '#0f172a' }}
name="type"
value={formData.type}
onChange={handleChange}
required
>
{Object.entries(categoryTypes).map(([key, label]) => (
<option key={key} value={key}>{label}</option>
))}
</select>
</div>
</div>
{/* Visual - Cor e Ícone */}
<div className="mb-4">
<label className="form-label text-white fw-medium mb-2">
<i className="bi bi-palette me-2 text-success"></i>
{t('categories.visualSettings') || 'Aparência'}
</label>
<div className="row g-3">
<div className="col-4">
<div className="p-3 rounded text-center" style={{ background: '#0f172a' }}>
<label className="text-slate-400 small d-block mb-2">{t('common.color')}</label>
<input
type="color"
className="form-control form-control-color mx-auto border-0"
style={{ width: 50, height: 50, cursor: 'pointer', background: 'transparent' }}
name="color"
value={formData.color}
onChange={handleChange}
/>
</div>
</div>
<div className="col-8">
<div className="p-3 rounded h-100" style={{ background: '#0f172a' }}>
<label className="text-slate-400 small d-block mb-2">{t('common.icon')}</label>
<IconSelector
value={formData.icon}
onChange={(icon) => setFormData(prev => ({ ...prev, icon }))}
type="category"
/>
</div>
</div>
</div>
</div>
{/* Categoria Pai (se não for edição de uma categoria raiz) */}
{!selectedItem?.children?.length && (
<div className="mb-4">
<label className="form-label text-white fw-medium mb-2">
<i className="bi bi-diagram-2 me-2 text-info"></i>
{t('categories.parentCategory')}
<span className="badge bg-secondary ms-2" style={{ fontSize: '0.65rem' }}>{t('common.optional')}</span>
</label>
<div className="row g-2">
<div className="col-4 col-md-3">
<div
onClick={() => 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
}}
>
<i className="bi bi-app d-block mb-1 text-slate-400"></i>
<small className="text-white">{t('categories.noParent')}</small>
</div>
</div>
{flatCategories
.filter(c => c.id !== selectedItem?.id && (c.type === formData.type || c.type === 'both'))
.slice(0, 7)
.map(cat => (
<div key={cat.id} className="col-4 col-md-3">
<div
onClick={() => 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
}}
>
<i className={`bi ${cat.icon || 'bi-tag'} d-block mb-1`} style={{ color: cat.color || '#6b7280' }}></i>
<small className="text-white text-truncate" title={cat.name}>{cat.name}</small>
</div>
</div>
))}
</div>
{flatCategories.filter(c => c.id !== selectedItem?.id && (c.type === formData.type || c.type === 'both')).length > 7 && (
<select
className="form-select bg-dark text-white border-0 mt-2"
style={{ background: '#0f172a' }}
name="parent_id"
value={formData.parent_id}
onChange={handleChange}
>
<option value="">{t('categories.selectParent') || 'Mais categorias...'}</option>
{flatCategories
.filter(c => c.id !== selectedItem?.id && (c.type === formData.type || c.type === 'both'))
.map((cat) => (
<option key={cat.id} value={cat.id}>{cat.name}</option>
))}
</select>
)}
</div>
)}
{/* Descrição */}
<div className="mb-4">
<label className="form-label text-white fw-medium mb-2">
<i className="bi bi-text-paragraph me-2 text-secondary"></i>
{t('common.description')}
<span className="badge bg-secondary ms-2" style={{ fontSize: '0.65rem' }}>{t('common.optional')}</span>
</label>
<textarea
className="form-control bg-dark text-white border-0"
style={{ background: '#0f172a' }}
name="description"
value={formData.description}
onChange={handleChange}
rows="2"
placeholder={t('categories.descPlaceholder') || 'Descreva esta categoria...'}
></textarea>
</div>
{/* Palavras-chave - Seção destacada */}
<div className="mb-3">
<label className="form-label text-white fw-medium mb-2">
<i className="bi bi-key me-2 text-warning"></i>
{t('categories.keywords')}
<span className="badge bg-warning text-dark ms-2" style={{ fontSize: '0.65rem' }}>
{t('categories.autoCategorizationLabel') || 'Auto-categorização'}
</span>
</label>
<div className="p-3 rounded" style={{ background: '#0f172a' }}>
<div className="input-group mb-2">
<input
type="text"
className="form-control bg-dark text-white border-0"
style={{ background: '#1e293b' }}
value={newKeyword}
onChange={(e) => setNewKeyword(e.target.value)}
onKeyPress={handleKeywordKeyPress}
placeholder={t('categories.keywordPlaceholder') || 'Digite e pressione Enter...'}
/>
<button
type="button"
className="btn btn-info px-3"
onClick={handleAddKeyword}
>
<i className="bi bi-plus-lg"></i>
</button>
</div>
<div className="d-flex flex-wrap gap-2">
{formData.keywords.map((keyword, index) => (
<span
key={index}
className="badge d-flex align-items-center py-2 px-3"
style={{
backgroundColor: formData.color + '25',
color: formData.color,
fontSize: '0.85rem'
}}
>
{keyword}
<button
type="button"
className="btn-close ms-2"
style={{ fontSize: '8px', filter: 'brightness(1.5)' }}
onClick={() => handleRemoveKeyword(keyword)}
></button>
</span>
))}
{formData.keywords.length === 0 && (
<small className="text-slate-500">
<i className="bi bi-info-circle me-1"></i>
{t('categories.noKeywords') || 'Nenhuma palavra-chave. Transações serão categorizadas manualmente.'}
</small>
)}
</div>
</div>
<small className="text-slate-500 mt-2 d-block">
<i className="bi bi-lightbulb me-1"></i>
{t('categories.keywordHelp') || 'Ex: "RESTAURANTE", "PIZZA" - Transações com essas palavras serão categorizadas automaticamente'}
</small>
</div>
{/* Status */}
<div className="form-check form-switch">
<input
type="checkbox"
className="form-check-input"
id="is_active"
name="is_active"
checked={formData.is_active}
onChange={handleChange}
role="switch"
/>
<label className="form-check-label text-white" htmlFor="is_active">
<i className={`bi ${formData.is_active ? 'bi-check-circle text-success' : 'bi-x-circle text-secondary'} me-2`}></i>
{formData.is_active ? t('common.active') : t('common.inactive')}
</label>
</div>
</div>
{/* Footer elegante */}
<div className="modal-footer border-0">
<button type="button" className="btn btn-outline-secondary px-4" onClick={handleCloseModal}>
<i className="bi bi-x-lg me-2"></i>
{t('common.cancel')}
</button>
<button type="submit" className="btn btn-info px-4" disabled={saving || !formData.name.trim()}>
{saving ? (
<>
<span className="spinner-border spinner-border-sm me-2"></span>
{t('common.saving')}
</>
) : (
<>
<i className={`bi ${selectedItem ? 'bi-check-lg' : 'bi-plus-lg'} me-2`}></i>
{selectedItem ? t('common.save') : t('common.create')}
</>
)}
</button>
</div>
</form>
</div>
</div>
</div>
)}
{/* Modal de Confirmação de Exclusão */}
<ConfirmModal
show={showDeleteModal}
onHide={() => 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 && (
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.8)' }}>
<div className="modal-dialog modal-lg modal-dialog-centered" style={{ maxWidth: '700px' }}>
<div className="modal-content border-0" style={{ background: '#1e293b', maxHeight: '90vh' }}>
{/* Header elegante */}
<div className="modal-header border-0 pb-0">
<div>
<h5 className="modal-title text-white mb-1">
<i className="bi bi-lightning-charge-fill me-2 text-warning"></i>
{t('categories.batchCategorize') || 'Categorização Automática'}
</h5>
<p className="text-slate-400 mb-0 small">
{t('categories.batchDescription') || 'Categorize transações automaticamente usando palavras-chave'}
</p>
</div>
<button type="button" className="btn-close btn-close-white" onClick={handleCloseBatchModal}></button>
</div>
<div className="modal-body pt-3" style={{ maxHeight: '65vh', overflowY: 'auto' }}>
{loadingBatch ? (
<div className="text-center py-5">
<div className="spinner-border text-warning" role="status" style={{ width: '3rem', height: '3rem' }}>
<span className="visually-hidden">Loading...</span>
</div>
<p className="text-slate-400 mt-3 mb-0">{t('categories.analyzingTransactions') || 'Analisando transações...'}</p>
</div>
) : batchPreview ? (
<>
{/* Cards de Resumo */}
<div className="row g-3 mb-4">
<div className="col-6 col-md-3">
<div className="p-3 rounded-3 text-center" style={{ background: '#0f172a' }}>
<div className="rounded-circle d-inline-flex align-items-center justify-content-center mb-2"
style={{ width: 40, height: 40, background: 'rgba(234, 179, 8, 0.2)' }}>
<i className="bi bi-question-circle text-warning"></i>
</div>
<h4 className="text-warning mb-0">{batchPreview.total_uncategorized}</h4>
<small className="text-slate-500">{t('categories.uncategorized') || 'Sem categoria'}</small>
</div>
</div>
<div className="col-6 col-md-3">
<div className="p-3 rounded-3 text-center" style={{ background: '#0f172a' }}>
<div className="rounded-circle d-inline-flex align-items-center justify-content-center mb-2"
style={{ width: 40, height: 40, background: 'rgba(34, 197, 94, 0.2)' }}>
<i className="bi bi-check-circle text-success"></i>
</div>
<h4 className="text-success mb-0">{batchPreview.would_categorize}</h4>
<small className="text-slate-500">{t('categories.willCategorize') || 'Serão categorizadas'}</small>
</div>
</div>
<div className="col-6 col-md-3">
<div className="p-3 rounded-3 text-center" style={{ background: '#0f172a' }}>
<div className="rounded-circle d-inline-flex align-items-center justify-content-center mb-2"
style={{ width: 40, height: 40, background: 'rgba(148, 163, 184, 0.2)' }}>
<i className="bi bi-dash-circle text-slate-400"></i>
</div>
<h4 className="text-slate-400 mb-0">{batchPreview.would_skip}</h4>
<small className="text-slate-500">{t('categories.willSkip') || 'Sem correspondência'}</small>
</div>
</div>
<div className="col-6 col-md-3">
<div className="p-3 rounded-3 text-center" style={{ background: '#0f172a' }}>
<div className="rounded-circle d-inline-flex align-items-center justify-content-center mb-2"
style={{ width: 40, height: 40, background: 'rgba(59, 130, 246, 0.2)' }}>
<i className="bi bi-key text-info"></i>
</div>
<h4 className="text-info mb-0">{batchPreview.total_keywords}</h4>
<small className="text-slate-500">{t('categories.totalKeywords') || 'Palavras-chave'}</small>
</div>
</div>
</div>
{/* Preview Table */}
{batchPreview.preview.length > 0 ? (
<>
<div className="d-flex align-items-center mb-3">
<i className="bi bi-eye me-2 text-info"></i>
<h6 className="text-white mb-0">
{t('categories.previewTitle') || 'Preview das categorizações'}
</h6>
<span className="badge bg-info bg-opacity-25 text-info ms-2">
{batchPreview.preview.length} {t('common.items') || 'itens'}
</span>
</div>
<div className="rounded-3 overflow-hidden" style={{ background: '#0f172a' }}>
<div style={{ maxHeight: '250px', overflowY: 'auto' }}>
<table className="table table-dark mb-0" style={{ background: 'transparent' }}>
<thead style={{ position: 'sticky', top: 0, background: '#0f172a', zIndex: 1 }}>
<tr>
<th className="border-0 text-slate-400 fw-normal small">{t('transactions.description') || 'Descrição'}</th>
<th className="border-0 text-slate-400 fw-normal small text-center" style={{ width: '120px' }}>{t('categories.matchedKeyword') || 'Keyword'}</th>
<th className="border-0 text-slate-400 fw-normal small" style={{ width: '140px' }}>{t('categories.category') || 'Categoria'}</th>
</tr>
</thead>
<tbody>
{batchPreview.preview.map((item, index) => (
<tr key={index} style={{ borderColor: '#334155' }}>
<td className="text-white text-truncate border-secondary" style={{ maxWidth: '200px' }}>
{item.description}
</td>
<td className="text-center border-secondary">
<span className="badge bg-warning bg-opacity-25 text-warning">{item.matched_keyword}</span>
</td>
<td className="text-info border-secondary">{item.category_name}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</>
) : (
<div className="p-4 rounded-3 text-center" style={{ background: '#0f172a' }}>
<i className="bi bi-search display-4 text-slate-600 mb-3 d-block"></i>
<h6 className="text-white mb-2">{t('categories.noMatchesFoundTitle') || 'Nenhuma correspondência encontrada'}</h6>
<p className="text-slate-400 mb-0 small">
{t('categories.noMatchesFound') || 'Adicione palavras-chave às categorias para permitir categorização automática'}
</p>
</div>
)}
</>
) : (
<div className="p-4 rounded-3 text-center" style={{ background: '#0f172a' }}>
<i className="bi bi-exclamation-triangle display-4 text-danger mb-3 d-block"></i>
<p className="text-slate-400 mb-0">{t('categories.previewError') || 'Erro ao carregar preview'}</p>
</div>
)}
</div>
{/* Footer elegante */}
<div className="modal-footer border-0">
<button type="button" className="btn btn-outline-secondary px-4" onClick={handleCloseBatchModal}>
<i className="bi bi-x-lg me-2"></i>
{t('common.cancel') || 'Cancelar'}
</button>
<button
type="button"
className="btn btn-warning px-4"
onClick={handleExecuteBatch}
disabled={executingBatch || !batchPreview || batchPreview.would_categorize === 0}
>
{executingBatch ? (
<>
<span className="spinner-border spinner-border-sm me-2" role="status"></span>
{t('common.processing') || 'Processando...'}
</>
) : (
<>
<i className="bi bi-lightning-charge-fill me-2"></i>
{t('categories.executeBatch') || 'Categorizar'} ({batchPreview?.would_categorize || 0})
</>
)}
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default Categories;