diff --git a/components/AccountsPayableView.tsx b/components/AccountsPayableView.tsx index ced847a..1ffbfa1 100644 --- a/components/AccountsPayableView.tsx +++ b/components/AccountsPayableView.tsx @@ -282,7 +282,7 @@ export const AccountsPayableView: React.FC = ({ expens
setNewExpense({...newExpense, category: val})} options={[ { value: 'Operacional', label: 'Operacional' }, @@ -296,7 +296,7 @@ export const AccountsPayableView: React.FC = ({ expens
setNewExpense({...newExpense, type: val})} options={[ { value: 'fixed', label: 'Fixa' }, diff --git a/components/AccountsReceivableView.tsx b/components/AccountsReceivableView.tsx index 6118272..3eab8b8 100644 --- a/components/AccountsReceivableView.tsx +++ b/components/AccountsReceivableView.tsx @@ -13,7 +13,7 @@ interface AccountsReceivableViewProps { export const AccountsReceivableView: React.FC = ({ receivables, setReceivables }) => { const { addToast } = useToast(); - const { companies } = useComFi(); + const { companies } = useComFi(); // Acesso ao CRM para gerar recorrência const [isModalOpen, setIsModalOpen] = useState(false); const [filterStatus, setFilterStatus] = useState<'all' | 'paid' | 'pending'>('all'); @@ -25,27 +25,33 @@ export const AccountsReceivableView: React.FC = ({ const [editingId, setEditingId] = useState(null); + // KPI Calculations 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')); + // --- ACTIONS --- + const handleGenerateRecurring = () => { - const currentMonth = new Date().toISOString().slice(0, 7); + const currentMonth = new Date().toISOString().slice(0, 7); // YYYY-MM 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') { + // Check duplicates for this month 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), @@ -53,7 +59,7 @@ export const AccountsReceivableView: React.FC = ({ companyName: company.fantasyName || company.name, category: service.category, value: service.price, - dueDate: today, + dueDate: today, // Simplificação: gera para hoje ou data padrão de vencimento status: 'pending', type: 'recurring' }); @@ -65,15 +71,15 @@ export const AccountsReceivableView: React.FC = ({ if (generatedCount > 0) { setReceivables(prev => [...prev, ...newReceivables]); - addToast({ type: 'success', title: 'Processamento Concluído', message: `${generatedCount} faturas recorrentes geradas.` }); + addToast({ type: 'success', title: 'Processamento Concluído', message: `${generatedCount} faturas recorrentes foram geradas.` }); } else { - addToast({ type: 'info', title: 'Tudo em dia', message: 'Cobranças mensais já processadas.' }); + addToast({ type: 'info', title: 'Tudo em dia', message: 'Todas as cobranças recorrentes deste mês já foram geradas.' }); } }; const handleSave = () => { if (!newReceivable.description || !newReceivable.value) { - addToast({ type: 'warning', title: 'Dados Incompletos' }); + addToast({ type: 'warning', title: 'Dados Incompletos', message: 'Preencha a descrição e o valor.' }); return; } @@ -82,105 +88,302 @@ export const AccountsReceivableView: React.FC = ({ ...newReceivable, id: editingId, value: Number(newReceivable.value), - category: newReceivable.category ?? 'Outros', - companyName: newReceivable.companyName ?? 'Avulso' + category: newReceivable.category || 'Outros', + companyName: newReceivable.companyName || 'Avulso' } as Receivable : r)); + addToast({ type: 'success', title: 'Atualizado', message: 'Recebimento atualizado com sucesso.' }); } 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' + category: newReceivable.category || 'Outros', + companyName: newReceivable.companyName || 'Avulso' } as Receivable; setReceivables([...receivables, item]); + addToast({ type: 'success', title: 'Criado', message: 'Novo recebimento registrado.' }); } setIsModalOpen(false); 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) => { - setReceivables(receivables.map(r => r.id === id ? { ...r, status: r.status === 'paid' ? 'pending' : 'paid' } : r)); + setReceivables(receivables.map(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 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"; + const openCreateModal = () => { + 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 (
+ + {/* KPI Cards */}
-
-

Previsto

R$ {totalReceivable.toLocaleString('pt-BR')}

+
+ +
+
+

Receita Total Prevista

+

R$ {totalReceivable.toLocaleString('pt-BR')}

+
-
-

Recebido

R$ {totalReceived.toLocaleString('pt-BR')}

+
+ +
+
+

Recebido

+

R$ {totalReceived.toLocaleString('pt-BR')}

+
-
-

A Receber

R$ {totalPending.toLocaleString('pt-BR')}

+
+ +
+
+

A Receber

+

R$ {totalPending.toLocaleString('pt-BR')}

+
-
-

Contas a Receber

+
+
+

Contas a Receber

+

Gestão de faturas, contratos e recebimentos avulsos.

+
- - + +
- - - - - - - - - - - {filteredList.map(item => ( - - - - - - - ))} - -
DescriçãoValorStatus
{item.description}
{item.companyName}
R$ {item.value.toLocaleString('pt-BR')} - - - -
+ {/* Toolbar */} +
+
+ + +
+
+ + + +
+
+ + {/* Table */} +
+ + + + + + + + + + + + + + {filteredList.map(item => ( + + + + + + + + + + ))} + +
Descrição / ClienteCategoriaVencimentoValorTipoStatus
+
{item.description}
+
{item.companyName}
+
+ {item.category} + +
+ + {new Date(item.dueDate).toLocaleDateString('pt-BR')} +
+
R$ {item.value.toLocaleString('pt-BR')} + + {item.type === 'recurring' ? 'Mensal' : 'Avulso'} + + + + +
+ + +
+
+ {filteredList.length === 0 && ( +
+ +

Nenhum lançamento encontrado.

+
+ )} +
+ {/* Modal */} {isModalOpen && (
setIsModalOpen(false)}>
-
-

Lançamento de Receita

-
- setNewReceivable({...newReceivable, description: e.target.value})} /> - setNewReceivable({...newReceivable, value: Number(e.target.value)})} /> -
- setNewReceivable({...newReceivable, category: val})} - options={[{value:'Serviços', label:'Serviços'}, {value:'Produtos', label:'Produtos'}, {value:'Outros', label:'Outros'}]} - /> - setNewReceivable({...newReceivable, type: val})} - options={[{value:'one-time', label:'Avulso'}, {value:'recurring', label:'Mensal'}]} +
+
+

{editingId ? 'Editar Recebimento' : 'Novo Recebimento'}

+ +
+ +
+
+ + setNewReceivable({...newReceivable, description: e.target.value})} />
- + +
+ + setNewReceivable({...newReceivable, companyName: e.target.value})} + /> +
+ +
+
+ + setNewReceivable({...newReceivable, value: Number(e.target.value)})} + /> +
+
+ + setNewReceivable({...newReceivable, dueDate: e.target.value})} + /> +
+
+ +
+
+ + setNewReceivable({...newReceivable, category: val})} + options={[ + { value: 'Serviços', label: 'Serviços' }, + { value: 'Produtos', label: 'Produtos' }, + { value: 'Reembolso', label: 'Reembolso' }, + { value: 'Outros', label: 'Outros' } + ]} + /> +
+
+ + setNewReceivable({...newReceivable, type: val})} + options={[ + { value: 'one-time', label: 'Avulso' }, + { value: 'recurring', label: 'Recorrente' } + ]} + /> +
+
+ +
diff --git a/index.html b/index.html index 989603e..d160577 100644 --- a/index.html +++ b/index.html @@ -14,9 +14,9 @@ body { font-family: 'Akzidenz Grotesk', 'Helvetica Neue', Helvetica, Arial, sans-serif; - background-color: #F8FAFC; + background-color: #F8FAFC; /* Slate 50 */ } - + /* Custom scrollbar */ ::-webkit-scrollbar { width: 8px; height: 8px; diff --git a/package.json b/package.json index ceed7ed..b2c9394 100644 --- a/package.json +++ b/package.json @@ -11,16 +11,13 @@ "preview": "vite preview", "start": "node server.js" }, - "engines": { - "node": ">=18.0.0" - }, "dependencies": { "@google/genai": "^1.39.0", - "express": "^4.21.1", "lucide-react": "^0.454.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "recharts": "^2.13.0" + "recharts": "^2.13.0", + "express": "^4.21.1" }, "devDependencies": { "@types/react": "^19.0.0", diff --git a/vite.config.ts b/vite.config.ts index cec5a04..dd6e2c4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,20 +8,8 @@ export default defineConfig({ define: { 'process.env': process.env }, - base: '/', build: { outDir: 'dist', emptyOutDir: true, - sourcemap: false, - rollupOptions: { - output: { - manualChunks: { - vendor: ['react', 'react-dom', 'recharts', 'lucide-react'] - } - } - } - }, - server: { - port: 3000 } });