// 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.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 (
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'}>
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 });