1065 lines
59 KiB
TypeScript
1065 lines
59 KiB
TypeScript
'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<FinancialTransaction[]>([]);
|
|
const [entities, setEntities] = useState<Entity[]>([]);
|
|
const [crmCustomers, setCrmCustomers] = useState<CRMCustomer[]>([]);
|
|
const [categories, setCategories] = useState<FinancialCategory[]>([]);
|
|
const [accounts, setAccounts] = useState<BankAccount[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
const [bulkConfirmOpen, setBulkConfirmOpen] = useState(false);
|
|
const [transactionToDelete, setTransactionToDelete] = useState<string | null>(null);
|
|
const [editingTransaction, setEditingTransaction] = useState<Partial<FinancialTransaction> | null>(null);
|
|
const [formData, setFormData] = useState<Partial<FinancialTransaction>>({
|
|
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<Partial<FinancialCategory>>({
|
|
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<string | 'all'>('all');
|
|
const [selectedIds, setSelectedIds] = useState<(string | number)[]>([]);
|
|
|
|
const [isAccountModalOpen, setIsAccountModalOpen] = useState(false);
|
|
const [editingAccount, setEditingAccount] = useState<BankAccount | null>(null);
|
|
const [accountFormData, setAccountFormData] = useState<Partial<BankAccount>>({
|
|
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 (
|
|
<div className="flex items-center gap-1">
|
|
<span className="text-xs text-blue-600 font-bold bg-blue-50 px-1 rounded">CRM</span>
|
|
<span>{cust?.name || 'Cliente CRM'}</span>
|
|
</div>
|
|
);
|
|
}
|
|
if (row.entity_id) {
|
|
const ent = entities.find(e => e.id === row.entity_id);
|
|
return (
|
|
<div className="flex items-center gap-1">
|
|
<span className="text-xs text-purple-600 font-bold bg-purple-50 px-1 rounded">{ent?.type === 'supplier' ? 'FORN' : 'ENT'}</span>
|
|
<span>{ent?.name || 'Entidade ERP'}</span>
|
|
</div>
|
|
);
|
|
}
|
|
return <span className="text-zinc-400">Geral</span>;
|
|
};
|
|
|
|
if (loading && transactions.length === 0) return (
|
|
<div className="p-6 max-w-[1600px] mx-auto text-center py-20 text-zinc-500">
|
|
Carregando dados financeiros...
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="p-6 max-w-[1600px] mx-auto space-y-6">
|
|
<PageHeader
|
|
title={getPageTitle()}
|
|
description={type ? `Gerenciamento de ${getPageTitle().toLowerCase()}.` : "Controle suas contas a pagar, a receber e acompanhe seu fluxo de caixa."}
|
|
primaryAction={{
|
|
label: "Novo Lançamento",
|
|
icon: <PlusIcon className="w-5 h-5" />,
|
|
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: <ArrowsRightLeftIcon className="w-5 h-5" />,
|
|
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}
|
|
/>
|
|
|
|
<div className="space-y-8">
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<StatsCard
|
|
title={selectedAccountId === 'all' ? "A Receber (Geral)" : "A Receber (Conta)"}
|
|
value={formatCurrency(incomeTotal)}
|
|
icon={<ArrowUpCircleIcon className="w-6 h-6 text-emerald-500" />}
|
|
/>
|
|
<StatsCard
|
|
title={selectedAccountId === 'all' ? "A Pagar (Geral)" : "A Pagar (Conta)"}
|
|
value={formatCurrency(expenseTotal)}
|
|
icon={<ArrowDownCircleIcon className="w-6 h-6 text-rose-500" />}
|
|
/>
|
|
<StatsCard
|
|
title={selectedAccountId === 'all' ? "Saldo Geral" : `Saldo ${accounts.find(a => a.id === selectedAccountId)?.name}`}
|
|
value={formatCurrency(totalBalance)}
|
|
icon={<CheckCircleIcon className="w-6 h-6 text-brand-500" />}
|
|
/>
|
|
</div>
|
|
|
|
{!type && (
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between px-2">
|
|
<h2 className="text-[10px] font-black text-zinc-400 uppercase tracking-widest">Minhas Contas / Caixas</h2>
|
|
<button
|
|
onClick={() => { setEditingAccount(null); setAccountFormData({ name: '', bank_name: '', initial_balance: 0 }); setIsAccountModalOpen(true); }}
|
|
className="text-[10px] font-black text-brand-600 uppercase tracking-widest hover:text-brand-700 transition-colors"
|
|
>
|
|
+ Adicionar Conta
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-4">
|
|
<Card
|
|
onClick={() => 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'}`}
|
|
>
|
|
<div className="flex justify-between items-start mb-2">
|
|
<span className="text-[10px] font-bold text-zinc-400 uppercase">Visão Consolidada</span>
|
|
<BanknotesIcon className={`w-4 h-4 ${selectedAccountId === 'all' ? 'text-brand-500' : 'text-zinc-300'}`} />
|
|
</div>
|
|
<h3 className="text-sm font-black text-zinc-800 dark:text-zinc-100">Todas as Contas</h3>
|
|
<div className="mt-4 flex flex-col">
|
|
<span className="text-[10px] text-zinc-400 uppercase font-bold">Saldo Total</span>
|
|
<span className="text-lg font-black text-zinc-900 dark:text-white">
|
|
{formatCurrency(accounts.reduce((sum, acc) => sum + Number(acc.current_balance || 0), 0))}
|
|
</span>
|
|
</div>
|
|
</Card>
|
|
|
|
{accounts.map(acc => (
|
|
<Card
|
|
key={acc.id}
|
|
onClick={() => 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'}`}
|
|
>
|
|
<div className="flex justify-between items-start mb-2">
|
|
<span className="text-[10px] font-bold text-zinc-400 uppercase">{acc.bank_name || 'Geral'}</span>
|
|
<div className="flex gap-1">
|
|
<button
|
|
onClick={(e) => { e.stopPropagation(); setEditingAccount(acc); setAccountFormData(acc); setIsAccountModalOpen(true); }}
|
|
className="p-1 opacity-0 group-hover:opacity-100 transition-all text-zinc-400 hover:text-brand-500"
|
|
>
|
|
<PencilSquareIcon className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<h3 className="text-sm font-bold text-zinc-800 dark:text-zinc-100 truncate">{acc.name}</h3>
|
|
<div className="mt-4 flex flex-col">
|
|
<span className="text-[10px] text-zinc-400 uppercase font-bold">Saldo</span>
|
|
<span className={`text-lg font-black ${Number(acc.current_balance) >= 0 ? 'text-zinc-900 dark:text-white' : 'text-rose-500'}`}>
|
|
{formatCurrency(acc.current_balance)}
|
|
</span>
|
|
</div>
|
|
</Card>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<Card noPadding allowOverflow>
|
|
<div className="p-4 border-b border-zinc-100 dark:border-zinc-800 flex flex-col lg:flex-row gap-4 justify-between items-center bg-zinc-50/50 dark:bg-zinc-900/50">
|
|
<div className="flex-1 w-full lg:max-w-md">
|
|
<Input
|
|
placeholder="Pesquisar por descrição ou entidade..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
leftIcon={<MagnifyingGlassIcon className="w-5 h-5 text-zinc-400" />}
|
|
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]"
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col md:flex-row gap-4 w-full lg:w-auto items-center">
|
|
<div className="flex bg-zinc-100 dark:bg-zinc-800/50 p-1 rounded-xl items-center w-full md:w-auto overflow-x-auto md:overflow-visible no-scrollbar">
|
|
{[
|
|
{ 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) => (
|
|
<button
|
|
key={opt.value}
|
|
onClick={() => setStatusFilter(opt.value)}
|
|
className={`flex items-center gap-2 px-3 py-1.5 text-[11px] font-bold rounded-lg transition-all whitespace-nowrap ${statusFilter === opt.value
|
|
? 'bg-white dark:bg-zinc-700 text-zinc-900 dark:text-white shadow-sm ring-1 ring-black/5'
|
|
: 'text-zinc-500 hover:text-zinc-700 dark:hover:text-zinc-300'
|
|
}`}
|
|
>
|
|
{opt.color && <span className={`w-2 h-2 rounded-full ${opt.color}`} />}
|
|
{opt.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<div className="w-full md:w-[280px]">
|
|
<DatePicker
|
|
value={dateRange}
|
|
onChange={setDateRange}
|
|
placeholder="Filtrar Período"
|
|
buttonClassName="bg-white dark:bg-zinc-900 border-zinc-200 dark:border-zinc-800 text-zinc-700 dark:text-zinc-300 hover:border-zinc-400 h-[38px] rounded-xl"
|
|
/>
|
|
</div>
|
|
{hasActiveFilters && (
|
|
<button
|
|
onClick={handleClearFilters}
|
|
className="px-2 py-2 text-[10px] font-black uppercase tracking-widest text-rose-500 hover:text-rose-600 transition-colors whitespace-nowrap animate-in fade-in duration-200"
|
|
>
|
|
Limpar
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<DataTable
|
|
selectable
|
|
isLoading={loading}
|
|
selectedIds={selectedIds}
|
|
onSelectionChange={setSelectedIds}
|
|
data={filteredTransactions.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage)}
|
|
columns={[
|
|
{
|
|
header: 'Descrição',
|
|
accessor: (row) => (
|
|
<div className="flex flex-col">
|
|
<span className="font-bold text-zinc-900 dark:text-white">{row.description}</span>
|
|
<div className="flex items-center gap-2 mt-0.5">
|
|
{row.category_id && (
|
|
<span className="text-[10px] bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 rounded text-zinc-500 font-medium">
|
|
{categories.find(c => c.id === row.category_id)?.name}
|
|
</span>
|
|
)}
|
|
<div className="text-xs">
|
|
{getEntityDisplay(row)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
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) => (
|
|
<span className={`font-bold ${row.type === 'income' ? 'text-emerald-600' : 'text-rose-600'}`}>
|
|
{row.type === 'income' ? '+' : '-'} {formatCurrency(row.amount)}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
header: 'Status',
|
|
accessor: (row) => (
|
|
<span className={`inline-flex px-2 py-1 rounded-full text-[10px] font-bold uppercase ${row.status === 'paid' ? 'bg-emerald-50 text-emerald-700' : 'bg-amber-50 text-amber-700'}`}>
|
|
{row.status === 'paid' ? 'Pago' : 'Pendente'}
|
|
</span>
|
|
)
|
|
},
|
|
{
|
|
header: '',
|
|
className: 'text-right',
|
|
accessor: (row) => (
|
|
<div className="flex justify-end gap-2">
|
|
<button onClick={() => handleEdit(row)} className="p-2 text-zinc-400 hover:text-brand-500">
|
|
<PencilSquareIcon className="w-5 h-5" />
|
|
</button>
|
|
<button onClick={() => { setTransactionToDelete(row.id); setConfirmOpen(true); }} className="p-2 text-zinc-400 hover:text-rose-500">
|
|
<TrashIcon className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
]}
|
|
/>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Main Transaction Modal */}
|
|
<Transition show={isModalOpen} as={Fragment}>
|
|
<Dialog as="div" className="relative z-50" onClose={() => setIsModalOpen(false)}>
|
|
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0">
|
|
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm" />
|
|
</TransitionChild>
|
|
<div className="fixed inset-0 overflow-y-auto">
|
|
<div className="flex min-h-full items-center justify-center p-4">
|
|
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="ease-in duration-200" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95">
|
|
<DialogPanel className="w-full max-w-2xl transform overflow-hidden rounded-2xl bg-white dark:bg-zinc-900 p-8 shadow-xl transition-all border border-zinc-200 dark:border-zinc-800">
|
|
<DialogTitle className="text-xl font-bold mb-6">{editingTransaction ? 'Editar Lançamento' : 'Novo Lançamento'}</DialogTitle>
|
|
<form onSubmit={handleSave} className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="col-span-full">
|
|
<Input label="Descrição" placeholder="Ex: Aluguel Janeiro" value={formData.description} onChange={e => setFormData({ ...formData, description: e.target.value })} required />
|
|
</div>
|
|
|
|
<Input label="Valor" type="number" step="0.01" value={formData.amount} onChange={e => setFormData({ ...formData, amount: Number(e.target.value) })} required />
|
|
|
|
<CustomSelect
|
|
label="Tipo"
|
|
options={[{ label: 'Entrada (Receita)', value: 'income' }, { label: 'Saída (Despesa)', value: 'expense' }]}
|
|
value={formData.type || 'expense'}
|
|
onChange={val => {
|
|
const newType = val as any;
|
|
setFormData({ ...formData, type: newType, entity_id: '', crm_customer_id: '' });
|
|
}}
|
|
/>
|
|
|
|
<CustomSelect
|
|
label="Conta / Caixa de Destino"
|
|
options={accounts.map(acc => ({ label: `${acc.name} (${acc.bank_name})`, value: acc.id }))}
|
|
value={formData.account_id || ''}
|
|
onChange={val => setFormData({ ...formData, account_id: val })}
|
|
/>
|
|
|
|
<div className="relative">
|
|
<CustomSelect
|
|
label="Categoria"
|
|
options={[
|
|
{ label: 'Sem Categoria', value: '' },
|
|
...categories.filter(c => c.type === formData.type).map(c => ({ label: c.name, value: c.id }))
|
|
]}
|
|
value={formData.category_id || ''}
|
|
onChange={val => setFormData({ ...formData, category_id: val })}
|
|
/>
|
|
<button
|
|
type="button"
|
|
onClick={() => { setCategoryFormData({ name: '', type: formData.type as any }); setIsCategoryModalOpen(true); }}
|
|
className="absolute top-0 right-0 text-[10px] text-brand-600 font-bold hover:underline"
|
|
>
|
|
+ Nova
|
|
</button>
|
|
</div>
|
|
|
|
<CustomSelect
|
|
label={formData.type === 'income' ? 'Cliente / Origem' : 'Fornecedor / Destino'}
|
|
options={[
|
|
{ label: 'Geral (Sem vínculo)', value: '' },
|
|
// CRM Customers Group
|
|
...crmCustomers.map(c => ({ 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: '' });
|
|
}
|
|
}}
|
|
/>
|
|
|
|
<Input
|
|
label="Vencimento"
|
|
type="date"
|
|
value={formData.due_date}
|
|
onChange={e => setFormData({ ...formData, due_date: e.target.value })}
|
|
/>
|
|
|
|
<CustomSelect
|
|
label="Forma de Pagamento"
|
|
options={PAYMENT_METHODS}
|
|
value={formData.payment_method || 'pix'}
|
|
onChange={val => setFormData({ ...formData, payment_method: val })}
|
|
/>
|
|
|
|
<CustomSelect
|
|
label="Status"
|
|
options={[
|
|
{ label: 'Pendente', value: 'pending' },
|
|
{ label: 'Pago / Recebido', value: 'paid' },
|
|
{ label: 'Cancelado', value: 'cancelled' }
|
|
]}
|
|
value={formData.status || 'pending'}
|
|
onChange={val => setFormData({ ...formData, status: val as any })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3 mt-8 pt-6 border-t border-zinc-100 dark:border-zinc-800">
|
|
<button type="button" onClick={() => setIsModalOpen(false)} className="px-4 py-2 text-zinc-500 font-bold">Cancelar</button>
|
|
<button type="submit" className="px-8 py-2 text-white rounded-xl font-bold shadow-lg shadow-brand-500/20" style={{ background: 'var(--gradient)' }}>
|
|
{editingTransaction ? 'Salvar Alterações' : 'Confirmar Lançamento'}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</DialogPanel>
|
|
</TransitionChild>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</Transition>
|
|
|
|
{/* Quick Add Category Modal */}
|
|
<Transition show={isCategoryModalOpen} as={Fragment}>
|
|
<Dialog as="div" className="relative z-[60]" onClose={() => setIsCategoryModalOpen(false)}>
|
|
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0">
|
|
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm" />
|
|
</TransitionChild>
|
|
<div className="fixed inset-0 overflow-y-auto">
|
|
<div className="flex min-h-full items-center justify-center p-4">
|
|
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="ease-in duration-200" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95">
|
|
<DialogPanel className="w-full max-w-sm transform overflow-hidden rounded-2xl bg-white dark:bg-zinc-900 p-8 shadow-xl transition-all border border-zinc-200 dark:border-zinc-800">
|
|
<DialogTitle className="text-xl font-bold mb-6">Nova Categoria</DialogTitle>
|
|
<form onSubmit={handleCategorySave} className="space-y-4">
|
|
<Input label="Nome da Categoria" value={categoryFormData.name} onChange={e => setCategoryFormData({ ...categoryFormData, name: e.target.value })} required />
|
|
<CustomSelect label="Tipo" options={[{ label: 'Entrada', value: 'income' }, { label: 'Saída', value: 'expense' }]} value={categoryFormData.type || 'expense'} onChange={val => setCategoryFormData({ ...categoryFormData, type: val as any })} />
|
|
<div className="flex justify-end gap-3 mt-8">
|
|
<button type="button" onClick={() => setIsCategoryModalOpen(false)} className="px-4 py-2 text-zinc-500 font-bold">Cancelar</button>
|
|
<button type="submit" className="px-6 py-2 text-white rounded-xl font-bold" style={{ background: 'var(--gradient)' }}>Criar</button>
|
|
</div>
|
|
</form>
|
|
</DialogPanel>
|
|
</TransitionChild>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</Transition>
|
|
|
|
<ConfirmDialog isOpen={confirmOpen} onClose={() => setConfirmOpen(false)} onConfirm={handleConfirmDelete} title="Excluir Lançamento" message="Tem certeza? Esta ação não poderá ser desfeita." confirmText="Excluir Lançamento" />
|
|
|
|
<ConfirmDialog
|
|
isOpen={bulkConfirmOpen}
|
|
onClose={() => 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"
|
|
/>
|
|
|
|
<BulkActionBar
|
|
selectedCount={selectedIds.length}
|
|
onClearSelection={() => setSelectedIds([])}
|
|
actions={[
|
|
{
|
|
label: "Marcar Pago",
|
|
icon: <CheckCircleIcon className="w-5 h-5" />,
|
|
onClick: () => handleBulkStatusUpdate('paid'),
|
|
variant: 'primary'
|
|
},
|
|
{
|
|
label: "Marcar Pendente",
|
|
icon: <ClockIcon className="w-5 h-5" />,
|
|
onClick: () => handleBulkStatusUpdate('pending'),
|
|
variant: 'secondary'
|
|
},
|
|
{
|
|
label: "Excluir",
|
|
icon: <TrashIcon className="w-5 h-5" />,
|
|
onClick: handleBulkDelete,
|
|
variant: 'danger'
|
|
}
|
|
]}
|
|
/>
|
|
|
|
{/* Account Modal */}
|
|
<Transition show={isAccountModalOpen} as={Fragment}>
|
|
<Dialog as="div" className="relative z-50" onClose={() => setIsAccountModalOpen(false)}>
|
|
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0">
|
|
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm" />
|
|
</TransitionChild>
|
|
<div className="fixed inset-0 overflow-y-auto">
|
|
<div className="flex min-h-full items-center justify-center p-4">
|
|
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="ease-in duration-200" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95">
|
|
<DialogPanel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white dark:bg-zinc-900 p-8 shadow-xl transition-all border border-zinc-200 dark:border-zinc-800">
|
|
<DialogTitle className="text-xl font-bold mb-6">{editingAccount ? 'Editar Conta' : 'Nova Conta / Caixa'}</DialogTitle>
|
|
<form onSubmit={handleAccountSave} className="space-y-4">
|
|
<Input label="Nome da Conta / Caixa" placeholder="Ex: Banco Itaú ou Caixa Loja" value={accountFormData.name} onChange={e => setAccountFormData({ ...accountFormData, name: e.target.value })} required />
|
|
<Input label="Banco / Tipo" placeholder="Ex: Itaú, Nubank, Dinheiro" value={accountFormData.bank_name} onChange={e => setAccountFormData({ ...accountFormData, bank_name: e.target.value })} />
|
|
{!editingAccount && (
|
|
<Input label="Saldo Inicial" type="number" step="0.01" value={accountFormData.initial_balance} onChange={e => setAccountFormData({ ...accountFormData, initial_balance: Number(e.target.value) })} />
|
|
)}
|
|
<div className="flex justify-end gap-3 mt-8">
|
|
<button type="button" onClick={() => setIsAccountModalOpen(false)} className="px-4 py-2 text-zinc-500 font-bold">Cancelar</button>
|
|
<button type="submit" className="px-6 py-2 text-white rounded-xl font-bold" style={{ background: 'var(--gradient)' }}>Salvar</button>
|
|
</div>
|
|
</form>
|
|
</DialogPanel>
|
|
</TransitionChild>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</Transition>
|
|
|
|
{/* Transfer / Adjustment Modal */}
|
|
<Transition show={isTransferModalOpen} as={Fragment}>
|
|
<Dialog as="div" className="relative z-50" onClose={() => setIsTransferModalOpen(false)}>
|
|
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0">
|
|
<div className="fixed inset-0 bg-black/40 backdrop-blur-sm" />
|
|
</TransitionChild>
|
|
<div className="fixed inset-0 overflow-y-auto">
|
|
<div className="flex min-h-full items-center justify-center p-4">
|
|
<TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="ease-in duration-200" leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95">
|
|
<DialogPanel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white dark:bg-zinc-900 p-8 shadow-xl transition-all border border-zinc-200 dark:border-zinc-800">
|
|
<DialogTitle className="text-xl font-bold mb-6">Movimentação Interna</DialogTitle>
|
|
<form onSubmit={handleTransferSave} className="space-y-6">
|
|
<div className="flex p-1 bg-zinc-100 dark:bg-zinc-800 rounded-xl mb-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => setTransferData({ ...transferData, type: 'transfer' })}
|
|
className={`flex-1 py-2 text-xs font-bold rounded-lg transition-all ${transferData.type === 'transfer' ? 'bg-white dark:bg-zinc-700 shadow-sm text-brand-600' : 'text-zinc-500'}`}
|
|
>
|
|
Transferência
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setTransferData({ ...transferData, type: 'adjustment_in' })}
|
|
className={`flex-1 py-2 text-xs font-bold rounded-lg transition-all ${transferData.type === 'adjustment_in' ? 'bg-emerald-500 text-white shadow-sm' : 'text-zinc-500'}`}
|
|
>
|
|
+ Crédito
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => setTransferData({ ...transferData, type: 'adjustment_out' })}
|
|
className={`flex-1 py-2 text-xs font-bold rounded-lg transition-all ${transferData.type === 'adjustment_out' ? 'bg-rose-500 text-white shadow-sm' : 'text-zinc-500'}`}
|
|
>
|
|
- Débito
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-4">
|
|
<Input
|
|
label="Valor"
|
|
type="number"
|
|
step="0.01"
|
|
value={transferData.amount}
|
|
onChange={e => setTransferData({ ...transferData, amount: Number(e.target.value) })}
|
|
required
|
|
/>
|
|
|
|
{transferData.type === 'transfer' ? (
|
|
<>
|
|
<CustomSelect
|
|
label="Conta de Origem"
|
|
options={accounts.map(acc => ({ label: acc.name, value: acc.id }))}
|
|
value={transferData.from_account_id}
|
|
onChange={val => setTransferData({ ...transferData, from_account_id: val })}
|
|
/>
|
|
<CustomSelect
|
|
label="Conta de Destino"
|
|
options={accounts.map(acc => ({ label: acc.name, value: acc.id }))}
|
|
value={transferData.to_account_id}
|
|
onChange={val => setTransferData({ ...transferData, to_account_id: val })}
|
|
/>
|
|
</>
|
|
) : (
|
|
<CustomSelect
|
|
label="Conta Bancária / Caixa"
|
|
options={accounts.map(acc => ({ label: acc.name, value: acc.id }))}
|
|
value={transferData.from_account_id}
|
|
onChange={val => setTransferData({ ...transferData, from_account_id: val })}
|
|
/>
|
|
)}
|
|
|
|
<Input
|
|
label="Descrição (Opcional)"
|
|
placeholder="Ex: Transferência de Saldo ou Correção"
|
|
value={transferData.description}
|
|
onChange={e => setTransferData({ ...transferData, description: e.target.value })}
|
|
/>
|
|
|
|
<Input
|
|
label="Data"
|
|
type="date"
|
|
value={transferData.date}
|
|
onChange={e => setTransferData({ ...transferData, date: e.target.value })}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3 mt-8">
|
|
<button type="button" onClick={() => setIsTransferModalOpen(false)} className="px-4 py-2 text-zinc-500 font-bold">Cancelar</button>
|
|
<button
|
|
type="submit"
|
|
className="px-8 py-2 text-white rounded-xl font-bold shadow-lg shadow-brand-500/20"
|
|
style={{ background: 'var(--gradient)' }}
|
|
>
|
|
Confirmar Movimentação
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</DialogPanel>
|
|
</TransitionChild>
|
|
</div>
|
|
</div>
|
|
</Dialog>
|
|
</Transition>
|
|
</div>
|
|
);
|
|
}
|