feat: Optimize build and dependencies
Introduce manual chunking for vendor dependencies to improve load times. Update Node.js engine requirement to >=18.0.0. Refactor `AccountsReceivableView` by removing commented-out code and unnecessary variables. Add a development server port configuration.
This commit is contained in:
@@ -13,7 +13,7 @@ interface AccountsReceivableViewProps {
|
|||||||
|
|
||||||
export const AccountsReceivableView: React.FC<AccountsReceivableViewProps> = ({ receivables, setReceivables }) => {
|
export const AccountsReceivableView: React.FC<AccountsReceivableViewProps> = ({ receivables, setReceivables }) => {
|
||||||
const { addToast } = useToast();
|
const { addToast } = useToast();
|
||||||
const { companies } = useComFi(); // Acesso ao CRM para gerar recorrência
|
const { companies } = useComFi();
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [filterStatus, setFilterStatus] = useState<'all' | 'paid' | 'pending'>('all');
|
const [filterStatus, setFilterStatus] = useState<'all' | 'paid' | 'pending'>('all');
|
||||||
|
|
||||||
@@ -25,33 +25,27 @@ export const AccountsReceivableView: React.FC<AccountsReceivableViewProps> = ({
|
|||||||
|
|
||||||
const [editingId, setEditingId] = useState<string | null>(null);
|
const [editingId, setEditingId] = useState<string | null>(null);
|
||||||
|
|
||||||
// KPI Calculations
|
|
||||||
const totalReceivable = receivables.reduce((acc, curr) => acc + curr.value, 0);
|
const totalReceivable = receivables.reduce((acc, curr) => acc + curr.value, 0);
|
||||||
const totalReceived = receivables.filter(r => r.status === 'paid').reduce((acc, curr) => acc + curr.value, 0);
|
const totalReceived = receivables.filter(r => r.status === 'paid').reduce((acc, curr) => acc + curr.value, 0);
|
||||||
const totalPending = receivables.filter(r => r.status === 'pending' || r.status === 'overdue').reduce((acc, curr) => acc + curr.value, 0);
|
const totalPending = receivables.filter(r => r.status === 'pending' || r.status === 'overdue').reduce((acc, curr) => acc + curr.value, 0);
|
||||||
|
|
||||||
const filteredList = receivables.filter(r => filterStatus === 'all' ? true : r.status === (filterStatus === 'paid' ? 'paid' : 'pending'));
|
const filteredList = receivables.filter(r => filterStatus === 'all' ? true : r.status === (filterStatus === 'paid' ? 'paid' : 'pending'));
|
||||||
|
|
||||||
// --- ACTIONS ---
|
|
||||||
|
|
||||||
const handleGenerateRecurring = () => {
|
const handleGenerateRecurring = () => {
|
||||||
const currentMonth = new Date().toISOString().slice(0, 7); // YYYY-MM
|
const currentMonth = new Date().toISOString().slice(0, 7);
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
let generatedCount = 0;
|
let generatedCount = 0;
|
||||||
const newReceivables: Receivable[] = [];
|
const newReceivables: Receivable[] = [];
|
||||||
|
|
||||||
companies.forEach(company => {
|
companies.forEach(company => {
|
||||||
if (company.status !== 'active') return;
|
if (company.status !== 'active') return;
|
||||||
|
|
||||||
company.activeServices.forEach(service => {
|
company.activeServices.forEach(service => {
|
||||||
if (service.billingType === 'recurring') {
|
if (service.billingType === 'recurring') {
|
||||||
// Check duplicates for this month
|
|
||||||
const exists = receivables.find(r =>
|
const exists = receivables.find(r =>
|
||||||
r.companyName === (company.fantasyName || company.name) &&
|
r.companyName === (company.fantasyName || company.name) &&
|
||||||
r.description === service.name &&
|
r.description === service.name &&
|
||||||
r.dueDate.startsWith(currentMonth)
|
r.dueDate.startsWith(currentMonth)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
newReceivables.push({
|
newReceivables.push({
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
@@ -59,7 +53,7 @@ export const AccountsReceivableView: React.FC<AccountsReceivableViewProps> = ({
|
|||||||
companyName: company.fantasyName || company.name,
|
companyName: company.fantasyName || company.name,
|
||||||
category: service.category,
|
category: service.category,
|
||||||
value: service.price,
|
value: service.price,
|
||||||
dueDate: today, // Simplificação: gera para hoje ou data padrão de vencimento
|
dueDate: today,
|
||||||
status: 'pending',
|
status: 'pending',
|
||||||
type: 'recurring'
|
type: 'recurring'
|
||||||
});
|
});
|
||||||
@@ -71,15 +65,15 @@ export const AccountsReceivableView: React.FC<AccountsReceivableViewProps> = ({
|
|||||||
|
|
||||||
if (generatedCount > 0) {
|
if (generatedCount > 0) {
|
||||||
setReceivables(prev => [...prev, ...newReceivables]);
|
setReceivables(prev => [...prev, ...newReceivables]);
|
||||||
addToast({ type: 'success', title: 'Processamento Concluído', message: `${generatedCount} faturas recorrentes foram geradas.` });
|
addToast({ type: 'success', title: 'Processamento Concluído', message: `${generatedCount} faturas recorrentes geradas.` });
|
||||||
} else {
|
} else {
|
||||||
addToast({ type: 'info', title: 'Tudo em dia', message: 'Todas as cobranças recorrentes deste mês já foram geradas.' });
|
addToast({ type: 'info', title: 'Tudo em dia', message: 'Cobranças mensais já processadas.' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!newReceivable.description || !newReceivable.value) {
|
if (!newReceivable.description || !newReceivable.value) {
|
||||||
addToast({ type: 'warning', title: 'Dados Incompletos', message: 'Preencha a descrição e o valor.' });
|
addToast({ type: 'warning', title: 'Dados Incompletos' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,302 +82,105 @@ export const AccountsReceivableView: React.FC<AccountsReceivableViewProps> = ({
|
|||||||
...newReceivable,
|
...newReceivable,
|
||||||
id: editingId,
|
id: editingId,
|
||||||
value: Number(newReceivable.value),
|
value: Number(newReceivable.value),
|
||||||
category: newReceivable.category || 'Outros',
|
category: newReceivable.category ?? 'Outros',
|
||||||
companyName: newReceivable.companyName || 'Avulso'
|
companyName: newReceivable.companyName ?? 'Avulso'
|
||||||
} as Receivable : r));
|
} as Receivable : r));
|
||||||
addToast({ type: 'success', title: 'Atualizado', message: 'Recebimento atualizado com sucesso.' });
|
|
||||||
} else {
|
} else {
|
||||||
const item: Receivable = {
|
const item: Receivable = {
|
||||||
...newReceivable,
|
...newReceivable,
|
||||||
id: Math.random().toString(36).substr(2, 9),
|
id: Math.random().toString(36).substr(2, 9),
|
||||||
value: Number(newReceivable.value),
|
value: Number(newReceivable.value),
|
||||||
category: newReceivable.category || 'Outros',
|
category: newReceivable.category ?? 'Outros',
|
||||||
companyName: newReceivable.companyName || 'Avulso'
|
companyName: newReceivable.companyName ?? 'Avulso'
|
||||||
} as Receivable;
|
} as Receivable;
|
||||||
setReceivables([...receivables, item]);
|
setReceivables([...receivables, item]);
|
||||||
addToast({ type: 'success', title: 'Criado', message: 'Novo recebimento registrado.' });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsModalOpen(false);
|
setIsModalOpen(false);
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
setNewReceivable({ type: 'one-time', status: 'pending', dueDate: new Date().toISOString().split('T')[0] });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = (id: string) => {
|
|
||||||
if(window.confirm("Excluir recebimento?")) {
|
|
||||||
setReceivables(receivables.filter(r => r.id !== id));
|
|
||||||
addToast({ type: 'info', title: 'Excluído', message: 'Registro removido.' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleStatus = (id: string) => {
|
const toggleStatus = (id: string) => {
|
||||||
setReceivables(receivables.map(r => {
|
setReceivables(receivables.map(r => r.id === id ? { ...r, status: r.status === 'paid' ? 'pending' : 'paid' } : r));
|
||||||
if(r.id === id) {
|
|
||||||
const newStatus = r.status === 'paid' ? 'pending' : 'paid';
|
|
||||||
if (newStatus === 'paid') addToast({ type: 'success', title: 'Recebido!', message: `Valor de R$ ${r.value} confirmado.` });
|
|
||||||
return { ...r, status: newStatus };
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const openEditModal = (item: Receivable) => {
|
|
||||||
setNewReceivable(item);
|
|
||||||
setEditingId(item.id);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const openCreateModal = () => {
|
const inputClass = "w-full p-3 bg-white border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-primary-500 text-slate-800";
|
||||||
setNewReceivable({
|
|
||||||
type: 'one-time',
|
|
||||||
status: 'pending',
|
|
||||||
dueDate: new Date().toISOString().split('T')[0]
|
|
||||||
});
|
|
||||||
setEditingId(null);
|
|
||||||
setIsModalOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
const inputClass = "w-full p-3 bg-white border border-slate-200 rounded-xl focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none text-slate-800";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6 animate-fade-in">
|
<div className="space-y-6 animate-fade-in">
|
||||||
|
|
||||||
{/* KPI Cards */}
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
<div className="bg-white p-6 rounded-[2rem] border border-slate-50 shadow-sm flex items-center gap-4">
|
<div className="bg-white p-6 rounded-[2rem] border border-slate-50 shadow-sm flex items-center gap-4">
|
||||||
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center text-slate-500">
|
<div className="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center text-slate-500"><DollarSign size={24} /></div>
|
||||||
<DollarSign size={24} />
|
<div><p className="text-slate-400 text-xs font-bold uppercase">Previsto</p><h3 className="text-2xl font-bold text-slate-800">R$ {totalReceivable.toLocaleString('pt-BR')}</h3></div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-slate-400 text-xs font-bold uppercase">Receita Total Prevista</p>
|
|
||||||
<h3 className="text-2xl font-bold text-slate-800">R$ {totalReceivable.toLocaleString('pt-BR')}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white p-6 rounded-[2rem] border border-slate-50 shadow-sm flex items-center gap-4">
|
<div className="bg-white p-6 rounded-[2rem] border border-slate-50 shadow-sm flex items-center gap-4">
|
||||||
<div className="w-12 h-12 rounded-xl bg-green-50 flex items-center justify-center text-green-600">
|
<div className="w-12 h-12 rounded-xl bg-green-50 flex items-center justify-center text-green-600"><CheckCircle2 size={24} /></div>
|
||||||
<CheckCircle2 size={24} />
|
<div><p className="text-slate-400 text-xs font-bold uppercase">Recebido</p><h3 className="text-2xl font-bold text-green-600">R$ {totalReceived.toLocaleString('pt-BR')}</h3></div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-slate-400 text-xs font-bold uppercase">Recebido</p>
|
|
||||||
<h3 className="text-2xl font-bold text-green-600">R$ {totalReceived.toLocaleString('pt-BR')}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white p-6 rounded-[2rem] border border-slate-50 shadow-sm flex items-center gap-4">
|
<div className="bg-white p-6 rounded-[2rem] border border-slate-50 shadow-sm flex items-center gap-4">
|
||||||
<div className="w-12 h-12 rounded-xl bg-amber-50 flex items-center justify-center text-amber-600">
|
<div className="w-12 h-12 rounded-xl bg-amber-50 flex items-center justify-center text-amber-600"><TrendingUp size={24} /></div>
|
||||||
<TrendingUp size={24} />
|
<div><p className="text-slate-400 text-xs font-bold uppercase">A Receber</p><h3 className="text-2xl font-bold text-amber-600">R$ {totalPending.toLocaleString('pt-BR')}</h3></div>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p className="text-slate-400 text-xs font-bold uppercase">A Receber</p>
|
|
||||||
<h3 className="text-2xl font-bold text-amber-600">R$ {totalPending.toLocaleString('pt-BR')}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<h1 className="text-2xl font-bold text-slate-800">Contas a Receber</h1>
|
||||||
<h1 className="text-2xl font-bold text-slate-800">Contas a Receber</h1>
|
|
||||||
<p className="text-slate-500">Gestão de faturas, contratos e recebimentos avulsos.</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button
|
<button onClick={handleGenerateRecurring} className="flex items-center gap-2 px-5 py-3 bg-indigo-500 text-white rounded-xl font-bold"><RefreshCw size={18} /> Gerar Mensalidades</button>
|
||||||
onClick={handleGenerateRecurring}
|
<button onClick={() => { setEditingId(null); setIsModalOpen(true); }} className="flex items-center gap-2 px-5 py-3 bg-green-500 text-white rounded-xl font-bold"><Plus size={18} /> Novo</button>
|
||||||
className="flex items-center gap-2 px-5 py-3 bg-indigo-500 text-white rounded-xl shadow-lg shadow-indigo-200/50 hover:bg-indigo-600 font-bold transition-all"
|
|
||||||
title="Gera cobranças baseadas nos serviços ativos do CRM"
|
|
||||||
>
|
|
||||||
<RefreshCw size={20} /> <span className="hidden sm:inline">Gerar Mensalidades</span>
|
|
||||||
</button>
|
|
||||||
<button onClick={openCreateModal} className="flex items-center gap-2 px-5 py-3 bg-green-500 text-white rounded-xl shadow-lg shadow-green-200/50 hover:bg-green-600 font-bold transition-all">
|
|
||||||
<Plus size={20} /> <span className="hidden sm:inline">Novo Recebimento</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
||||||
{/* Toolbar */}
|
<table className="w-full text-left">
|
||||||
<div className="p-4 border-b border-slate-100 flex flex-wrap gap-4 items-center">
|
<thead className="bg-slate-50">
|
||||||
<div className="relative flex-1 min-w-[200px]">
|
<tr>
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400" size={18} />
|
<th className="p-4 text-xs font-bold uppercase text-slate-500">Descrição</th>
|
||||||
<input
|
<th className="p-4 text-xs font-bold uppercase text-slate-500">Valor</th>
|
||||||
type="text"
|
<th className="p-4 text-xs font-bold uppercase text-slate-500 text-center">Status</th>
|
||||||
placeholder="Buscar cliente ou descrição..."
|
<th className="p-4"></th>
|
||||||
className="w-full pl-10 pr-4 py-2 bg-slate-50 border-none rounded-xl focus:ring-2 focus:ring-primary-500 outline-none text-slate-600"
|
</tr>
|
||||||
/>
|
</thead>
|
||||||
</div>
|
<tbody>
|
||||||
<div className="flex gap-2 bg-slate-50 p-1 rounded-xl">
|
{filteredList.map(item => (
|
||||||
<button
|
<tr key={item.id} className="border-b border-slate-50">
|
||||||
onClick={() => setFilterStatus('all')}
|
<td className="p-4 font-bold text-slate-800">{item.description}<div className="text-xs text-slate-400">{item.companyName}</div></td>
|
||||||
className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-colors ${filterStatus === 'all' ? 'bg-white shadow text-slate-800' : 'text-slate-500 hover:text-slate-700'}`}
|
<td className="p-4 font-bold">R$ {item.value.toLocaleString('pt-BR')}</td>
|
||||||
>
|
<td className="p-4 text-center">
|
||||||
Todos
|
<button onClick={() => toggleStatus(item.id)} className={`px-3 py-1 rounded-full text-xs font-bold border ${item.status === 'paid' ? 'bg-green-50 text-green-600 border-green-100' : 'bg-amber-50 text-amber-600 border-amber-100'}`}>
|
||||||
</button>
|
{item.status === 'paid' ? 'PAGO' : 'PENDENTE'}
|
||||||
<button
|
</button>
|
||||||
onClick={() => setFilterStatus('paid')}
|
</td>
|
||||||
className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-colors ${filterStatus === 'paid' ? 'bg-white shadow text-slate-800' : 'text-slate-500 hover:text-slate-700'}`}
|
<td className="p-4 text-right">
|
||||||
>
|
<button onClick={() => { setNewReceivable(item); setEditingId(item.id); setIsModalOpen(true); }} className="p-2 text-slate-400 hover:text-primary-500"><Pencil size={18} /></button>
|
||||||
Recebidos
|
</td>
|
||||||
</button>
|
</tr>
|
||||||
<button
|
))}
|
||||||
onClick={() => setFilterStatus('pending')}
|
</tbody>
|
||||||
className={`px-4 py-1.5 rounded-lg text-sm font-medium transition-colors ${filterStatus === 'pending' ? 'bg-white shadow text-slate-800' : 'text-slate-500 hover:text-slate-700'}`}
|
</table>
|
||||||
>
|
|
||||||
Pendentes
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Table */}
|
|
||||||
<div className="overflow-x-auto">
|
|
||||||
<table className="w-full text-left">
|
|
||||||
<thead className="bg-slate-50/50">
|
|
||||||
<tr>
|
|
||||||
<th className="p-4 text-xs font-bold uppercase text-slate-500">Descrição / Cliente</th>
|
|
||||||
<th className="p-4 text-xs font-bold uppercase text-slate-500">Categoria</th>
|
|
||||||
<th className="p-4 text-xs font-bold uppercase text-slate-500">Vencimento</th>
|
|
||||||
<th className="p-4 text-xs font-bold uppercase text-slate-500">Valor</th>
|
|
||||||
<th className="p-4 text-xs font-bold uppercase text-slate-500 text-center">Tipo</th>
|
|
||||||
<th className="p-4 text-xs font-bold uppercase text-slate-500 text-center">Status</th>
|
|
||||||
<th className="p-4"></th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-slate-50">
|
|
||||||
{filteredList.map(item => (
|
|
||||||
<tr key={item.id} className="hover:bg-slate-50 transition-colors group">
|
|
||||||
<td className="p-4">
|
|
||||||
<div className="font-bold text-slate-800">{item.description}</div>
|
|
||||||
<div className="text-xs text-slate-400">{item.companyName}</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-4">
|
|
||||||
<span className="px-2 py-1 bg-slate-100 rounded text-xs text-slate-600">{item.category}</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4 text-sm text-slate-600">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Calendar size={14} className="text-slate-400"/>
|
|
||||||
{new Date(item.dueDate).toLocaleDateString('pt-BR')}
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td className="p-4 font-bold text-slate-800">R$ {item.value.toLocaleString('pt-BR')}</td>
|
|
||||||
<td className="p-4 text-center">
|
|
||||||
<span className={`text-[10px] font-bold uppercase px-2 py-0.5 rounded ${item.type === 'recurring' ? 'bg-indigo-50 text-indigo-600' : 'bg-slate-100 text-slate-600'}`}>
|
|
||||||
{item.type === 'recurring' ? 'Mensal' : 'Avulso'}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="p-4 text-center">
|
|
||||||
<button onClick={() => toggleStatus(item.id)} className={`px-3 py-1 rounded-full text-xs font-bold border transition-all ${
|
|
||||||
item.status === 'paid' ? 'bg-green-50 text-green-600 border-green-200 hover:bg-green-100' :
|
|
||||||
item.status === 'overdue' ? 'bg-red-50 text-red-600 border-red-200 hover:bg-red-100' :
|
|
||||||
'bg-amber-50 text-amber-600 border-amber-200 hover:bg-amber-100'
|
|
||||||
}`}>
|
|
||||||
{item.status === 'paid' ? 'RECEBIDO' : item.status === 'overdue' ? 'ATRASADO' : 'PENDENTE'}
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
<td className="p-4 text-right">
|
|
||||||
<div className="flex justify-end gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
||||||
<button onClick={() => openEditModal(item)} className="p-2 text-slate-400 hover:text-primary-500 hover:bg-primary-50 rounded-lg transition-colors">
|
|
||||||
<Pencil size={18} />
|
|
||||||
</button>
|
|
||||||
<button onClick={() => handleDelete(item.id)} className="p-2 text-slate-400 hover:text-red-500 hover:bg-red-50 rounded-lg transition-colors">
|
|
||||||
<Trash2 size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{filteredList.length === 0 && (
|
|
||||||
<div className="p-10 text-center text-slate-400">
|
|
||||||
<Sparkles size={32} className="mx-auto mb-2 opacity-20"/>
|
|
||||||
<p>Nenhum lançamento encontrado.</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal */}
|
|
||||||
{isModalOpen && (
|
{isModalOpen && (
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
<div className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setIsModalOpen(false)}></div>
|
<div className="absolute inset-0 bg-slate-900/60 backdrop-blur-sm" onClick={() => setIsModalOpen(false)}></div>
|
||||||
<div className="relative bg-white rounded-2xl shadow-xl w-full max-w-md animate-scale-up flex flex-col max-h-[90vh]">
|
<div className="relative bg-white rounded-2xl p-6 w-full max-w-md animate-scale-up">
|
||||||
<div className="flex justify-between items-center px-6 py-4 border-b border-slate-100 flex-shrink-0">
|
<h3 className="font-bold text-slate-800 text-lg mb-4">Lançamento de Receita</h3>
|
||||||
<h3 className="font-bold text-slate-800 text-lg">{editingId ? 'Editar Recebimento' : 'Novo Recebimento'}</h3>
|
<div className="space-y-4">
|
||||||
<button onClick={() => setIsModalOpen(false)}><X size={20} className="text-slate-400 hover:text-slate-600"/></button>
|
<input type="text" className={inputClass} placeholder="Descrição" value={newReceivable.description ?? ''} onChange={e => setNewReceivable({...newReceivable, description: e.target.value})} />
|
||||||
</div>
|
<input type="number" className={inputClass} placeholder="Valor" value={newReceivable.value ?? ''} onChange={e => setNewReceivable({...newReceivable, value: Number(e.target.value)})} />
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="p-6 space-y-4 overflow-y-auto">
|
<CustomSelect
|
||||||
<div>
|
value={newReceivable.category ?? 'Serviços'}
|
||||||
<label className="block text-sm font-bold text-slate-800 mb-1">Descrição</label>
|
onChange={val => setNewReceivable({...newReceivable, category: val})}
|
||||||
<input
|
options={[{value:'Serviços', label:'Serviços'}, {value:'Produtos', label:'Produtos'}, {value:'Outros', label:'Outros'}]}
|
||||||
type="text"
|
/>
|
||||||
className={inputClass}
|
<CustomSelect
|
||||||
placeholder="Ex: Consultoria Extra"
|
value={newReceivable.type ?? 'one-time'}
|
||||||
value={newReceivable.description || ''}
|
onChange={val => setNewReceivable({...newReceivable, type: val})}
|
||||||
onChange={e => setNewReceivable({...newReceivable, description: e.target.value})}
|
options={[{value:'one-time', label:'Avulso'}, {value:'recurring', label:'Mensal'}]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<button onClick={handleSave} className="w-full py-3 bg-primary-500 text-white font-bold rounded-xl shadow-lg">Salvar</button>
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-bold text-slate-800 mb-1">Cliente / Empresa</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
className={inputClass}
|
|
||||||
placeholder="Nome do cliente"
|
|
||||||
value={newReceivable.companyName || ''}
|
|
||||||
onChange={e => setNewReceivable({...newReceivable, companyName: e.target.value})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-bold text-slate-800 mb-1">Valor (R$)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
className={inputClass}
|
|
||||||
placeholder="0,00"
|
|
||||||
value={newReceivable.value || ''}
|
|
||||||
onChange={e => setNewReceivable({...newReceivable, value: Number(e.target.value)})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-bold text-slate-800 mb-1">Vencimento</label>
|
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
className={inputClass}
|
|
||||||
value={newReceivable.dueDate}
|
|
||||||
onChange={e => setNewReceivable({...newReceivable, dueDate: e.target.value})}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-bold text-slate-800 mb-1">Categoria</label>
|
|
||||||
<CustomSelect
|
|
||||||
value={newReceivable.category ?? 'Serviços'}
|
|
||||||
onChange={(val) => setNewReceivable({...newReceivable, category: val})}
|
|
||||||
options={[
|
|
||||||
{ value: 'Serviços', label: 'Serviços' },
|
|
||||||
{ value: 'Produtos', label: 'Produtos' },
|
|
||||||
{ value: 'Reembolso', label: 'Reembolso' },
|
|
||||||
{ value: 'Outros', label: 'Outros' }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-bold text-slate-800 mb-1">Tipo</label>
|
|
||||||
<CustomSelect
|
|
||||||
value={newReceivable.type ?? 'one-time'}
|
|
||||||
onChange={(val) => setNewReceivable({...newReceivable, type: val})}
|
|
||||||
options={[
|
|
||||||
{ value: 'one-time', label: 'Avulso' },
|
|
||||||
{ value: 'recurring', label: 'Recorrente' }
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button onClick={handleSave} className="w-full py-3 bg-green-500 text-white font-bold rounded-xl mt-4 hover:bg-green-600 shadow-lg shadow-green-200">
|
|
||||||
{editingId ? 'Salvar Alterações' : 'Salvar Recebimento'}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
20
index.html
20
index.html
@@ -14,9 +14,9 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: 'Akzidenz Grotesk', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
font-family: 'Akzidenz Grotesk', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||||
background-color: #F8FAFC; /* Slate 50 */
|
background-color: #F8FAFC;
|
||||||
}
|
}
|
||||||
/* Custom scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
@@ -66,17 +66,17 @@
|
|||||||
<script type="importmap">
|
<script type="importmap">
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
"vite": "https://esm.sh/vite@^7.3.1",
|
|
||||||
"@google/genai": "https://esm.sh/@google/genai@^1.40.0",
|
|
||||||
"recharts": "https://esm.sh/recharts@^3.7.0",
|
|
||||||
"path": "https://esm.sh/path@^0.12.7",
|
|
||||||
"react/": "https://esm.sh/react@^19.2.4/",
|
|
||||||
"react": "https://esm.sh/react@^19.2.4",
|
"react": "https://esm.sh/react@^19.2.4",
|
||||||
"express": "https://esm.sh/express@^5.2.1",
|
|
||||||
"react-dom/": "https://esm.sh/react-dom@^19.2.4/",
|
"react-dom/": "https://esm.sh/react-dom@^19.2.4/",
|
||||||
"url": "https://esm.sh/url@^0.11.4",
|
"react/": "https://esm.sh/react@^19.2.4/",
|
||||||
|
"recharts": "https://esm.sh/recharts@^3.7.0",
|
||||||
|
"lucide-react": "https://esm.sh/lucide-react@^0.563.0",
|
||||||
|
"@google/genai": "https://esm.sh/@google/genai@^1.40.0",
|
||||||
|
"vite": "https://esm.sh/vite@^7.3.1",
|
||||||
"@vitejs/plugin-react": "https://esm.sh/@vitejs/plugin-react@^5.1.3",
|
"@vitejs/plugin-react": "https://esm.sh/@vitejs/plugin-react@^5.1.3",
|
||||||
"lucide-react": "https://esm.sh/lucide-react@^0.563.0"
|
"express": "https://esm.sh/express@^5.2.1",
|
||||||
|
"path": "https://esm.sh/path@^0.12.7",
|
||||||
|
"url": "https://esm.sh/url@^0.11.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,13 +11,16 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "node server.js"
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "^1.39.0",
|
"@google/genai": "^1.39.0",
|
||||||
|
"express": "^4.21.1",
|
||||||
"lucide-react": "^0.454.0",
|
"lucide-react": "^0.454.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"recharts": "^2.13.0",
|
"recharts": "^2.13.0"
|
||||||
"express": "^4.21.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19.0.0",
|
"@types/react": "^19.0.0",
|
||||||
|
|||||||
@@ -12,6 +12,16 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
outDir: 'dist',
|
outDir: 'dist',
|
||||||
emptyOutDir: true,
|
emptyOutDir: true,
|
||||||
sourcemap: false
|
sourcemap: false,
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
vendor: ['react', 'react-dom', 'recharts', 'lucide-react']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
port: 3000
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user