287 lines
18 KiB
TypeScript
287 lines
18 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { useCRM } from '../context/CRMContext';
|
|
import { ArrowUpRight, ArrowDownRight, TrendingUp, DollarSign, Calendar, CheckCircle, AlertCircle, Plus } from 'lucide-react';
|
|
import clsx from 'clsx';
|
|
import { PaymentMethod, Transaction } from '../types';
|
|
|
|
const Financial: React.FC = () => {
|
|
const { transactions, getFinancialSummary, addTransaction, updateTransaction, loading } = useCRM();
|
|
const { totalIncome, totalExpense, balance } = getFinancialSummary();
|
|
|
|
const [activeTab, setActiveTab] = useState<'payable' | 'receivable' | 'dashboard'>('dashboard');
|
|
const [showAddModal, setShowAddModal] = useState(false);
|
|
|
|
// New Bill State
|
|
const [newBill, setNewBill] = useState<Partial<Transaction>>({
|
|
type: 'Expense',
|
|
status: 'Pending',
|
|
paymentMethod: 'Boleto',
|
|
date: new Date().toISOString().split('T')[0],
|
|
dueDate: new Date().toISOString().split('T')[0]
|
|
});
|
|
|
|
const pendingPayables = transactions.filter(t => t.type === 'Expense' && t.status === 'Pending').sort((a, b) => new Date(a.dueDate || '').getTime() - new Date(b.dueDate || '').getTime());
|
|
const paidPayables = transactions.filter(t => t.type === 'Expense' && t.status === 'Paid');
|
|
|
|
// Sort Receivables by date desc
|
|
const receivables = transactions.filter(t => t.type === 'Income').sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());;
|
|
|
|
const handleSaveBill = async () => {
|
|
if (!newBill.description || !newBill.amount) return alert("Preencha descrição e valor");
|
|
|
|
await addTransaction({
|
|
type: 'Expense',
|
|
category: 'Contas',
|
|
description: newBill.description,
|
|
amount: Number(newBill.amount),
|
|
date: new Date(newBill.date!).toISOString(),
|
|
dueDate: new Date(newBill.dueDate!).toISOString(),
|
|
status: 'Pending', // Force pending
|
|
paymentMethod: newBill.paymentMethod as PaymentMethod || 'Boleto'
|
|
});
|
|
|
|
setShowAddModal(false);
|
|
setNewBill({ type: 'Expense', status: 'Pending', paymentMethod: 'Boleto', date: new Date().toISOString().split('T')[0], dueDate: new Date().toISOString().split('T')[0] });
|
|
};
|
|
|
|
const togglePaid = async (t: Transaction) => {
|
|
const newStatus = t.status === 'Paid' ? 'Pending' : 'Paid';
|
|
await updateTransaction(t.id, { status: newStatus });
|
|
}
|
|
|
|
const renderDashboard = () => (
|
|
<div className="space-y-6 animate-in fade-in duration-500">
|
|
{/* Summary Cards */}
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="glass-card p-6 rounded-[32px] border border-white/5 relative overflow-hidden group">
|
|
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
|
|
<ArrowUpRight size={80} className="text-emerald-500" />
|
|
</div>
|
|
<div className="relative z-10">
|
|
<p className="text-sm font-bold text-slate-500 uppercase tracking-widest mb-1">Entradas (Total)</p>
|
|
<h3 className="text-3xl font-bold text-white tracking-tight">R$ {totalIncome.toLocaleString('pt-BR')}</h3>
|
|
<div className="mt-4 flex items-center gap-2 text-emerald-400 text-xs font-bold bg-emerald-400/10 w-fit px-2 py-1 rounded-lg">
|
|
<TrendingUp size={14} /> +12% este mês
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="glass-card p-6 rounded-[32px] border border-white/5 relative overflow-hidden group">
|
|
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
|
|
<ArrowDownRight size={80} className="text-rose-500" />
|
|
</div>
|
|
<div className="relative z-10">
|
|
<p className="text-sm font-bold text-slate-500 uppercase tracking-widest mb-1">Saídas (Total)</p>
|
|
<h3 className="text-3xl font-bold text-white tracking-tight">R$ {totalExpense.toLocaleString('pt-BR')}</h3>
|
|
<div className="mt-4 flex items-center gap-2 text-rose-400 text-xs font-bold bg-rose-400/10 w-fit px-2 py-1 rounded-lg">
|
|
<TrendingUp size={14} /> Estável
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="glass-card p-6 rounded-[32px] border border-white/5 relative overflow-hidden group">
|
|
<div className="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
|
|
<DollarSign size={80} className="text-indigo-500" />
|
|
</div>
|
|
<div className="relative z-10">
|
|
<p className="text-sm font-bold text-slate-500 uppercase tracking-widest mb-1">Saldo Atual</p>
|
|
<h3 className={clsx("text-3xl font-bold tracking-tight", balance >= 0 ? "text-emerald-400" : "text-rose-400")}>
|
|
R$ {balance.toLocaleString('pt-BR')}
|
|
</h3>
|
|
<div className="mt-4 flex items-center gap-2 text-indigo-400 text-xs font-bold bg-indigo-400/10 w-fit px-2 py-1 rounded-lg">
|
|
Balanço Geral
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Upcoming Bills Alert */}
|
|
{pendingPayables.length > 0 && (
|
|
<div className="bg-amber-500/10 border border-amber-500/20 rounded-2xl p-4 flex items-center gap-4">
|
|
<div className="bg-amber-500/20 p-2 rounded-xl text-amber-500">
|
|
<AlertCircle size={24} />
|
|
</div>
|
|
<div>
|
|
<h4 className="text-white font-bold text-sm">Contas a Pagar Pendentes</h4>
|
|
<p className="text-slate-400 text-xs">Você tem {pendingPayables.length} contas pendentes. Verifique a aba "A Pagar".</p>
|
|
</div>
|
|
<button onClick={() => setActiveTab('payable')} className="ml-auto text-xs font-bold text-amber-400 hover:text-white underline">Ver Contas</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
|
|
const renderPayables = () => (
|
|
<div className="animate-in fade-in duration-500 space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-xl font-bold text-white tracking-tight">Contas a Pagar</h2>
|
|
<button onClick={() => setShowAddModal(true)} className="bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded-xl text-xs font-bold flex items-center gap-2 shadow-lg">
|
|
<Plus size={16} /> Nova Conta
|
|
</button>
|
|
</div>
|
|
|
|
<div className="glass-card rounded-2xl border border-white/5 overflow-hidden">
|
|
<table className="w-full text-left">
|
|
<thead className="bg-black/20 text-[10px] font-bold text-slate-500 uppercase tracking-widest border-b border-white/5">
|
|
<tr>
|
|
<th className="px-6 py-4">Vencimento</th>
|
|
<th className="px-6 py-4">Descrição</th>
|
|
<th className="px-6 py-4">Valor</th>
|
|
<th className="px-6 py-4 text-center">Via</th>
|
|
<th className="px-6 py-4 text-center">Status</th>
|
|
<th className="px-6 py-4 text-center">Ação</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-white/5">
|
|
{pendingPayables.concat(paidPayables).map(t => {
|
|
const isOverdue = new Date(t.dueDate || '') < new Date() && t.status !== 'Paid';
|
|
return (
|
|
<tr key={t.id} className="hover:bg-white/[0.02] transition-colors">
|
|
<td className="px-6 py-4">
|
|
<div className={clsx("flex items-center gap-2 font-mono text-xs", isOverdue ? "text-rose-400 font-bold" : "text-slate-400")}>
|
|
<Calendar size={12} />
|
|
{t.dueDate ? new Date(t.dueDate).toLocaleDateString() : '-'}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4 text-sm font-medium text-slate-200">{t.description}</td>
|
|
<td className="px-6 py-4 text-sm font-mono text-rose-400">- R$ {t.amount.toLocaleString('pt-BR')}</td>
|
|
<td className="px-6 py-4 text-center text-xs text-slate-500">{t.paymentMethod}</td>
|
|
<td className="px-6 py-4 text-center">
|
|
<span className={clsx("px-2 py-1 rounded-full text-[10px] font-bold uppercase",
|
|
t.status === 'Paid' ? "bg-emerald-500/10 text-emerald-400" :
|
|
isOverdue ? "bg-rose-500/10 text-rose-400" :
|
|
"bg-amber-500/10 text-amber-400")}>
|
|
{isOverdue ? 'Vencido' : t.status === 'Paid' ? 'Pago' : 'Pendente'}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 text-center">
|
|
<button
|
|
onClick={() => togglePaid(t)}
|
|
className={clsx("p-2 rounded-lg transition-colors", t.status === 'Paid' ? "text-emerald-500 bg-emerald-500/10" : "text-slate-500 hover:text-emerald-400 hover:bg-emerald-400/10")}
|
|
title={t.status === 'Paid' ? "Marcar como Pendente" : "Marcar como Pago"}
|
|
>
|
|
<CheckCircle size={18} />
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
{transactions.filter(t => t.type === 'Expense').length === 0 && (
|
|
<div className="p-8 text-center text-slate-500 text-xs">Nenhuma conta registrada.</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
const renderReceivables = () => (
|
|
<div className="animate-in fade-in duration-500 space-y-6">
|
|
<h2 className="text-xl font-bold text-white tracking-tight">Contas a Receber (Vendas)</h2>
|
|
<div className="glass-card rounded-2xl border border-white/5 overflow-hidden">
|
|
<table className="w-full text-left">
|
|
<thead className="bg-black/20 text-[10px] font-bold text-slate-500 uppercase tracking-widest border-b border-white/5">
|
|
<tr>
|
|
<th className="px-6 py-4">Data</th>
|
|
<th className="px-6 py-4">Origem</th>
|
|
<th className="px-6 py-4">Valor</th>
|
|
<th className="px-6 py-4 text-center">Forma Pagto</th>
|
|
<th className="px-6 py-4 text-center">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-white/5">
|
|
{receivables.map(t => (
|
|
<tr key={t.id} className="hover:bg-white/[0.02] transition-colors">
|
|
<td className="px-6 py-4 font-mono text-xs text-slate-400">{new Date(t.date).toLocaleDateString()}</td>
|
|
<td className="px-6 py-4 text-sm font-medium text-slate-200">{t.description}</td>
|
|
<td className="px-6 py-4 text-sm font-mono text-emerald-400 font-bold">+ R$ {t.amount.toLocaleString('pt-BR')}</td>
|
|
<td className="px-6 py-4 text-center text-xs text-slate-500">{t.paymentMethod}</td>
|
|
<td className="px-6 py-4 text-center">
|
|
<span className="px-2 py-1 rounded-full text-[10px] font-bold uppercase bg-emerald-500/10 text-emerald-400">
|
|
{t.status || 'Paid'}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
{receivables.length === 0 && (
|
|
<div className="p-8 text-center text-slate-500 text-xs">Nenhuma venda registrada.</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="space-y-8">
|
|
{/* Tabs */}
|
|
<div className="flex gap-4 border-b border-white/5 pb-1">
|
|
<button onClick={() => setActiveTab('dashboard')} className={clsx("pb-3 text-sm font-bold transition-all relative", activeTab === 'dashboard' ? "text-white" : "text-slate-500 hover:text-slate-300")}>
|
|
Dashboard
|
|
{activeTab === 'dashboard' && <div className="absolute bottom-[-1px] left-0 w-full h-0.5 bg-indigo-500 rounded-full"></div>}
|
|
</button>
|
|
<button onClick={() => setActiveTab('payable')} className={clsx("pb-3 text-sm font-bold transition-all relative", activeTab === 'payable' ? "text-white" : "text-slate-500 hover:text-slate-300")}>
|
|
A Pagar
|
|
{activeTab === 'payable' && <div className="absolute bottom-[-1px] left-0 w-full h-0.5 bg-indigo-500 rounded-full"></div>}
|
|
</button>
|
|
<button onClick={() => setActiveTab('receivable')} className={clsx("pb-3 text-sm font-bold transition-all relative", activeTab === 'receivable' ? "text-white" : "text-slate-500 hover:text-slate-300")}>
|
|
A Receber
|
|
{activeTab === 'receivable' && <div className="absolute bottom-[-1px] left-0 w-full h-0.5 bg-indigo-500 rounded-full"></div>}
|
|
</button>
|
|
</div>
|
|
|
|
{activeTab === 'dashboard' && renderDashboard()}
|
|
{activeTab === 'payable' && renderPayables()}
|
|
{activeTab === 'receivable' && renderReceivables()}
|
|
|
|
{/* Add Bill Modal */}
|
|
{showAddModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
|
|
<div className="bg-[#0F1115] border border-white/10 rounded-[32px] w-full max-w-sm shadow-2xl p-8 relative">
|
|
<h3 className="text-xl font-bold text-white mb-6">Agendar Pagamento</h3>
|
|
<div className="space-y-4">
|
|
<input
|
|
type="text" placeholder="Descrição (ex: Aluguel)"
|
|
value={newBill.description || ''} onChange={e => setNewBill({ ...newBill, description: e.target.value })}
|
|
className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-sm text-white focus:border-indigo-500 outline-none"
|
|
/>
|
|
<input
|
|
type="number" placeholder="Valor (R$)"
|
|
value={newBill.amount || ''} onChange={e => setNewBill({ ...newBill, amount: e.target.value })}
|
|
className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-sm text-white focus:border-indigo-500 outline-none"
|
|
/>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase mb-1 block">Vencimento</label>
|
|
<input
|
|
type="date"
|
|
value={newBill.dueDate || ''} onChange={e => setNewBill({ ...newBill, dueDate: e.target.value })}
|
|
className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-sm text-white focus:border-indigo-500 outline-none"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="text-[10px] text-slate-500 font-bold uppercase mb-1 block">Forma Pagto</label>
|
|
<select
|
|
value={newBill.paymentMethod || 'Boleto'} onChange={e => setNewBill({ ...newBill, paymentMethod: e.target.value as PaymentMethod })}
|
|
className="w-full bg-black/20 border border-white/10 rounded-xl px-4 py-3 text-sm text-white focus:border-indigo-500 outline-none"
|
|
>
|
|
<option value="Boleto">Boleto</option>
|
|
<option value="Pix">Pix</option>
|
|
<option value="Cash">Dinheiro</option>
|
|
<option value="Transfer">TED/DOC</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<button onClick={handleSaveBill} className="w-full mt-4 bg-rose-600 hover:bg-rose-500 text-white font-bold py-3 rounded-xl shadow-lg transition-all">
|
|
Agendar Conta
|
|
</button>
|
|
<button onClick={() => setShowAddModal(false)} className="w-full text-xs text-slate-500 hover:text-white py-2">Cancelar</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Financial;
|