import React, { useState, useEffect, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Link, useSearchParams } from 'react-router-dom'; import { transactionService, accountService, categoryService, costCenterService, transferDetectionService } from '../services/api'; import { useFormatters } from '../hooks'; import Modal from '../components/Modal'; import Toast from '../components/Toast'; import CreateRecurrenceModal from '../components/CreateRecurrenceModal'; import IconSelector from '../components/IconSelector'; import CategorySelector from '../components/CategorySelector'; import ConfirmModal from '../components/ConfirmModal'; export default function Transactions() { const { t, i18n } = useTranslation(); const { currency: formatCurrency, date: formatDate } = useFormatters(); // Helper para locale dinâmico const getLocale = () => i18n.language === 'pt-BR' ? 'pt-BR' : i18n.language === 'es' ? 'es-ES' : 'en-US'; const [searchParams, setSearchParams] = useSearchParams(); const highlightedRef = useRef(null); // Estado para destacar transação const [highlightedTransactionId, setHighlightedTransactionId] = useState(null); // Mobile detection const [isMobile, setIsMobile] = useState(window.innerWidth < 768); // Estados principais const [weeklyData, setWeeklyData] = useState(null); const [accounts, setAccounts] = useState([]); const [categories, setCategories] = useState([]); const [costCenters, setCostCenters] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // Estado de divisa selecionada const [selectedCurrency, setSelectedCurrency] = useState(null); const [availableCurrencies, setAvailableCurrencies] = useState([]); // Estados de paginação const [page, setPage] = useState(1); const [perPage] = useState(5); // Semanas por página // Estados de filtro const [filters, setFilters] = useState({ account_id: '', category_id: '', cost_center_id: '', type: '', status: '', search: '', start_date: '', end_date: '', date_field: 'effective_date', // Ordenar por data efetiva (com fallback para planejada) }); // Estados de modal const [showModal, setShowModal] = useState(false); const [showDetailModal, setShowDetailModal] = useState(false); const [showCompleteModal, setShowCompleteModal] = useState(false); const [showTransferModal, setShowTransferModal] = useState(false); const [showSplitModal, setShowSplitModal] = useState(false); const [showRecurrenceModal, setShowRecurrenceModal] = useState(false); const [recurrenceTransaction, setRecurrenceTransaction] = useState(null); const [editingTransaction, setEditingTransaction] = useState(null); const [selectedTransaction, setSelectedTransaction] = useState(null); const [formData, setFormData] = useState({ account_id: '', category_id: '', cost_center_id: '', type: 'debit', planned_amount: '', amount: '', description: '', notes: '', planned_date: new Date().toISOString().split('T')[0], effective_date: '', status: 'pending', reference: '', }); // Estados de toast const [toast, setToast] = useState({ show: false, message: '', type: 'success' }); // Estados de complete const [completeData, setCompleteData] = useState({ amount: '', effective_date: new Date().toISOString().split('T')[0], }); // Estados de transferência const [transferData, setTransferData] = useState({ from_account_id: '', to_account_id: '', amount: '', description: '', date: new Date().toISOString().split('T')[0], notes: '', }); // Estados de divisão const [splitData, setSplitData] = useState({ splits: [ { category_id: '', amount: '', description: '' }, { category_id: '', amount: '', description: '' }, ], }); // Estados de semanas expandidas const [expandedWeeks, setExpandedWeeks] = useState({}); // Estado para transferências expandidas const [expandedTransfers, setExpandedTransfers] = useState({}); // Estado para filtros colapsados const [filtersExpanded, setFiltersExpanded] = useState(false); // Estados de categorização em lote const [showBatchModal, setShowBatchModal] = useState(false); const [batchPreview, setBatchPreview] = useState(null); const [selectedTransactionIds, setSelectedTransactionIds] = useState(new Set()); const [loadingBatch, setLoadingBatch] = useState(false); const [executingBatch, setExecutingBatch] = useState(false); const [batchFormData, setBatchFormData] = useState({ category_id: '', cost_center_id: '', add_keyword: true, }); // Estados de converter para transferência const [showConvertTransferModal, setShowConvertTransferModal] = useState(false); const [convertTransferData, setConvertTransferData] = useState({ source: null, pairs: [], loading: false, selectedPairId: null, }); // Estados de conciliar com passivo const [showReconcileLiabilityModal, setShowReconcileLiabilityModal] = useState(false); const [reconcileLiabilityData, setReconcileLiabilityData] = useState({ transaction: null, installments: [], loading: false, selectedInstallmentId: null, }); // Estados de criação rápida (inline no modal de transação) const [showQuickCategoryModal, setShowQuickCategoryModal] = useState(false); const [showQuickCostCenterModal, setShowQuickCostCenterModal] = useState(false); const [quickCategoryData, setQuickCategoryData] = useState({ name: '', parent_id: '', type: 'both', icon: 'bi-tag', }); const [quickCostCenterData, setQuickCostCenterData] = useState({ name: '', code: '', }); const [savingQuickItem, setSavingQuickItem] = useState(false); const [quickCreateSource, setQuickCreateSource] = useState('form'); // 'form' ou 'batch' // Estado para modal de confirmação const [confirmModal, setConfirmModal] = useState({ show: false, message: '', onConfirm: null, variant: 'danger', }); // Estado para modal de categorização rápida individual const [showQuickCategorizeModal, setShowQuickCategorizeModal] = useState(false); const [quickCategorizeData, setQuickCategorizeData] = useState({ transaction: null, category_id: '', cost_center_id: '', add_keyword: true, }); const [savingQuickCategorize, setSavingQuickCategorize] = useState(false); // Calcular se há filtros ativos (excluindo date_field que é sempre preenchido) const hasActiveFilters = Object.entries(filters).some(([key, v]) => key !== 'date_field' && v !== ''); const activeFiltersCount = Object.entries(filters).filter(([key, v]) => key !== 'date_field' && v !== '').length; // Carregar dados iniciais const loadData = useCallback(async () => { try { setLoading(true); const [accountsRes, categoriesRes, costCentersRes] = await Promise.all([ accountService.getAll(), categoryService.getAll({ flat: true }), costCenterService.getAll(), ]); setAccounts(Array.isArray(accountsRes) ? accountsRes : (accountsRes.data || [])); setCategories(Array.isArray(categoriesRes) ? categoriesRes : (categoriesRes.data || [])); setCostCenters(Array.isArray(costCentersRes) ? costCentersRes : (costCentersRes.data || [])); } catch (err) { setError(err.response?.data?.message || 'Error loading data'); } finally { setLoading(false); } }, []); // Carregar transações por semana const loadWeeklyData = useCallback(async () => { try { setLoading(true); const params = { ...filters, page, per_page: perPage, currency: selectedCurrency, }; // Remover params vazios Object.keys(params).forEach(key => { if (params[key] === '' || params[key] === null) { delete params[key]; } }); const response = await transactionService.getByWeek(params); setWeeklyData(response); // Atualizar divisas disponíveis if (response.currencies && response.currencies.length > 0) { setAvailableCurrencies(response.currencies); // Se não tem divisa selecionada, usar a primeira if (!selectedCurrency && response.currencies.length > 0) { setSelectedCurrency(response.currencies[0]); } } // Expandir a primeira semana por padrão if (response.data && selectedCurrency && response.data[selectedCurrency]?.weeks?.length > 0) { const firstWeek = response.data[selectedCurrency].weeks[0]?.year_week; if (firstWeek && !expandedWeeks[firstWeek]) { setExpandedWeeks(prev => ({ ...prev, [firstWeek]: true })); } } } catch (err) { setError(err.response?.data?.message || 'Error loading transactions'); } finally { setLoading(false); } }, [filters, page, perPage, selectedCurrency, expandedWeeks]); useEffect(() => { loadData(); }, [loadData]); useEffect(() => { loadWeeklyData(); }, [filters, page, selectedCurrency]); // eslint-disable-line react-hooks/exhaustive-deps // Mobile resize detection useEffect(() => { const handleResize = () => setIsMobile(window.innerWidth < 768); window.addEventListener('resize', handleResize); return () => window.removeEventListener('resize', handleResize); }, []); // Efeito para destacar transação via URL useEffect(() => { const highlightId = searchParams.get('highlight'); if (highlightId) { const id = parseInt(highlightId, 10); setHighlightedTransactionId(id); // Buscar transação para encontrar a semana correta transactionService.getById(id).then(transaction => { if (transaction && transaction.effective_date) { // Calcular year_week da transação const date = new Date(transaction.effective_date); const year = date.getFullYear(); const startOfYear = new Date(year, 0, 1); const days = Math.floor((date - startOfYear) / (24 * 60 * 60 * 1000)); const week = Math.ceil((days + startOfYear.getDay() + 1) / 7); const yearWeek = `${year}-W${String(week).padStart(2, '0')}`; // Expandir a semana da transação setExpandedWeeks(prev => ({ ...prev, [yearWeek]: true })); // Scroll para a transação após um pequeno delay setTimeout(() => { if (highlightedRef.current) { highlightedRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); } }, 500); } }).catch(err => { console.error('Error fetching transaction for highlight:', err); }); // Limpar o parâmetro da URL após 5 segundos setTimeout(() => { setHighlightedTransactionId(null); searchParams.delete('highlight'); setSearchParams(searchParams, { replace: true }); }, 5000); } }, [searchParams, setSearchParams]); // Handlers de filtro const handleFilterChange = (e) => { const { name, value } = e.target; setFilters(prev => ({ ...prev, [name]: value })); setPage(1); // Reset para primeira página ao filtrar }; const clearFilters = () => { setFilters({ account_id: '', category_id: '', cost_center_id: '', type: '', status: '', search: '', start_date: '', end_date: '', }); setPage(1); }; // Toggle expansão de semana const toggleWeekExpansion = (yearWeek) => { setExpandedWeeks(prev => ({ ...prev, [yearWeek]: !prev[yearWeek] })); }; // Handlers de formulário const handleInputChange = (e) => { const { name, value } = e.target; setFormData(prev => ({ ...prev, [name]: value })); }; const resetForm = () => { setFormData({ account_id: accounts[0]?.id || '', category_id: '', cost_center_id: '', type: 'debit', planned_amount: '', amount: '', description: '', notes: '', planned_date: new Date().toISOString().split('T')[0], effective_date: '', status: 'pending', reference: '', }); setEditingTransaction(null); }; const openNewModal = () => { resetForm(); setShowModal(true); }; const openEditModal = (transaction) => { setEditingTransaction(transaction); setFormData({ account_id: transaction.account?.id || '', category_id: transaction.category?.id || '', cost_center_id: transaction.cost_center?.id || '', type: transaction.type, planned_amount: transaction.planned_amount, amount: transaction.amount || '', description: transaction.description, notes: transaction.notes || '', planned_date: transaction.planned_date, effective_date: transaction.effective_date || '', status: transaction.status, reference: transaction.reference || '', }); setShowModal(true); }; const openDetailModal = async (transaction) => { try { const detail = await transactionService.getById(transaction.id); setSelectedTransaction(detail); setShowDetailModal(true); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } }; const openCompleteModal = (transaction) => { setSelectedTransaction(transaction); setCompleteData({ amount: transaction.planned_amount, effective_date: new Date().toISOString().split('T')[0], }); setShowCompleteModal(true); }; // Salvar transação const handleSubmit = async (e) => { e.preventDefault(); try { const dataToSend = { ...formData, planned_amount: parseFloat(formData.planned_amount), amount: formData.amount ? parseFloat(formData.amount) : null, category_id: formData.category_id || null, cost_center_id: formData.cost_center_id || null, effective_date: formData.effective_date || null, reference: formData.reference || null, notes: formData.notes || null, }; if (editingTransaction) { await transactionService.update(editingTransaction.id, dataToSend); showToast(t('transactions.updated'), 'success'); } else { await transactionService.create(dataToSend); showToast(t('transactions.created'), 'success'); } setShowModal(false); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } }; // Marcar como concluída const handleComplete = async () => { try { await transactionService.complete(selectedTransaction.id, { amount: parseFloat(completeData.amount), effective_date: completeData.effective_date, }); showToast(t('transactions.completed'), 'success'); setShowCompleteModal(false); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } }; // Cancelar transação const handleCancel = async (transaction) => { setConfirmModal({ show: true, message: t('transactions.confirmCancel'), variant: 'warning', onConfirm: async () => { setConfirmModal(prev => ({ ...prev, show: false })); try { await transactionService.cancel(transaction.id); showToast(t('transactions.cancelled'), 'success'); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } } }); }; // Reverter para pendente const handleRevert = async (transaction) => { try { await transactionService.revert(transaction.id); showToast(t('transactions.reverted'), 'success'); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } }; // Duplicar transação const handleDuplicate = async (transaction) => { try { await transactionService.duplicate(transaction.id); showToast(t('transactions.duplicated'), 'success'); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } }; // Excluir transação const handleDelete = async (transaction) => { setConfirmModal({ show: true, message: t('transactions.confirmDelete'), variant: 'danger', onConfirm: async () => { setConfirmModal(prev => ({ ...prev, show: false })); try { await transactionService.delete(transaction.id); showToast(t('transactions.deleted'), 'success'); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } } }); }; // ======================================== // CRIAÇÃO RÁPIDA DE CATEGORIA/CENTRO DE CUSTO // ======================================== const openQuickCategoryModal = () => { setQuickCategoryData({ name: '', parent_id: '', type: formData.type === 'debit' ? 'expense' : formData.type === 'credit' ? 'income' : 'both', icon: 'bi-tag', }); setQuickCreateSource('form'); setShowQuickCategoryModal(true); }; // Abrir modal de criação rápida de categoria (do batch modal) const openQuickCategoryModalForBatch = () => { setQuickCategoryData({ name: '', parent_id: '', type: 'both', icon: 'bi-tag', }); setQuickCreateSource('batch'); setShowQuickCategoryModal(true); }; const handleQuickCategorySubmit = async (e) => { e.preventDefault(); try { setSavingQuickItem(true); const result = await categoryService.create(quickCategoryData); const newCategory = result.data || result; // Recarregar categorias const categoriesRes = await categoryService.getAll({ flat: true }); setCategories(Array.isArray(categoriesRes) ? categoriesRes : (categoriesRes.data || [])); // Selecionar a nova categoria (form ou batch) if (quickCreateSource === 'batch') { setBatchFormData(prev => ({ ...prev, category_id: newCategory.id })); } else { setFormData(prev => ({ ...prev, category_id: newCategory.id })); } showToast(t('categories.createSuccess'), 'success'); setShowQuickCategoryModal(false); } catch (err) { showToast(err.response?.data?.message || t('categories.createError'), 'danger'); } finally { setSavingQuickItem(false); } }; const openQuickCostCenterModal = () => { setQuickCostCenterData({ name: '', code: '', }); setQuickCreateSource('form'); setShowQuickCostCenterModal(true); }; // Abrir modal de criação rápida de centro de custo (do batch modal) const openQuickCostCenterModalForBatch = () => { setQuickCostCenterData({ name: '', code: '', }); setQuickCreateSource('batch'); setShowQuickCostCenterModal(true); }; const handleQuickCostCenterSubmit = async (e) => { e.preventDefault(); try { setSavingQuickItem(true); const result = await costCenterService.create(quickCostCenterData); const newCostCenter = result.data || result; // Recarregar centros de custo const costCentersRes = await costCenterService.getAll(); setCostCenters(Array.isArray(costCentersRes) ? costCentersRes : (costCentersRes.data || [])); // Selecionar o novo centro de custo (form ou batch) if (quickCreateSource === 'batch') { setBatchFormData(prev => ({ ...prev, cost_center_id: newCostCenter.id })); } else { setFormData(prev => ({ ...prev, cost_center_id: newCostCenter.id })); } showToast(t('costCenters.createSuccess'), 'success'); setShowQuickCostCenterModal(false); } catch (err) { showToast(err.response?.data?.message || t('costCenters.createError'), 'danger'); } finally { setSavingQuickItem(false); } }; // ======================================== // CATEGORIZAÇÃO RÁPIDA INDIVIDUAL // ======================================== const openQuickCategorizeModal = (transaction) => { setQuickCategorizeData({ transaction: transaction, category_id: transaction.category_id || '', cost_center_id: transaction.cost_center_id || '', add_keyword: true, }); setShowQuickCategorizeModal(true); }; const handleQuickCategorizeSubmit = async (e) => { e.preventDefault(); try { setSavingQuickCategorize(true); const updateData = { category_id: quickCategorizeData.category_id || null, cost_center_id: quickCategorizeData.cost_center_id || null, }; // Se add_keyword está ativo e há descrição original, criar keyword if (quickCategorizeData.add_keyword && quickCategorizeData.transaction.original_description) { // Atualizar transação e criar keyword via batch com uma única transação await categoryService.batchCategorize({ transaction_ids: [quickCategorizeData.transaction.id], category_id: updateData.category_id, cost_center_id: updateData.cost_center_id, add_keyword: true, }); } else { // Apenas atualizar a transação await transactionService.update(quickCategorizeData.transaction.id, updateData); } showToast(t('transactions.categorized'), 'success'); setShowQuickCategorizeModal(false); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } finally { setSavingQuickCategorize(false); } }; // ======================================== // EFETIVAÇÃO RÁPIDA // ======================================== const handleQuickComplete = async (transaction) => { try { await transactionService.quickComplete(transaction.id, { amount: transaction.planned_amount, effective_date: new Date().toISOString().split('T')[0], }); showToast(t('transactions.quickCompleted'), 'success'); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } }; // ======================================== // TRANSFERÊNCIA ENTRE CONTAS // ======================================== const openTransferModal = () => { setTransferData({ from_account_id: accounts[0]?.id || '', to_account_id: accounts[1]?.id || '', amount: '', description: '', date: new Date().toISOString().split('T')[0], notes: '', }); setShowTransferModal(true); }; const handleTransferChange = (e) => { const { name, value } = e.target; setTransferData(prev => ({ ...prev, [name]: value })); }; const handleTransferSubmit = async () => { try { await transactionService.transfer({ ...transferData, amount: parseFloat(transferData.amount), }); showToast(t('transactions.transferCreated'), 'success'); setShowTransferModal(false); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } }; // ======================================== // DIVISÃO DE TRANSAÇÕES // ======================================== const openSplitModal = (transaction) => { setSelectedTransaction(transaction); const originalAmount = parseFloat(transaction.amount || transaction.planned_amount); setSplitData({ splits: [ { category_id: '', amount: (originalAmount / 2).toFixed(2), description: '' }, { category_id: '', amount: (originalAmount / 2).toFixed(2), description: '' }, ], }); setShowSplitModal(true); }; // RECORRÊNCIA // ======================================== const openRecurrenceModal = (transaction) => { setRecurrenceTransaction(transaction); setShowRecurrenceModal(true); }; const handleRecurrenceSuccess = (result) => { showToast(t('recurring.createSuccess'), 'success'); loadData(); }; const handleSplitChange = (index, field, value) => { setSplitData(prev => { const newSplits = [...prev.splits]; newSplits[index] = { ...newSplits[index], [field]: value }; return { splits: newSplits }; }); }; const addSplitRow = () => { setSplitData(prev => ({ splits: [...prev.splits, { category_id: '', amount: '', description: '' }], })); }; const removeSplitRow = (index) => { if (splitData.splits.length <= 2) return; setSplitData(prev => ({ splits: prev.splits.filter((_, i) => i !== index), })); }; const handleSplitSubmit = async () => { try { const splitsToSend = splitData.splits.map(s => ({ category_id: s.category_id || null, amount: parseFloat(s.amount), description: s.description || null, })); await transactionService.split(selectedTransaction.id, splitsToSend); showToast(t('transactions.splitCreated'), 'success'); setShowSplitModal(false); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } }; const handleUnsplit = async (transaction) => { setConfirmModal({ show: true, message: t('transactions.unsplitConfirm'), variant: 'warning', onConfirm: async () => { setConfirmModal(prev => ({ ...prev, show: false })); try { await transactionService.unsplit(transaction.id); showToast(t('transactions.unsplitSuccess'), 'success'); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } } }); }; // ======================================== // DESVINCULAR TRANSFERÊNCIA // ======================================== const toggleTransferExpanded = (transferId) => { setExpandedTransfers(prev => ({ ...prev, [transferId]: !prev[transferId] })); }; const handleUnlinkTransfer = async (transfer) => { setConfirmModal({ show: true, message: t('transactions.unlinkTransferConfirm'), variant: 'warning', onConfirm: async () => { setConfirmModal(prev => ({ ...prev, show: false })); try { await transactionService.unlinkTransfer(transfer.debit_transaction_id || transfer.id); showToast(t('transactions.unlinkTransferSuccess'), 'success'); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } } }); }; // Converter transação em transferência const openConvertTransferModal = async (transaction) => { setShowConvertTransferModal(true); setConvertTransferData({ source: transaction, pairs: [], loading: true, selectedPairId: null, }); try { const result = await transferDetectionService.findPairs(transaction.id); setConvertTransferData(prev => ({ ...prev, source: result.source, pairs: result.pairs, loading: false, })); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); setShowConvertTransferModal(false); } }; const handleConvertToTransfer = async () => { const { source, selectedPairId } = convertTransferData; if (!source || !selectedPairId) return; try { // Determinar qual é o débito e qual é o crédito const debitId = source.type === 'debit' ? source.id : selectedPairId; const creditId = source.type === 'credit' ? source.id : selectedPairId; await transferDetectionService.confirm(debitId, creditId); showToast(t('transactions.convertToTransferSuccess'), 'success'); setShowConvertTransferModal(false); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } }; // Conciliar com passivo const openReconcileLiabilityModal = async (transaction) => { // Só transações de débito podem ser conciliadas if (transaction.type !== 'debit') { showToast(t('transactions.onlyDebitCanReconcile'), 'warning'); return; } // Já está conciliada? if (transaction.is_reconciled) { showToast(t('transactions.alreadyReconciled'), 'warning'); return; } setShowReconcileLiabilityModal(true); setReconcileLiabilityData({ transaction, installments: [], loading: true, selectedInstallmentId: null, }); try { const result = await transactionService.findLiabilityInstallments(transaction.id); setReconcileLiabilityData(prev => ({ ...prev, transaction: result.transaction, installments: result.installments, loading: false, })); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); setShowReconcileLiabilityModal(false); } }; const handleReconcileWithLiability = async () => { const { transaction, selectedInstallmentId } = reconcileLiabilityData; if (!transaction || !selectedInstallmentId) return; try { await transactionService.reconcileWithLiability(transaction.id, selectedInstallmentId); showToast(t('transactions.reconcileSuccess'), 'success'); setShowReconcileLiabilityModal(false); loadWeeklyData(); } catch (err) { showToast(err.response?.data?.message || t('common.error'), 'danger'); } }; // Toast helper const showToast = (message, type) => { setToast({ show: true, message, type }); }; // Obter moeda da conta const getAccountCurrency = (accountId) => { if (!accounts || !Array.isArray(accounts)) return 'EUR'; const account = accounts.find(a => a.id === parseInt(accountId)); return account?.currency || 'EUR'; }; // Obter categorias pai (sem parent_id) const parentCategories = Array.isArray(categories) ? categories.filter(c => !c.parent_id) : []; // Obter subcategorias de uma categoria pai const getSubcategories = (parentId) => { if (!categories || !Array.isArray(categories)) return []; return categories.filter(c => c.parent_id === parentId); }; // Status badge const getStatusBadge = (status) => { const colors = { pending: 'warning', completed: 'success', cancelled: 'secondary', effective: 'success', scheduled: 'primary', }; const labels = { pending: t('transactions.status.pending'), completed: t('transactions.status.completed'), cancelled: t('transactions.status.cancelled'), effective: t('transactions.status.effective'), scheduled: t('transactions.status.scheduled'), }; return {labels[status]}; }; // Type badge const getTypeBadge = (type) => { const colors = { credit: 'success', debit: 'danger' }; const labels = { credit: t('transactions.type.credit'), debit: t('transactions.type.debit') }; return {labels[type]}; }; // Formatar nome da semana const formatWeekName = (week) => { const startDate = new Date(week.start_date); const endDate = new Date(week.end_date); const startDay = startDate.getDate(); const endDay = endDate.getDate(); const startMonth = startDate.toLocaleDateString(getLocale(), { month: 'short' }); const endMonth = endDate.toLocaleDateString(getLocale(), { month: 'short' }); if (startMonth === endMonth) { return `${startDay} - ${endDay} ${startMonth}`; } return `${startDay} ${startMonth} - ${endDay} ${endMonth}`; }; // Funções de seleção de transações const getAllVisibleTransactionIds = () => { const ids = []; weeks.forEach(week => { week.transactions?.forEach(t => ids.push(t.id)); }); return ids; }; const handleToggleTransaction = (id) => { setSelectedTransactionIds(prev => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const handleSelectAllVisible = () => { const allIds = getAllVisibleTransactionIds(); if (selectedTransactionIds.size === allIds.length) { setSelectedTransactionIds(new Set()); } else { setSelectedTransactionIds(new Set(allIds)); } }; // Funções de categorização em lote const handleOpenBatchModal = () => { // Permitir abrir modal se há transações selecionadas OU se há filtros ativos if (selectedTransactionIds.size === 0 && !hasActiveFilters) { showToast(t('transactions.noTransactionsSelected'), 'warning'); return; } setShowBatchModal(true); setBatchFormData({ category_id: '', cost_center_id: '', add_keyword: !!filters.search, }); }; const handleCloseBatchModal = () => { setShowBatchModal(false); setBatchFormData({ category_id: '', cost_center_id: '', add_keyword: true, }); }; const handleExecuteBatchManual = async () => { if (!batchFormData.category_id && !batchFormData.cost_center_id) { showToast(t('transactions.batchSelectRequired'), 'warning'); return; } // Obter IDs selecionados const selectedIds = Array.from(selectedTransactionIds); // Verificar se há transações selecionadas ou filtros ativos if (selectedIds.length === 0 && !hasActiveFilters) { showToast(t('transactions.noTransactionsSelected'), 'warning'); return; } setExecutingBatch(true); try { const response = await categoryService.categorizeBatchManual( batchFormData.category_id || null, batchFormData.cost_center_id || null, filters, // Enviar filtros sempre batchFormData.add_keyword, selectedIds.length > 0 ? selectedIds : null // Só enviar IDs se houver seleção ); if (response.success) { let message = `${response.data.updated} ${t('transactions.batchUpdated')}`; if (response.data.keyword_added) { message += ` • ${t('transactions.keywordAdded')}: "${response.data.keyword_text}"`; } showToast(message, 'success'); handleCloseBatchModal(); setSelectedTransactionIds(new Set()); loadWeeklyData(); } } catch (error) { showToast(error.response?.data?.message || t('categories.batchError'), 'error'); } finally { setExecutingBatch(false); } }; // Obter dados da divisa atual const currentCurrencyData = weeklyData?.data?.[selectedCurrency]; const pagination = currentCurrencyData?.pagination; const weeks = currentCurrencyData?.weeks || []; // Calcular totais gerais const totalStats = weeks.reduce((acc, week) => ({ credits: acc.credits + (week.summary?.credits?.total || 0), debits: acc.debits + (week.summary?.debits?.total || 0), pending: acc.pending + (week.summary?.pending?.total || 0), transactions: acc.transactions + (week.summary?.total_transactions || 0), }), { credits: 0, debits: 0, pending: 0, transactions: 0 }); if (error) { return (
{t('transactions.empty')}
| 0 && week.transactions.every(t => selectedTransactionIds.has(t.id))} onChange={() => { const weekIds = week.transactions?.map(t => t.id) || []; const allSelected = weekIds.every(id => selectedTransactionIds.has(id)); setSelectedTransactionIds(prev => { const next = new Set(prev); weekIds.forEach(id => allSelected ? next.delete(id) : next.add(id)); return next; }); }} /> | )}{t('transactions.date')} | {t('transactions.description')} | {t('transactions.account')} | {t('transactions.category')} | {t('transactions.amount')} | {t('transactions.type.label')} | {t('transactions.status.label')} | |
|---|---|---|---|---|---|---|---|---|
| handleToggleTransaction(transaction.id)} /> | )}{formatDate(transaction.effective_date || transaction.planned_date)} {transaction.is_overdue && } |
{transaction.is_transfer && }
{transaction.is_reconciled && (
)}
openDetailModal(transaction)}>
{transaction.description}
{transaction.original_description && transaction.original_description !== transaction.description && (
{transaction.original_description.substring(0, 45)}{transaction.original_description.length > 45 ? '...' : ''}
)}
|
{transaction.account?.name} | {transaction.category && ( {transaction.category.name} )} | {transaction.type === 'credit' ? '+' : '-'} {formatCurrency(transaction.amount || transaction.planned_amount, selectedCurrency)} | {transaction.type === 'credit' ? t('transactions.type.credit') : t('transactions.type.debit')} | {t(`transactions.status.${transaction.status}`)} |
|
{selectedTransaction.description}
{selectedTransaction.account?.name}
{selectedTransaction.original_description}
{getTypeBadge(selectedTransaction.type)}
{getStatusBadge(selectedTransaction.status)}
{formatCurrency(selectedTransaction.planned_amount, selectedTransaction.account?.currency || 'EUR')}
{formatDate(selectedTransaction.planned_date)}
{formatCurrency(selectedTransaction.amount, selectedTransaction.account?.currency || 'EUR')}
{formatDate(selectedTransaction.effective_date)}
{selectedTransaction.category.parent?.name ? `${selectedTransaction.category.parent.name} > ${selectedTransaction.category.name}` : selectedTransaction.category.name }
{selectedTransaction.cost_center.name}
{selectedTransaction.reference}
{selectedTransaction.notes}
{t('transactions.completeDescription', { description: selectedTransaction.description })}
{t('common.loading')}
{t('transactions.description')}:{' '} {convertTransferData.source.description}
{t('accounts.account')}:{' '} {convertTransferData.source.account_name}
{t('transactions.amount')}:{' '} {convertTransferData.source.type === 'credit' ? '+' : '-'} {formatCurrency(convertTransferData.source.amount, 'EUR')}
{t('transactions.date')}:{' '} {formatDate(convertTransferData.source.date)}
{t('common.loading')}
{reconcileLiabilityData.transaction.description}