'use client'; import React, { useState, useEffect, Fragment } from 'react'; import { ArrowUpCircleIcon, ArrowDownCircleIcon, CheckCircleIcon, PlusIcon, PencilSquareIcon, TrashIcon, MagnifyingGlassIcon, ArrowsRightLeftIcon, BanknotesIcon, XMarkIcon, ClockIcon, } from "@heroicons/react/24/outline"; import { erpApi, FinancialTransaction, Entity, FinancialCategory, BankAccount } from '@/lib/api-erp'; import { formatCurrency } from '@/lib/format'; import { useToast } from '@/components/layout/ToastContext'; import { format, parseISO } from 'date-fns'; import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild } from '@headlessui/react'; import { PageHeader, StatsCard, DataTable, CustomSelect, Input, Card, DatePicker, BulkActionBar, ConfirmDialog, } from "@/components/ui"; interface FinanceContentProps { type?: 'receber' | 'pagar'; } interface CRMCustomer { id: string; name: string; email: string; company: string; } const PAYMENT_METHODS = [ { label: 'PIX', value: 'pix' }, { label: 'Boleto', value: 'boleto' }, { label: 'Cartão de Crédito', value: 'credit_card' }, { label: 'Cartão de Débito', value: 'debit_card' }, { label: 'Dinheiro', value: 'cash' }, { label: 'Transferência Bancária', value: 'transfer' }, ]; export default function FinanceContent({ type }: FinanceContentProps) { const toast = useToast(); const [transactions, setTransactions] = useState([]); const [entities, setEntities] = useState([]); const [crmCustomers, setCrmCustomers] = useState([]); const [categories, setCategories] = useState([]); const [accounts, setAccounts] = useState([]); const [loading, setLoading] = useState(true); const [isModalOpen, setIsModalOpen] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false); const [transactionToDelete, setTransactionToDelete] = useState(null); const [editingTransaction, setEditingTransaction] = useState | null>(null); const [formData, setFormData] = useState>({ description: '', amount: 0, type: type === 'receber' ? 'income' : 'expense', status: 'pending', due_date: format(new Date(), 'yyyy-MM-dd'), account_id: '', category_id: '', entity_id: '', crm_customer_id: '', payment_method: 'pix', }); // Category Modal State const [isCategoryModalOpen, setIsCategoryModalOpen] = useState(false); const [categoryFormData, setCategoryFormData] = useState>({ name: '', type: 'expense' }); const [searchTerm, setSearchTerm] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [dateRange, setDateRange] = useState<{ start: Date | null; end: Date | null }>({ start: null, end: null }); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 10; // Transfer Modal State const [isTransferModalOpen, setIsTransferModalOpen] = useState(false); const [transferData, setTransferData] = useState({ from_account_id: '', to_account_id: '', amount: 0, description: '', date: format(new Date(), 'yyyy-MM-dd'), type: 'transfer' as 'transfer' | 'adjustment_in' | 'adjustment_out' }); const [selectedAccountId, setSelectedAccountId] = useState('all'); const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]); const [isAccountModalOpen, setIsAccountModalOpen] = useState(false); const [editingAccount, setEditingAccount] = useState(null); const [accountFormData, setAccountFormData] = useState>({ name: '', bank_name: '', initial_balance: 0, }); useEffect(() => { fetchData(); }, []); const fetchData = async (silent = false) => { try { if (!silent) setLoading(true); const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; const [txData, entData, catData, accData, crmResp] = await Promise.all([ erpApi.getTransactions(), erpApi.getEntities(), erpApi.getFinancialCategories(), erpApi.getBankAccounts(), fetch('/api/crm/customers', { headers: { 'Authorization': `Bearer ${token}` } }).then(res => res.ok ? res.json() : []) ]); setTransactions(txData || []); setEntities(entData || []); setCategories(catData || []); setAccounts(accData || []); setCrmCustomers(crmResp?.customers || []); // Set default account if none selected and accounts exist if (accData?.length > 0 && !formData.account_id) { setFormData(prev => ({ ...prev, account_id: accData[0].id })); } if (accData?.length > 0 && !transferData.from_account_id) { setTransferData(prev => ({ ...prev, from_account_id: accData[0].id, to_account_id: accData[1]?.id || '' })); } } catch (error) { toast.error('Erro ao carregar', 'Não foi possível carregar os dados financeiros'); } finally { setLoading(false); setSelectedIds([]); } }; const handleClearFilters = () => { setSearchTerm(''); setStatusFilter('all'); setDateRange({ start: null, end: null }); setSelectedAccountId('all'); }; const handleSave = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.description || !formData.amount || formData.amount <= 0) { toast.error('Informe uma descrição e um valor válido.'); return; } const cleanData = { ...formData, due_date: formData.due_date ? new Date(formData.due_date + 'T12:00:00').toISOString() : undefined, payment_date: formData.status === 'paid' ? new Date().toISOString() : undefined, entity_id: formData.entity_id || undefined, crm_customer_id: formData.crm_customer_id || undefined, category_id: formData.category_id || undefined, account_id: formData.account_id || undefined, payment_method: formData.payment_method || undefined, }; try { if (editingTransaction?.id) { await erpApi.updateTransaction(editingTransaction.id, cleanData); toast.success('Lançamento atualizado!'); } else { await erpApi.createTransaction(cleanData); toast.success('Lançamento cadastrado!'); } setIsModalOpen(false); setEditingTransaction(null); await fetchData(true); } catch (error) { toast.error('Erro ao salvar lançamento'); } }; const handleCategorySave = async (e: React.FormEvent) => { e.preventDefault(); if (!categoryFormData.name) return; try { const newCat = await erpApi.createFinancialCategory(categoryFormData); toast.success('Categoria criada!'); await fetchData(true); setFormData(prev => ({ ...prev, category_id: newCat.id })); setIsCategoryModalOpen(false); } catch (error) { toast.error('Erro ao criar categoria'); } }; const handleConfirmDelete = async () => { if (!transactionToDelete) return; const idToDelete = String(transactionToDelete); const txToDelete = transactions.find(t => String(t.id) === idToDelete); if (!txToDelete) return; const originalTransactions = [...transactions]; const originalAccounts = [...accounts]; // 1. Optimistic Update: Transactions setTransactions(prev => prev.filter(t => String(t.id) !== idToDelete)); // 2. Optimistic Update: Account Balance (only if paid) if (txToDelete.status === 'paid' && txToDelete.account_id) { setAccounts(prev => prev.map(acc => { if (String(acc.id) === String(txToDelete.account_id)) { const amount = Number(txToDelete.amount); const newBalance = txToDelete.type === 'income' ? Number(acc.current_balance) - amount : Number(acc.current_balance) + amount; return { ...acc, current_balance: newBalance }; } return acc; })); } try { console.log(`🗑️ Deleting transaction ${idToDelete}...`); await erpApi.deleteTransaction(idToDelete); toast.success('Exclusão completa', 'Lançamento excluído com sucesso.'); // Refresh in background to ensure everything is in sync after a short delay setTimeout(() => fetchData(true), 500); } catch (error) { console.error('❌ Error deleting:', error); setTransactions(originalTransactions); setAccounts(originalAccounts); toast.error('Erro ao excluir', 'Não foi possível excluir o lançamento.'); } finally { setConfirmOpen(false); setTransactionToDelete(null); setSelectedIds([]); } }; const handleBulkDelete = async () => { if (selectedIds.length === 0) return; setBulkConfirmOpen(true); }; const handleConfirmBulkDelete = async () => { if (selectedIds.length === 0) return; const idsToDelete = selectedIds.map(String); const txsToDelete = transactions.filter(t => idsToDelete.includes(String(t.id))); const originalTransactions = [...transactions]; const originalAccounts = [...accounts]; // 1. Optimistic Update: Transactions setTransactions(prev => prev.filter(t => !idsToDelete.includes(String(t.id)))); // 2. Optimistic Update: Account Balances setAccounts(prev => { let newAccounts = [...prev]; txsToDelete.forEach(tx => { if (tx.status === 'paid' && tx.account_id) { newAccounts = newAccounts.map(acc => { if (String(acc.id) === String(tx.account_id)) { const amount = Number(tx.amount); const newBalance = tx.type === 'income' ? Number(acc.current_balance) - amount : Number(acc.current_balance) + amount; return { ...acc, current_balance: newBalance }; } return acc; }); } }); return newAccounts; }); try { await Promise.all(idsToDelete.map(id => erpApi.deleteTransaction(id))); toast.success('Exclusão completa', `${selectedIds.length} lançamentos excluídos com sucesso.`); setTimeout(() => fetchData(true), 500); } catch (error) { setTransactions(originalTransactions); setAccounts(originalAccounts); toast.error('Erro ao excluir', 'Ocorreu um erro ao excluir alguns lançamentos.'); } finally { setBulkConfirmOpen(false); setSelectedIds([]); } }; const handleBulkStatusUpdate = async (newStatus: 'paid' | 'pending') => { if (selectedIds.length === 0) return; // Optimistic update const originalTransactions = [...transactions]; setTransactions(prev => prev.map(t => selectedIds.includes(t.id) ? { ...t, status: newStatus } : t )); try { setLoading(true); await Promise.all(selectedIds.map(id => { const tx = originalTransactions.find(t => t.id === id); if (!tx) return Promise.resolve(); return erpApi.updateTransaction(id as string, { ...tx, status: newStatus }); })); toast.success('Status atualizado', 'O status dos itens selecionados foi atualizado.'); await fetchData(true); } catch (error) { setTransactions(originalTransactions); toast.error('Erro ao atualizar', 'Não foi possível atualizar o status de alguns lançamentos.'); } finally { setLoading(false); setSelectedIds([]); } }; const handleAccountSave = async (e: React.FormEvent) => { e.preventDefault(); try { if (editingAccount?.id) { await erpApi.updateBankAccount(editingAccount.id, accountFormData); toast.success('Conta atualizada!'); } else { await erpApi.createBankAccount(accountFormData); toast.success('Conta criada!'); } setIsAccountModalOpen(false); fetchData(); } catch (error) { toast.error('Erro ao salvar conta'); } }; const handleTransferSave = async (e: React.FormEvent) => { e.preventDefault(); if (transferData.amount <= 0) { toast.error('Informe um valor válido.'); return; } try { setLoading(true); const dateStr = new Date(transferData.date + 'T12:00:00').toISOString(); if (transferData.type === 'transfer') { if (!transferData.from_account_id || !transferData.to_account_id) { toast.error('Selecione as contas de origem e destino.'); return; } if (transferData.from_account_id === transferData.to_account_id) { toast.error('As contas de origem e destino devem ser diferentes.'); return; } // Saída da conta A await erpApi.createTransaction({ description: transferData.description || `Transferência para ${accounts.find(a => a.id === transferData.to_account_id)?.name}`, amount: transferData.amount, type: 'expense', status: 'paid', due_date: dateStr, payment_date: dateStr, account_id: transferData.from_account_id, payment_method: 'transfer' }); // Entrada na conta B await erpApi.createTransaction({ description: transferData.description || `Transferência de ${accounts.find(a => a.id === transferData.from_account_id)?.name}`, amount: transferData.amount, type: 'income', status: 'paid', due_date: dateStr, payment_date: dateStr, account_id: transferData.to_account_id, payment_method: 'transfer' }); toast.success('Transferência realizada com sucesso!'); } else { // Ajuste de entrada ou saída await erpApi.createTransaction({ description: transferData.description || (transferData.type === 'adjustment_in' ? 'Ajuste de Saldo (Crédito)' : 'Ajuste de Saldo (Débito)'), amount: transferData.amount, type: transferData.type === 'adjustment_in' ? 'income' : 'expense', status: 'paid', due_date: dateStr, payment_date: dateStr, account_id: transferData.from_account_id, payment_method: 'cash' }); toast.success('Ajuste de saldo realizado!'); } setIsTransferModalOpen(false); await fetchData(); } catch (error) { toast.error('Erro ao processar movimentação'); } finally { setLoading(false); } }; const handleEdit = (tx: FinancialTransaction) => { setEditingTransaction(tx); setFormData({ description: tx.description, amount: Number(tx.amount), type: tx.type, status: tx.status, due_date: tx.due_date ? format(parseISO(tx.due_date), 'yyyy-MM-dd') : '', entity_id: tx.entity_id || '', crm_customer_id: tx.crm_customer_id || '', category_id: tx.category_id || '', account_id: tx.account_id || '', payment_method: tx.payment_method || 'pix', }); setIsModalOpen(true); }; const hasActiveFilters = searchTerm !== '' || statusFilter !== 'all' || dateRange.start !== null || dateRange.end !== null || selectedAccountId !== 'all'; const filteredTransactions = transactions.filter(t => { const entityName = entities.find(e => e.id === t.entity_id)?.name || crmCustomers.find(c => c.id === t.crm_customer_id)?.name || ''; const matchesSearch = (t.description || '').toLowerCase().includes(searchTerm.toLowerCase()) || entityName.toLowerCase().includes(searchTerm.toLowerCase()); const matchesType = !type ? true : type === 'receber' ? t.type === 'income' : t.type === 'expense'; const matchesStatus = statusFilter === 'all' ? true : t.status === statusFilter; const matchesAccount = selectedAccountId === 'all' ? true : t.account_id === selectedAccountId; const txDate = t.due_date ? parseISO(t.due_date) : null; const matchesDate = !dateRange.start || !dateRange.end || !txDate ? true : (txDate >= dateRange.start && txDate <= dateRange.end); return matchesSearch && matchesType && matchesStatus && matchesDate && matchesAccount; }); const incomeTotal = (transactions || []) .filter(t => t.type === 'income' && t.status === 'pending' && (selectedAccountId === 'all' ? true : t.account_id === selectedAccountId)) .reduce((sum, t) => sum + Number(t.amount || 0), 0); const expenseTotal = (transactions || []) .filter(t => t.type === 'expense' && t.status === 'pending' && (selectedAccountId === 'all' ? true : t.account_id === selectedAccountId)) .reduce((sum, t) => sum + Number(t.amount || 0), 0); const totalBalance = selectedAccountId === 'all' ? accounts.reduce((sum, acc) => sum + Number(acc.current_balance || 0), 0) : Number(accounts.find(a => a.id === selectedAccountId)?.current_balance || 0); const getPageTitle = () => { if (type === 'receber') return 'Contas a Receber'; if (type === 'pagar') return 'Contas a Pagar'; return 'Caixa & Financeiro'; }; const getEntityDisplay = (row: FinancialTransaction) => { if (row.crm_customer_id) { const cust = crmCustomers.find(c => c.id === row.crm_customer_id); return (
CRM {cust?.name || 'Cliente CRM'}
); } if (row.entity_id) { const ent = entities.find(e => e.id === row.entity_id); return (
{ent?.type === 'supplier' ? 'FORN' : 'ENT'} {ent?.name || 'Entidade ERP'}
); } return Geral; }; if (loading && transactions.length === 0) return (
Carregando dados financeiros...
); return (
, onClick: () => { setEditingTransaction(null); setFormData({ description: '', amount: 0, type: type === 'receber' ? 'income' : 'expense', status: 'pending', due_date: format(new Date(), 'yyyy-MM-dd'), account_id: selectedAccountId !== 'all' ? selectedAccountId : (accounts[0]?.id || ''), category_id: '', entity_id: '', crm_customer_id: '', payment_method: 'pix', }); setIsModalOpen(true); } }} secondaryAction={!type ? { label: "Transferência / Ajuste", icon: , onClick: () => { setTransferData({ from_account_id: accounts[0]?.id || '', to_account_id: accounts[1]?.id || '', amount: 0, description: '', date: format(new Date(), 'yyyy-MM-dd'), type: 'transfer' }); setIsTransferModalOpen(true); } } : undefined} />
} /> } /> a.id === selectedAccountId)?.name}`} value={formatCurrency(totalBalance)} icon={} />
{!type && (

Minhas Contas / Caixas

setSelectedAccountId('all')} className={`relative cursor-pointer transition-all border-2 ${selectedAccountId === 'all' ? 'border-brand-500 bg-brand-50/10' : 'border-transparent hover:border-zinc-200'}`} >
Visão Consolidada

Todas as Contas

Saldo Total {formatCurrency(accounts.reduce((sum, acc) => sum + Number(acc.current_balance || 0), 0))}
{accounts.map(acc => ( setSelectedAccountId(acc.id)} className={`relative group cursor-pointer transition-all border-2 ${selectedAccountId === acc.id ? 'border-brand-500 bg-brand-50/10 shadow-sm' : 'border-transparent hover:border-zinc-200'}`} >
{acc.bank_name || 'Geral'}

{acc.name}

Saldo = 0 ? 'text-zinc-900 dark:text-white' : 'text-rose-500'}`}> {formatCurrency(acc.current_balance)}
))}
)}
setSearchTerm(e.target.value)} leftIcon={} className="bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 focus:border-zinc-400 dark:focus:border-zinc-500 shadow-none h-[38px]" />
{[ { label: 'Todos', value: 'all' }, { label: 'Pendentes', value: 'pending', color: 'bg-amber-500' }, { label: 'Pagos', value: 'paid', color: 'bg-emerald-500' }, { label: 'Cancelados', value: 'cancelled', color: 'bg-rose-500' }, ].map((opt) => ( ))}
{hasActiveFilters && ( )}
(
{row.description}
{row.category_id && ( {categories.find(c => c.id === row.category_id)?.name} )}
{getEntityDisplay(row)}
) }, { header: 'Conta / Caixa', accessor: (row) => accounts.find(a => a.id === row.account_id)?.name || '-' }, { header: 'Vencimento', accessor: (row) => row.due_date ? format(parseISO(row.due_date), 'dd/MM/yyyy') : '-' }, { header: 'Valor', accessor: (row) => ( {row.type === 'income' ? '+' : '-'} {formatCurrency(row.amount)} ) }, { header: 'Status', accessor: (row) => ( {row.status === 'paid' ? 'Pago' : 'Pendente'} ) }, { header: '', className: 'text-right', accessor: (row) => (
) } ]} />
{/* Main Transaction Modal */} setIsModalOpen(false)}>
{editingTransaction ? 'Editar Lançamento' : 'Novo Lançamento'}
setFormData({ ...formData, description: e.target.value })} required />
setFormData({ ...formData, amount: Number(e.target.value) })} required /> { const newType = val as any; setFormData({ ...formData, type: newType, entity_id: '', crm_customer_id: '' }); }} /> ({ label: `${acc.name} (${acc.bank_name})`, value: acc.id }))} value={formData.account_id || ''} onChange={val => setFormData({ ...formData, account_id: val })} />
c.type === formData.type).map(c => ({ label: c.name, value: c.id })) ]} value={formData.category_id || ''} onChange={val => setFormData({ ...formData, category_id: val })} />
({ label: `[CRM] ${c.name}`, value: `crm_${c.id}` })), // ERP Entities Group ...entities.filter(e => formData.type === 'income' ? (e.type === 'customer' || e.type === 'both') : (e.type === 'supplier' || e.type === 'both')).map(e => ({ label: `[ERP] ${e.name}`, value: `erp_${e.id}` })) ]} value={formData.crm_customer_id ? `crm_${formData.crm_customer_id}` : (formData.entity_id ? `erp_${formData.entity_id}` : '')} onChange={val => { if (val === '') { setFormData({ ...formData, crm_customer_id: '', entity_id: '' }); } else if (val.startsWith('crm_')) { setFormData({ ...formData, crm_customer_id: val.replace('crm_', ''), entity_id: '' }); } else if (val.startsWith('erp_')) { setFormData({ ...formData, entity_id: val.replace('erp_', ''), crm_customer_id: '' }); } }} /> setFormData({ ...formData, due_date: e.target.value })} /> setFormData({ ...formData, payment_method: val })} /> setFormData({ ...formData, status: val as any })} />
{/* Quick Add Category Modal */} setIsCategoryModalOpen(false)}>
Nova Categoria
setCategoryFormData({ ...categoryFormData, name: e.target.value })} required /> setCategoryFormData({ ...categoryFormData, type: val as any })} />
setConfirmOpen(false)} onConfirm={handleConfirmDelete} title="Excluir Lançamento" message="Tem certeza? Esta ação não poderá ser desfeita." confirmText="Excluir Lançamento" /> setBulkConfirmOpen(false)} onConfirm={handleConfirmBulkDelete} title="Excluir Itens Selecionados" message={`Tem certeza que deseja excluir os ${selectedIds.length} lançamentos selecionados? Esta ação não pode ser desfeita.`} confirmText="Excluir Tudo" variant="danger" /> setSelectedIds([])} actions={[ { label: "Marcar Pago", icon: , onClick: () => handleBulkStatusUpdate('paid'), variant: 'primary' }, { label: "Marcar Pendente", icon: , onClick: () => handleBulkStatusUpdate('pending'), variant: 'secondary' }, { label: "Excluir", icon: , onClick: handleBulkDelete, variant: 'danger' } ]} /> {/* Account Modal */} setIsAccountModalOpen(false)}>
{editingAccount ? 'Editar Conta' : 'Nova Conta / Caixa'}
setAccountFormData({ ...accountFormData, name: e.target.value })} required /> setAccountFormData({ ...accountFormData, bank_name: e.target.value })} /> {!editingAccount && ( setAccountFormData({ ...accountFormData, initial_balance: Number(e.target.value) })} /> )}
{/* Transfer / Adjustment Modal */} setIsTransferModalOpen(false)}>
Movimentação Interna
setTransferData({ ...transferData, amount: Number(e.target.value) })} required /> {transferData.type === 'transfer' ? ( <> ({ label: acc.name, value: acc.id }))} value={transferData.from_account_id} onChange={val => setTransferData({ ...transferData, from_account_id: val })} /> ({ label: acc.name, value: acc.id }))} value={transferData.to_account_id} onChange={val => setTransferData({ ...transferData, to_account_id: val })} /> ) : ( ({ label: acc.name, value: acc.id }))} value={transferData.from_account_id} onChange={val => setTransferData({ ...transferData, from_account_id: val })} /> )} setTransferData({ ...transferData, description: e.target.value })} /> setTransferData({ ...transferData, date: e.target.value })} />
); }