Files
ComFi/components/AccountsReceivableView.tsx
MMrp89 b91517dea2 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.
2026-02-10 01:34:52 -03:00

191 lines
9.7 KiB
TypeScript

import React, { useState } from 'react';
import { Search, Plus, DollarSign, CheckCircle2, TrendingUp, Trash2, X, Calendar, Pencil, RefreshCw, Sparkles, ChevronDown } from 'lucide-react';
import { Receivable } from '../types';
import { useToast } from '../contexts/ToastContext';
import { useComFi } from '../contexts/ComFiContext';
import { CustomSelect } from './CustomSelect';
interface AccountsReceivableViewProps {
receivables: Receivable[];
setReceivables: React.Dispatch<React.SetStateAction<Receivable[]>>;
}
export const AccountsReceivableView: React.FC<AccountsReceivableViewProps> = ({ receivables, setReceivables }) => {
const { addToast } = useToast();
const { companies } = useComFi();
const [isModalOpen, setIsModalOpen] = useState(false);
const [filterStatus, setFilterStatus] = useState<'all' | 'paid' | 'pending'>('all');
const [newReceivable, setNewReceivable] = useState<Partial<Receivable>>({
type: 'one-time',
status: 'pending',
dueDate: new Date().toISOString().split('T')[0]
});
const [editingId, setEditingId] = useState<string | null>(null);
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 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 handleGenerateRecurring = () => {
const currentMonth = new Date().toISOString().slice(0, 7);
const today = new Date().toISOString().split('T')[0];
let generatedCount = 0;
const newReceivables: Receivable[] = [];
companies.forEach(company => {
if (company.status !== 'active') return;
company.activeServices.forEach(service => {
if (service.billingType === 'recurring') {
const exists = receivables.find(r =>
r.companyName === (company.fantasyName || company.name) &&
r.description === service.name &&
r.dueDate.startsWith(currentMonth)
);
if (!exists) {
newReceivables.push({
id: Math.random().toString(36).substr(2, 9),
description: service.name,
companyName: company.fantasyName || company.name,
category: service.category,
value: service.price,
dueDate: today,
status: 'pending',
type: 'recurring'
});
generatedCount++;
}
}
});
});
if (generatedCount > 0) {
setReceivables(prev => [...prev, ...newReceivables]);
addToast({ type: 'success', title: 'Processamento Concluído', message: `${generatedCount} faturas recorrentes geradas.` });
} else {
addToast({ type: 'info', title: 'Tudo em dia', message: 'Cobranças mensais já processadas.' });
}
};
const handleSave = () => {
if (!newReceivable.description || !newReceivable.value) {
addToast({ type: 'warning', title: 'Dados Incompletos' });
return;
}
if (editingId) {
setReceivables(receivables.map(r => r.id === editingId ? {
...newReceivable,
id: editingId,
value: Number(newReceivable.value),
category: newReceivable.category ?? 'Outros',
companyName: newReceivable.companyName ?? 'Avulso'
} as Receivable : r));
} else {
const item: Receivable = {
...newReceivable,
id: Math.random().toString(36).substr(2, 9),
value: Number(newReceivable.value),
category: newReceivable.category ?? 'Outros',
companyName: newReceivable.companyName ?? 'Avulso'
} as Receivable;
setReceivables([...receivables, item]);
}
setIsModalOpen(false);
setEditingId(null);
};
const toggleStatus = (id: string) => {
setReceivables(receivables.map(r => r.id === id ? { ...r, status: r.status === 'paid' ? 'pending' : 'paid' } : r));
};
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";
return (
<div className="space-y-6 animate-fade-in">
<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="w-12 h-12 rounded-xl bg-slate-100 flex items-center justify-center text-slate-500"><DollarSign size={24} /></div>
<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 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"><CheckCircle2 size={24} /></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 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"><TrendingUp size={24} /></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 className="flex justify-between items-center">
<h1 className="text-2xl font-bold text-slate-800">Contas a Receber</h1>
<div className="flex gap-3">
<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>
<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>
</div>
</div>
<div className="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
<table className="w-full text-left">
<thead className="bg-slate-50">
<tr>
<th className="p-4 text-xs font-bold uppercase text-slate-500">Descrição</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">Status</th>
<th className="p-4"></th>
</tr>
</thead>
<tbody>
{filteredList.map(item => (
<tr key={item.id} className="border-b border-slate-50">
<td className="p-4 font-bold text-slate-800">{item.description}<div className="text-xs text-slate-400">{item.companyName}</div></td>
<td className="p-4 font-bold">R$ {item.value.toLocaleString('pt-BR')}</td>
<td className="p-4 text-center">
<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'}`}>
{item.status === 'paid' ? 'PAGO' : 'PENDENTE'}
</button>
</td>
<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>
</td>
</tr>
))}
</tbody>
</table>
</div>
{isModalOpen && (
<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="relative bg-white rounded-2xl p-6 w-full max-w-md animate-scale-up">
<h3 className="font-bold text-slate-800 text-lg mb-4">Lançamento de Receita</h3>
<div className="space-y-4">
<input type="text" className={inputClass} placeholder="Descrição" value={newReceivable.description ?? ''} onChange={e => setNewReceivable({...newReceivable, description: e.target.value})} />
<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">
<CustomSelect
value={newReceivable.category ?? 'Serviços'}
onChange={val => setNewReceivable({...newReceivable, category: val})}
options={[{value:'Serviços', label:'Serviços'}, {value:'Produtos', label:'Produtos'}, {value:'Outros', label:'Outros'}]}
/>
<CustomSelect
value={newReceivable.type ?? 'one-time'}
onChange={val => setNewReceivable({...newReceivable, type: val})}
options={[{value:'one-time', label:'Avulso'}, {value:'recurring', label:'Mensal'}]}
/>
</div>
<button onClick={handleSave} className="w-full py-3 bg-primary-500 text-white font-bold rounded-xl shadow-lg">Salvar</button>
</div>
</div>
</div>
)}
</div>
);
};