// Financeiro Module function Financeiro({ onNavigate }) { const [loading, setLoading] = React.useState(true); const [activeTab, setActiveTab] = React.useState('geral'); const [budgets, setBudgets] = React.useState([]); const [budgetItems, setBudgetItems] = React.useState([]); const [freelancers, setFreelancers] = React.useState([]); const [periodType, setPeriodType] = React.useState('mensal'); const [periodOffset, setPeriodOffset] = React.useState(0); React.useEffect(() => { Promise.all([ sb.from('budgets').select('*').order('created_at', { ascending: false }), sb.from('budget_items').select('*'), sb.from('freelancers').select('id, name, role, rate, payments, status').order('name'), ]).then(([{ data: b }, { data: bi }, { data: f }]) => { setBudgets(b || []); setBudgetItems(bi || []); setFreelancers(f || []); setLoading(false); }); }, []); // ── Período ────────────────────────────────────────────── const getRange = (type, offset) => { const now = new Date(); let start, end, label; if (type === 'todos') { return { start: new Date(2000, 0, 1), end: new Date(2099, 11, 31), label: 'Todo o período' }; } if (type === 'diario') { const d = new Date(now.getFullYear(), now.getMonth(), now.getDate() + offset); start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0); end = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59); label = d.toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }); label = label.charAt(0).toUpperCase() + label.slice(1); } else if (type === 'semanal') { const d = new Date(now.getFullYear(), now.getMonth(), now.getDate()); const dow = d.getDay(); d.setDate(d.getDate() - dow + offset * 7); start = new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0); end = new Date(d.getFullYear(), d.getMonth(), d.getDate() + 6, 23, 59, 59); const fmt = { day: 'numeric', month: 'short' }; label = `${start.toLocaleDateString('pt-BR', fmt)} – ${end.toLocaleDateString('pt-BR', { ...fmt, year: 'numeric' })}`; } else { const d = new Date(now.getFullYear(), now.getMonth() + offset, 1); start = new Date(d.getFullYear(), d.getMonth(), 1, 0, 0, 0); end = new Date(d.getFullYear(), d.getMonth() + 1, 0, 23, 59, 59); label = d.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' }); label = label.charAt(0).toUpperCase() + label.slice(1); } return { start, end, label }; }; const { start: rangeStart, end: rangeEnd, label: rangeLabel } = getRange(periodType, periodOffset); // Parseia "dd/mm/yyyy" → Date const parsePayDate = (str) => { if (!str) return null; const p = str.split('/'); if (p.length !== 3) return null; return new Date(+p[2], +p[1] - 1, +p[0]); }; const inRange = (d) => d && d >= rangeStart && d <= rangeEnd; // ── Dados filtrados ─────────────────────────────────────── const budgetTotals = React.useMemo(() => { const map = {}; (budgetItems || []).forEach(item => { if (!map[item.budget_id]) map[item.budget_id] = 0; map[item.budget_id] += (item.qty || 1) * (item.unit_price || 0); }); return map; }, [budgetItems]); const allPayments = freelancers.flatMap(f => (f.payments || []).map(p => ({ ...p, freelancer: f.name, freelancerRole: f.role })) ); // Filtrar pagamentos pelo período const filteredPayments = allPayments.filter(p => inRange(parsePayDate(p.data))); // Filtrar orçamentos pelo período (usa event_date ou created_at) const filteredBudgets = budgets.filter(b => { if (periodType === 'todos') return true; const d = b.event_date ? new Date(b.event_date) : (b.created_at ? new Date(b.created_at) : null); return inRange(d); }); const totalAprovado = filteredBudgets.filter(b => ['Aprovado','Concluído'].includes(b.status)).reduce((s,b) => s + (budgetTotals[b.id]||0), 0); const totalPendente = filteredBudgets.filter(b => ['Pendente','Aguardando'].includes(b.status)).reduce((s,b) => s + (budgetTotals[b.id]||0), 0); const totalPagoFreelancers = filteredPayments.reduce((s,p) => s + (p.valor||0), 0); const saldoLiquido = totalAprovado - totalPagoFreelancers; const recentPayments = [...filteredPayments].sort((a,b) => b.id - a.id); const OG = DS.colors.primary; const metodoColor = { PIX: '#10B981', Cartão: '#3B82F6', Dinheiro: '#F59E0B' }; const metodoEmoji = { PIX: '⚡', Cartão: '💳', Dinheiro: '💵' }; const statusColor = { Aprovado:'success', Concluído:'success', Pendente:'warning', Aguardando:'warning', Rascunho:'default', Cancelado:'danger' }; const statusFColor = { Disponível:'success', Escalado:'primary', Indisponível:'danger' }; const Tab = ({ id, label, icon }) => ( ); if (loading) return
; return (
{/* ── Header ── */}

Financeiro

Controle de receitas, orçamentos e pagamentos

{/* ── Seletor de período ── */}
{/* Toggle tipo */}
{[ { id:'diario', label:'Diário' }, { id:'semanal', label:'Semanal' }, { id:'mensal', label:'Mensal' }, { id:'todos', label:'Todos' }, ].map(t => ( ))}
{/* Navegação ◀ label ▶ */} {periodType !== 'todos' && (
{rangeLabel}
{periodOffset !== 0 && ( )}
)} {/* Resumo rápido do período */}
{filteredPayments.length} pgto{filteredPayments.length!==1?'s':''} ·{' '} {filteredBudgets.length} orçamento{filteredBudgets.length!==1?'s':''}
{/* ── Cards resumo ── */}
{[ { label:'Receita aprovada', value:totalAprovado, color:'#10B981', icon:'budget', desc:'Orçamentos aprovados / concluídos' }, { label:'Em negociação', value:totalPendente, color:'#F59E0B', icon:'clock', desc:'Orçamentos pendentes de aprovação' }, { label:'Pago a freelancers', value:totalPagoFreelancers, color:'#EF4444', icon:'users', desc:`${filteredPayments.length} pagamento${filteredPayments.length!==1?'s':''} no período` }, { label:'Saldo líquido', value:saldoLiquido, color:saldoLiquido>=0?'#10B981':'#EF4444', icon:'reports', desc:'Receita aprovada − cachês pagos' }, ].map(c => (
{c.label}
{c.value<0&&'−'} R$ {Math.abs(c.value).toLocaleString('pt-BR',{minimumFractionDigits:2})}
{c.desc}
))}
{/* ── Tabs ── */}
{/* ── VISÃO GERAL ── */} {activeTab==='geral' && (
Pagamentos no período ({recentPayments.length})
{recentPayments.length===0 ? ( ) : (
{recentPayments.map((p,i) => (
{metodoEmoji[p.metodo]||'💰'}
{p.freelancer}
{p.descricao||p.freelancerRole} · {p.data}
− R$ {(p.valor||0).toLocaleString('pt-BR',{minimumFractionDigits:2})}
{p.metodo} {p.tipo==='parcial'&&Parcial}
))}
)}
{/* Orçamentos por status */}
Orçamentos no período
{filteredBudgets.length===0 ? (
Nenhum orçamento neste período.
) : ( ['Aprovado','Concluído','Pendente','Rascunho','Cancelado'].map(st => { const list = filteredBudgets.filter(b => b.status===st); if(list.length===0) return null; const total = list.reduce((s,b) => s+(budgetTotals[b.id]||0), 0); const col = { Aprovado:'#10B981', Concluído:'#3B82F6', Pendente:'#F59E0B', Rascunho:'#94A3B8', Cancelado:'#EF4444' }; return (
{st} {list.length}
R$ {total.toLocaleString('pt-BR',{minimumFractionDigits:0})}
); }) )} {/* Método de pagamento */}
Método de pagamento
{filteredPayments.length===0 ? (
Nenhum pagamento neste período.
) : ( ['PIX','Cartão','Dinheiro'].map(m => { const pags = filteredPayments.filter(p => p.metodo===m); const total = pags.reduce((s,p) => s+(p.valor||0), 0); if(total===0) return null; return (
{metodoEmoji[m]} {m} {pags.length}
R$ {total.toLocaleString('pt-BR',{minimumFractionDigits:2})}
); }) )}
{/* Atalhos */}
Atalhos
onNavigate&&onNavigate('budgets')}>Ir para Orçamentos onNavigate&&onNavigate('freelancers')}>Ir para Freelancers
)} {/* ── ORÇAMENTOS ── */} {activeTab==='orcamentos' && (
Orçamentos · {rangeLabel} {filteredBudgets.length} orçamento{filteredBudgets.length!==1?'s':''} · Aprovados: R$ {totalAprovado.toLocaleString('pt-BR',{minimumFractionDigits:2})}
{filteredBudgets.length===0 ? (
) : (
Código
Cliente
Data
Valor
Status
{filteredBudgets.map((b,i) => (
e.currentTarget.style.background='#F8FAFC'} onMouseLeave={e => e.currentTarget.style.background='transparent'}>
{b.code}
{b.client_name}
{b.notes&&
{String(b.notes).slice(0,55)}{String(b.notes).length>55?'…':''}
}
{b.event_date?new Date(b.event_date).toLocaleDateString('pt-BR'):'—'}
R$ {(budgetTotals[b.id]||0).toLocaleString('pt-BR',{minimumFractionDigits:2})}
{b.status}
))}
Total
R$ {filteredBudgets.reduce((s,b)=>s+(budgetTotals[b.id]||0),0).toLocaleString('pt-BR',{minimumFractionDigits:2})}
)} )} {/* ── FREELANCERS ── */} {activeTab==='freelancers' && (
Freelancers · {rangeLabel} Total pago: R$ {totalPagoFreelancers.toLocaleString('pt-BR',{minimumFractionDigits:2})}
Freelancer
Cachê/dia
Pago no período
Pgtos
Status
{freelancers.map((f,i) => { const pags = filteredPayments.filter(p => p.freelancer===f.name); const totalPago = pags.reduce((s,p)=>s+(p.valor||0),0); return (
e.currentTarget.style.background='#F8FAFC'} onMouseLeave={e=>e.currentTarget.style.background='transparent'}>
{f.name}
{f.role}
R$ {(f.rate||0).toLocaleString('pt-BR')}
0?'#10B981':DS.colors.textMuted }}> {totalPago>0?`R$ ${totalPago.toLocaleString('pt-BR',{minimumFractionDigits:2})}`:'—'}
{pags.length}
{f.status}
); })}
{freelancers.length} freelancer{freelancers.length!==1?'s':''}
R$ {totalPagoFreelancers.toLocaleString('pt-BR',{minimumFractionDigits:2})}
{filteredPayments.length}
{recentPayments.length>0&&(
Detalhamento · {rangeLabel}
{recentPayments.map((p,i)=>(
{metodoEmoji[p.metodo]||'💰'}
{p.freelancer} · {p.freelancerRole}
{p.descricao||'—'} · {p.data}
R$ {(p.valor||0).toLocaleString('pt-BR',{minimumFractionDigits:2})}
{p.metodo} {p.tipo==='parcial'&&Parcial}
))}
)}
)}
); } Object.assign(window, { Financeiro });