// Lançamentos globais + Relatórios consolidados const { useState: useState_L, useMemo: useMemo_L, useEffect: useEffect_L } = React; function LancamentoGlobalFormModal({ data, lanc, onClose, onSave }) { const { useState: useState_M } = React; const isMobile = useIsMobile(); const toast = useToast(); const [f, setF] = useState_M({ empresaId: lanc?.empresaId || data.empresas[0]?.id || '', tipo: lanc?.tipo || 'saida', descricao: lanc?.descricao || '', valor: lanc?.valor || '', vencimento: lanc?.vencimento || todayISO(), formaPagamento: lanc?.formaPagamento || data.formasPagamento[0] || 'PIX', portadorId: lanc?.portadorId || data.portadores[0]?.id || '', centroCustoId: lanc?.centroCustoId || data.centrosCusto[0]?.id || '', }); const set = (k, v) => setF(p => ({ ...p, [k]: v })); const [isParcelado, setIsParcelado] = useState_M(false); const [numParcelas, setNumParcelas] = useState_M(2); const ccsFiltrados = data.centrosCusto.filter(c => c.tipo === f.tipo); function addMonthsString(isoDateStr, numMonths) { if (!isoDateStr) return ''; const d = new Date(isoDateStr + 'T12:00:00Z'); d.setUTCMonth(d.getUTCMonth() + numMonths); return d.toISOString().split('T')[0]; } function submit(e) { if (e) e.preventDefault(); if (!f.empresaId) return toast.push('Selecione uma empresa', 'error'); const errDesc = Validacao.required(f.descricao, 'Descrição'); if (errDesc) return toast.push(errDesc, 'error'); const errValor = Validacao.valor(f.valor); if (errValor) return toast.push(errValor, 'error'); const errVenc = Validacao.required(f.vencimento, 'Vencimento'); if (errVenc) return toast.push(errVenc, 'error'); const cc = ccsFiltrados.find(c => c.id === f.centroCustoId) || ccsFiltrados[0]; const valOriginal = parseFloat(String(f.valor).replace(',', '.')); if (isParcelado && numParcelas > 1) { const baseVal = Math.floor((valOriginal * 100) / numParcelas) / 100; const diff = Math.round((valOriginal - baseVal * numParcelas) * 100) / 100; const ref = uid('parc'); const parcelas = []; for (let i = 0; i < numParcelas; i++) { const vDate = i === 0 ? f.vencimento : addMonthsString(f.vencimento, i); parcelas.push({ ...f, id: uid('lanc'), valor: i === numParcelas - 1 ? baseVal + diff : baseVal, vencimento: vDate, competencia: competenciaFromDate(vDate), centroCustoId: cc.id, parcelaRef: ref, parcelaNum: i + 1, parcelaTotal: numParcelas, descricao: `${f.descricao} (${i + 1}/${numParcelas})`, pago: false }); } onSave(parcelas); } else { onSave({ ...f, id: lanc?.id || uid('lanc'), valor: valOriginal, centroCustoId: cc.id, competencia: competenciaFromDate(f.vencimento), pago: lanc?.pago ?? false, pagamento: lanc?.pagamento ?? null, observacao: lanc?.observacao ?? '' }); } } return ( Cancelar Salvar Lançamento }>
set('empresaId', e.target.value)} options={[ { value: '', label: 'Selecione uma empresa...' }, ...data.empresas.map(emp => ({ value: emp.id, label: emp.nome })) ]} />
{ set('tipo', e.target.value); const firstCc = data.centrosCusto.find(c => c.tipo === e.target.value); if (firstCc) set('centroCustoId', firstCc.id); }} options={[ { value: 'entrada', label: 'Entrada' }, { value: 'saida', label: 'Saída' } ]} /> set('vencimento', e.target.value)} />
set('valor', e.target.value)} placeholder="0,00" /> {!lanc?.id && ( )} set('formaPagamento', e.target.value)} options={ data.formasPagamento.map(fp => ({ value: fp, label: fp })) } />
{isParcelado && (
setNumParcelas(parseInt(e.target.value) || 2)} style={{ width: 100 }} /> {numParcelas} parcelas de aprox. {formatBRL((parseFloat(String(f.valor).replace(',','.')) || 0) / numParcelas)} / mês
)} set('descricao', e.target.value)} placeholder="Ex: Pagamento de honorários" autoFocus />
set('portadorId', e.target.value)} options={ data.portadores.map(p => ({ value: p.id, label: p.nome })) } /> set('centroCustoId', e.target.value)} options={ ccsFiltrados.map(c => ({ value: c.id, label: c.nome })) } />
); } function LancamentosGlobais({ data, onOpenEmpresa, onUpsertLanc, onPayLanc, onGoCentral }) { const isMobile = useIsMobile(); const [filtros, setFiltros] = useState_L({ empresa: 'todas', tipo: 'todos', status: 'todos', portador: 'todos', centroCusto: 'todos', formaPgto: 'todos', dataIni: '', dataFim: '', busca: '' }); const toast = useToast(); const hoje = todayISO(); const POR_PAGINA = 20; const [pagina, setPagina] = useState_L(1); const [novoLanc, setNovoLanc] = useState_L(null); const [pgto, setPgto] = useState_L(null); const [comprovante, setComprovante] = useState_L(null); useEffect_L(() => setPagina(1), [filtros]); const empMap = Object.fromEntries(data.empresas.map(e => [e.id, e])); const portMap = Object.fromEntries(data.portadores.map(p => [p.id, p])); const ccMap = Object.fromEntries(data.centrosCusto.map(c => [c.id, c])); const todos = useMemo_L(() => { const arr = []; data.empresas.forEach(e => { (data.lancamentos[e.id] || []).forEach(l => arr.push({ ...l, empresaNome: e.nome })); }); return arr; }, [data]); const filtrados = todos.filter(l => { if (filtros.empresa !== 'todas' && l.empresaId !== filtros.empresa) return false; if (filtros.tipo !== 'todos' && l.tipo !== filtros.tipo) return false; if (filtros.portador !== 'todos' && l.portadorId !== filtros.portador) return false; if (filtros.centroCusto !== 'todos' && l.centroCustoId !== filtros.centroCusto) return false; if (filtros.formaPgto !== 'todos' && l.formaPagamento !== filtros.formaPgto) return false; if (filtros.dataIni && l.vencimento < filtros.dataIni) return false; if (filtros.dataFim && l.vencimento > filtros.dataFim) return false; if (filtros.busca && !l.descricao.toLowerCase().includes(filtros.busca.toLowerCase())) return false; if (filtros.status !== 'todos' && lancStatus(l, hoje) !== filtros.status) return false; return true; }).sort((a, b) => b.vencimento.localeCompare(a.vencimento)); const totalFiltrados = filtrados.length; const totalPaginas = Math.ceil(totalFiltrados / POR_PAGINA); const paginados = filtrados.slice((pagina - 1) * POR_PAGINA, pagina * POR_PAGINA); return (
{novoLanc && ( setNovoLanc(null)} onSave={async (l) => { await onUpsertLanc(l); setNovoLanc(null); toast.push(Array.isArray(l) ? `${l.length} lançamentos salvos com sucesso!` : 'Lançamento salvo com sucesso!'); }} /> )} {pgto && ( setPgto(null)} onConfirm={(payload) => { onPayLanc(pgto.empresaId, pgto.id, payload); toast.push(`${pgto.tipo === 'entrada' ? 'Recebimento' : 'Pagamento'} confirmado`); setPgto(null); }} /> )} {comprovante && ( setComprovante(null)} /> )}
Lançamentos

Todos os Lançamentos

Visão consolidada de {todos.length} lançamentos em {data.empresas.length} empresas
setNovoLanc({ tipo: 'saida', vencimento: hoje, valor: '', descricao: '', portadorId: data.portadores[0]?.id || '', centroCustoId: data.centrosCusto.find(c => c.tipo === 'saida')?.id || '', formaPagamento: data.formasPagamento[0] || 'PIX' })}> Novo Lançamento { exportConsolidadoXLSX(data.empresas, data.lancamentos, data.portadores, data.centrosCusto); toast.push('Excel consolidado gerado'); }}> Exportar Excel
setFiltros({ ...filtros, busca: e.target.value })} placeholder="Descrição..." /> setFiltros({ ...filtros, empresa: e.target.value })} options={[ { value: "todas", label: "Todas" }, ...data.empresas.map(e => ({ value: e.id, label: e.nome })) ]} style={{ minWidth: isMobile ? 0 : 140 }} /> setFiltros({ ...filtros, tipo: e.target.value })} options={[ { value: "todos", label: "Todos" }, { value: "entrada", label: "Entradas" }, { value: "saida", label: "Saídas" } ]} style={{ minWidth: isMobile ? 0 : 140 }} /> setFiltros({ ...filtros, status: e.target.value })} options={[ { value: "todos", label: "Todos" }, { value: "pago", label: "Pago" }, { value: "parcial", label: "Parcial" }, { value: "em-dia", label: "Em dia" }, { value: "vencendo", label: "Vencendo" }, { value: "vencido", label: "Vencido" } ]} style={{ minWidth: isMobile ? 0 : 140 }} /> setFiltros({ ...filtros, portador: e.target.value })} options={[ { value: "todos", label: "Todos" }, ...data.portadores.map(p => ({ value: p.id, label: p.nome })) ]} style={{ minWidth: isMobile ? 0 : 140 }} /> setFiltros({ ...filtros, centroCusto: e.target.value })} options={[ { value: "todos", label: "Todos" }, ...data.centrosCusto.map(c => ({ value: c.id, label: c.nome })) ]} style={{ minWidth: isMobile ? 0 : 140 }} /> setFiltros({ ...filtros, dataIni: e.target.value })} /> setFiltros({ ...filtros, dataFim: e.target.value })} />
l.tipo === 'entrada').reduce((s, l) => s + l.valor, 0))} color="#16a34a" /> l.tipo === 'saida').reduce((s, l) => s + l.valor, 0))} color="#dc2626" /> lancStatus(l, hoje) === 'vencido').length} color="#dc2626" />
{paginados.map((l, i) => { const s = lancStatus(l, hoje); return ( setNovoLanc(l)}> ); })}
Empresa Vencimento Descrição Tipo Centro Custo Portador Valor Status Ações
{l.empresaNome} {formatDate(l.vencimento)} {l.descricao} {l.tipo === 'entrada' ? '↓ Entrada' : '↑ Saída'} {ccMap[l.centroCustoId]?.nome} {portMap[l.portadorId]?.nome}
{l.tipo === 'saida' && '-'}{formatBRL(l.valor)}
{l.statusPg === 'parcial' && (
Falta: {formatBRL(l.saldoRestante)} (pago {formatBRL(l.totalPago)})
)}
{(!l.pago || l.statusPg === 'parcial') && ( { e.stopPropagation(); setPgto(l); }}>Pagar )} {(l.pago || l.statusPg === 'parcial') && ( { e.stopPropagation(); setComprovante(l); }}>Comprovante )}
{totalFiltrados === 0 && (
📋
Nenhum lançamento encontrado
Cadastre uma empresa e comece a registrar suas entradas e saídas financeiras.
setNovoLanc({ tipo: 'saida', vencimento: hoje, valor: '', descricao: '', portadorId: data.portadores[0]?.id || '', centroCustoId: data.centrosCusto.find(c => c.tipo === 'saida')?.id || '', formaPagamento: data.formasPagamento[0] || 'PIX' })}> + Novo Lançamento Cadastrar empresa →
)} {totalPaginas > 1 && (
Mostrando {((pagina-1)*POR_PAGINA)+1}–{Math.min(pagina*POR_PAGINA, totalFiltrados)} de {totalFiltrados} lançamentos
setPagina(p => p - 1)}> ← Anterior {Array.from({ length: Math.min(5, totalPaginas) }, (_, i) => { let p; if (totalPaginas <= 5) p = i + 1; else if (pagina <= 3) p = i + 1; else if (pagina >= totalPaginas - 2) p = totalPaginas - 4 + i; else p = pagina - 2 + i; return ( ); })} setPagina(p => p + 1)}> Próxima →
)}
); } function Stat({ label, v, color = 'var(--c-text)' }) { return (
{label}
{v}
); } // Calcula o intervalo de datas [iniISO, fimISO] a partir do filtro de período function periodoRange(periodo, dataIni, dataFim) { if (periodo === 'custom') return [dataIni || '0000-01-01', dataFim || '9999-12-31']; const qtd = { '1m': 1, '3m': 3, '6m': 6, '12m': 12 }[periodo] ?? 6; const d = new Date(); const start = new Date(d.getFullYear(), d.getMonth() - (qtd - 1), 1); const end = new Date(d.getFullYear(), d.getMonth() + 1, 0); // último dia do mês corrente const iso = (dt) => `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}-${String(dt.getDate()).padStart(2, '0')}`; return [iso(start), iso(end)]; } // Lista de meses {label, comp} dentro de um intervalo ISO function mesesEntre(iniISO, fimISO) { const arr = []; const start = new Date((iniISO || '2000-01-01') + 'T00:00:00'); const end = new Date((fimISO || '2000-01-01') + 'T00:00:00'); let cur = new Date(start.getFullYear(), start.getMonth(), 1); const last = new Date(end.getFullYear(), end.getMonth(), 1); let guard = 0; while (cur <= last && guard < 240) { arr.push({ label: cur.toLocaleDateString('pt-BR', { month: 'short' }).replace('.', '') + '/' + String(cur.getFullYear()).slice(2), comp: `${String(cur.getMonth() + 1).padStart(2, '0')}/${cur.getFullYear()}` }); cur = new Date(cur.getFullYear(), cur.getMonth() + 1, 1); guard++; } return arr; } // ----- Relatórios Consolidados ----- function RelatoriosConsolidados({ data }) { const isMobile = useIsMobile(); const toast = useToast(); const hoje = todayISO(); const [cruzamento, setCruzamento] = useState_L('portador-empresa'); const [rankSort, setRankSort] = useState_L('saldo'); // saldo | receita | despesa | inadimplente const [abaRelatorio, setAbaRelatorio] = useState_L('historico'); const [paginaInad, setPaginaInad] = useState_L(1); const POR_PAGINA_INAD = 20; const [filtros, setFiltros] = useState_L({ empresa: 'todas', periodo: '6m', tipo: 'todos', status: 'todos', portador: 'todos', centroCusto: 'todos', dataIni: '', dataFim: '' }); useEffect_L(() => setPaginaInad(1), [filtros]); const empMap = Object.fromEntries(data.empresas.map(e => [e.id, e])); const portMap = Object.fromEntries(data.portadores.map(p => [p.id, p])); const ccMap = Object.fromEntries(data.centrosCusto.map(c => [c.id, c])); const [rIni, rFim] = useMemo_L(() => periodoRange(filtros.periodo, filtros.dataIni, filtros.dataFim), [filtros.periodo, filtros.dataIni, filtros.dataFim]); const meses = useMemo_L(() => mesesEntre(rIni, rFim), [rIni, rFim]); // Todos os lançamentos com nome da empresa const allLancs = useMemo_L(() => { const arr = []; data.empresas.forEach(e => (data.lancamentos[e.id] || []).forEach(l => arr.push({ ...l, empresaNome: e.nome }))); return arr; }, [data]); // Lançamentos após aplicar os filtros globais const lancs = useMemo_L(() => allLancs.filter(l => { if (filtros.empresa !== 'todas' && l.empresaId !== filtros.empresa) return false; if (filtros.tipo !== 'todos' && l.tipo !== filtros.tipo) return false; if (filtros.portador !== 'todos' && l.portadorId !== filtros.portador) return false; if (filtros.centroCusto !== 'todos' && l.centroCustoId !== filtros.centroCusto) return false; if (filtros.status !== 'todos' && lancStatus(l, hoje) !== filtros.status) return false; if (l.vencimento < rIni || l.vencimento > rFim) return false; return true; }), [allLancs, filtros, rIni, rFim, hoje]); // Empresas dentro do filtro + seus lançamentos filtrados const empresasFiltradas = useMemo_L(() => ( filtros.empresa === 'todas' ? data.empresas : data.empresas.filter(e => e.id === filtros.empresa) ), [data.empresas, filtros.empresa]); const lancsPorEmpresa = useMemo_L(() => { const map = {}; empresasFiltradas.forEach(e => map[e.id] = []); lancs.forEach(l => { if (map[l.empresaId]) map[l.empresaId].push(l); }); return map; }, [empresasFiltradas, lancs]); const hasFilter = filtros.empresa !== 'todas' || filtros.periodo !== '6m' || filtros.tipo !== 'todos' || filtros.status !== 'todos' || filtros.portador !== 'todos' || filtros.centroCusto !== 'todos' || filtros.dataIni || filtros.dataFim; function clearFiltros() { setFiltros({ empresa: 'todas', periodo: '6m', tipo: 'todos', status: 'todos', portador: 'todos', centroCusto: 'todos', dataIni: '', dataFim: '' }); } // Histórico por competência const seriesEnt = meses.map(m => lancs.filter(l => l.competencia === m.comp && l.tipo === 'entrada').reduce((s, l) => s + l.valor, 0)); const seriesSai = meses.map(m => lancs.filter(l => l.competencia === m.comp && l.tipo === 'saida').reduce((s, l) => s + l.valor, 0)); // Top categorias de gasto const topGastos = useMemo_L(() => { const agg = {}; lancs.filter(l => l.tipo === 'saida').forEach(l => { agg[l.descricao] = (agg[l.descricao] || 0) + l.valor; }); return Object.entries(agg).sort((a, b) => b[1] - a[1]).slice(0, 8).map(([k, v]) => ({ label: k, value: v })); }, [lancs]); // Fluxo de caixa projetado (lançamentos não pagos por competência) const fluxoProjetado = useMemo_L(() => meses.map(m => { const naoPagos = lancs.filter(l => l.competencia === m.comp && !l.pago); const aReceber = naoPagos.filter(l => l.tipo === 'entrada').reduce((s, l) => s + (l.saldoRestante !== undefined ? l.saldoRestante : l.valor), 0); const aPagar = naoPagos.filter(l => l.tipo === 'saida').reduce((s, l) => s + (l.saldoRestante !== undefined ? l.saldoRestante : l.valor), 0); return { label: m.label, aReceber, aPagar, saldo: aReceber - aPagar }; }), [meses, lancs]); const totProjReceber = fluxoProjetado.reduce((s, m) => s + m.aReceber, 0); const totProjPagar = fluxoProjetado.reduce((s, m) => s + m.aPagar, 0); const maxFluxo = Math.max(1, ...fluxoProjetado.map(m => Math.max(m.aReceber, m.aPagar))); // Análise por forma de pagamento const formasAnalise = useMemo_L(() => { const rows = data.formasPagamento.map(f => { const ls = lancs.filter(l => l.formaPagamento === f); const entradas = ls.filter(l => l.tipo === 'entrada').reduce((s, l) => s + l.valor, 0); const saidas = ls.filter(l => l.tipo === 'saida').reduce((s, l) => s + l.valor, 0); return { forma: f, entradas, saidas, total: entradas + saidas }; }).filter(r => r.total > 0).sort((a, b) => b.total - a.total); const totalGeral = rows.reduce((s, r) => s + r.total, 0); return { rows, totalGeral }; }, [lancs, data.formasPagamento]); // Inadimplência: lançamentos vencidos (não pagos) const vencidos = useMemo_L(() => lancs .filter(l => !l.pago && l.statusPg !== 'pago' && lancStatus(l, hoje) === 'vencido') .map(l => ({ ...l, diasAtraso: daysBetween(l.vencimento, hoje) })) .sort((a, b) => b.diasAtraso - a.diasAtraso), [lancs, hoje]); const totalVencido = vencidos.reduce((s, l) => s + (l.saldoRestante !== undefined ? l.saldoRestante : l.valor), 0); const empresaMaisInadimplente = useMemo_L(() => { const agg = {}; vencidos.forEach(l => { agg[l.empresaNome] = (agg[l.empresaNome] || 0) + (l.saldoRestante !== undefined ? l.saldoRestante : l.valor); }); const top = Object.entries(agg).sort((a, b) => b[1] - a[1])[0]; return top ? { nome: top[0], valor: top[1] } : null; }, [vencidos]); const totalInad = vencidos.length; const totalPaginasInad = Math.ceil(totalInad / POR_PAGINA_INAD); const vencidosPaginados = vencidos.slice((paginaInad - 1) * POR_PAGINA_INAD, paginaInad * POR_PAGINA_INAD); // Ranking de empresas (sobre lançamentos filtrados) const ranking = useMemo_L(() => { const arr = empresasFiltradas.map(e => { const st = empresaStats(e, lancsPorEmpresa[e.id] || [], hoje); return { ...e, st }; }); const keys = { saldo: (x) => x.st.saldo, receita: (x) => x.st.recebido, despesa: (x) => x.st.pago, inadimplente: (x) => x.st.vencidos, }; const k = keys[rankSort] || keys.saldo; return [...arr].sort((a, b) => k(b) - k(a)); }, [empresasFiltradas, lancsPorEmpresa, hoje, rankSort]); // Tabela cruzada (sobre lançamentos filtrados) const tabelaCruzada = useMemo_L(() => { const header = empresasFiltradas.map(e => e.nome); if (cruzamento === 'portador-empresa') { const rows = data.portadores.map(p => { const cells = empresasFiltradas.map(e => (lancsPorEmpresa[e.id] || []).filter(l => l.portadorId === p.id && l.pago).reduce((s, l) => s + (l.tipo === 'entrada' ? l.valor : -l.valor), 0)); return { id: p.id, label: p.nome, cells, total: cells.reduce((s, v) => s + v, 0) }; }); return { header, rows, label: 'Portador × Empresa (saldo realizado)' }; } if (cruzamento === 'centro-empresa') { const rows = data.centrosCusto.map(c => { const cells = empresasFiltradas.map(e => (lancsPorEmpresa[e.id] || []).filter(l => l.centroCustoId === c.id).reduce((s, l) => s + l.valor, 0)); return { id: c.id, label: c.nome, sub: c.tipo, cells, total: cells.reduce((s, v) => s + v, 0) }; }); return { header, rows, label: 'Centro de Custo × Empresa (volume total)' }; } const rows = data.formasPagamento.map(f => { const cells = empresasFiltradas.map(e => (lancsPorEmpresa[e.id] || []).filter(l => l.formaPagamento === f).reduce((s, l) => s + l.valor, 0)); return { id: f, label: f, cells, total: cells.reduce((s, v) => s + v, 0) }; }); return { header, rows, label: 'Forma de Pagamento × Empresa' }; }, [cruzamento, empresasFiltradas, lancsPorEmpresa, data.portadores, data.centrosCusto, data.formasPagamento]); const rankCols = [ { v: 'receita', label: 'Maior receita' }, { v: 'despesa', label: 'Maior despesa' }, { v: 'saldo', label: 'Maior saldo' }, { v: 'inadimplente', label: 'Mais inadimplente' }, ]; const abasRelatorio = [ { id: 'historico', label: 'Histórico Financeiro' }, { id: 'fluxo', label: 'Fluxo de Caixa' }, { id: 'formas', label: 'Formas de Pagamento' }, { id: 'inadimplencia', label: 'Inadimplência' }, { id: 'ranking', label: 'Ranking de Empresas' }, { id: 'cruzadas', label: 'Análises Cruzadas' }, ]; // Exporta apenas os dados da aba ativa, com nome de arquivo correspondente function exportAbaRelatorio() { const wb = XLSX.utils.book_new(); let aoa, nome, sheetName; if (abaRelatorio === 'historico') { const totE = seriesEnt.reduce((s, v) => s + v, 0), totS = seriesSai.reduce((s, v) => s + v, 0); aoa = [['Competência', 'Entradas', 'Saídas', 'Resultado']]; meses.forEach((m, i) => aoa.push([m.comp, seriesEnt[i], seriesSai[i], seriesEnt[i] - seriesSai[i]])); aoa.push(['TOTAL', totE, totS, totE - totS]); nome = 'Historico_Financeiro.xlsx'; sheetName = 'Histórico'; } else if (abaRelatorio === 'fluxo') { aoa = [['Mês', 'Entradas previstas', 'Saídas previstas', 'Saldo projetado']]; fluxoProjetado.forEach(m => aoa.push([m.label, m.aReceber, m.aPagar, m.saldo])); aoa.push(['TOTAL', totProjReceber, totProjPagar, totProjReceber - totProjPagar]); nome = 'Fluxo_de_Caixa.xlsx'; sheetName = 'Fluxo Projetado'; } else if (abaRelatorio === 'formas') { aoa = [['Forma', 'Entradas', 'Saídas', 'Total', '% do total']]; formasAnalise.rows.forEach(r => aoa.push([r.forma, r.entradas, r.saidas, r.total, formasAnalise.totalGeral > 0 ? +(r.total / formasAnalise.totalGeral * 100).toFixed(1) : 0])); nome = 'Formas_de_Pagamento.xlsx'; sheetName = 'Formas Pgto'; } else if (abaRelatorio === 'inadimplencia') { aoa = [['Empresa', 'Descrição', 'Vencimento', 'Dias em atraso', 'Valor', 'Portador']]; vencidos.forEach(l => aoa.push([l.empresaNome, l.descricao, formatDate(l.vencimento), l.diasAtraso, l.valor, portMap[l.portadorId]?.nome || ''])); nome = 'Inadimplencia.xlsx'; sheetName = 'Inadimplência'; } else if (abaRelatorio === 'ranking') { aoa = [['Posição', 'Empresa', 'Segmento', 'Recebido', 'Pago', 'Saldo', 'Vencidos', 'Status']]; ranking.forEach((e, i) => aoa.push([i + 1, e.nome, e.segmento, e.st.recebido, e.st.pago, e.st.saldo, e.st.vencidos, (statusColor(e.st.statusEmpresa) || {}).label || e.st.statusEmpresa])); nome = 'Ranking_de_Empresas.xlsx'; sheetName = 'Ranking'; } else { aoa = [['Item', ...tabelaCruzada.header, 'TOTAL']]; tabelaCruzada.rows.forEach(r => aoa.push([r.label, ...r.cells, r.total])); nome = 'Analises_Cruzadas.xlsx'; sheetName = 'Cruzada'; } XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(aoa), sheetName.slice(0, 31)); XLSX.writeFile(wb, nome); toast.push(nome + ' gerado'); } return (
Relatórios Consolidados

Análises & Indicadores

Fluxo projetado, formas de pagamento, inadimplência, ranking e cruzamentos
{ const el = document.getElementById('relatorios-content'); if (el) imprimirPDF(el.innerHTML, 'Relatório BPO'); }}> Exportar PDF Exportar XLSX
{/* Filtros globais */}
setFiltros({ ...filtros, empresa: e.target.value })} options={[ { value: "todas", label: "Todas" }, ...data.empresas.map(e => ({ value: e.id, label: e.nome })) ]} /> setFiltros({ ...filtros, periodo: e.target.value })} options={[ { value: "1m", label: "1 mês" }, { value: "3m", label: "3 meses" }, { value: "6m", label: "6 meses" }, { value: "12m", label: "12 meses" }, { value: "custom", label: "Personalizado" } ]} /> setFiltros({ ...filtros, tipo: e.target.value })} options={[ { value: "todos", label: "Todos" }, { value: "entrada", label: "Entradas" }, { value: "saida", label: "Saídas" } ]} /> setFiltros({ ...filtros, status: e.target.value })} options={[ { value: "todos", label: "Todos" }, { value: "pago", label: "Pago" }, { value: "em-dia", label: "Em dia" }, { value: "vencendo", label: "Vencendo" }, { value: "vencido", label: "Vencido" } ]} /> setFiltros({ ...filtros, portador: e.target.value })} options={[ { value: "todos", label: "Todos" }, ...data.portadores.map(p => ({ value: p.id, label: p.nome })) ]} /> setFiltros({ ...filtros, centroCusto: e.target.value })} options={[ { value: "todos", label: "Todos" }, ...data.centrosCusto.map(c => ({ value: c.id, label: c.nome })) ]} /> {hasFilter && Limpar}
{filtros.periodo === 'custom' && (
setFiltros({ ...filtros, dataIni: e.target.value })} /> setFiltros({ ...filtros, dataFim: e.target.value })} />
)}
{/* Abas de navegação por seção */}
{abasRelatorio.map(a => ( ))}
{/* Histórico */} {abaRelatorio === 'historico' && (
Histórico Financeiro Consolidado
Entradas vs saídas por competência · {meses.length} {meses.length === 1 ? 'mês' : 'meses'}
s + v, 0)) }, { color: '#dc2626', label: 'Saídas', value: formatBRL(seriesSai.reduce((s, v) => s + v, 0)) }]} />
m.label)} height={240} />
)} {/* Fluxo de Caixa Projetado */} {abaRelatorio === 'fluxo' && (
Fluxo de Caixa Projetado
Previsto a partir de lançamentos ainda não pagos
{/* Mini gráfico de barras por mês */}
{fluxoProjetado.map((m, i) => (
0 ? 2 : 0 }} />
0 ? 2 : 0 }} />
{m.label}
))}
{fluxoProjetado.map((m, i) => ( ))}
Mês Entradas previstas Saídas previstas Saldo projetado
{m.label} {formatBRL(m.aReceber)} {formatBRL(m.aPagar)} = 0 ? '#16a34a' : '#dc2626' }}>{formatBRL(m.saldo)}
TOTAL {formatBRL(totProjReceber)} {formatBRL(totProjPagar)} = 0 ? '#16a34a' : '#dc2626' }}>{formatBRL(totProjReceber - totProjPagar)}
{fluxoProjetado.every(m => m.aReceber === 0 && m.aPagar === 0) && (
Nenhum lançamento previsto no período.
)} )} {/* Análise por Forma de Pagamento */} {abaRelatorio === 'formas' && (
Análise por Forma de Pagamento
Volume total movimentado por meio de pagamento
{formasAnalise.rows.length === 0 ? (
Sem movimentações no período.
) : ( <> {/* Barras horizontais */}
{formasAnalise.rows.map((r, i) => { const pct = (r.total / formasAnalise.rows[0].total) * 100; return (
{r.forma} {formatBRL(r.total)}
); })}
{formasAnalise.rows.map(r => ( ))}
Forma Entradas Saídas Total % do total
{r.forma} {formatBRL(r.entradas)} {formatBRL(r.saidas)} {formatBRL(r.total)} {formasAnalise.totalGeral > 0 ? ((r.total / formasAnalise.totalGeral) * 100).toFixed(1) : '0.0'}%
)} )} {/* Inadimplência e Vencimentos */} {abaRelatorio === 'inadimplencia' && (
Inadimplência e Vencimentos
Títulos vencidos e não pagos no período
{vencidosPaginados.map(l => ( ))}
Empresa Descrição Vencimento Dias em atraso Valor Portador
{l.empresaNome} {l.descricao} {formatDate(l.vencimento)} {l.diasAtraso} dias {formatBRL(l.valor)} {portMap[l.portadorId]?.nome}
{totalInad === 0 && } {totalPaginasInad > 1 && (
Mostrando {((paginaInad-1)*POR_PAGINA_INAD)+1}–{Math.min(paginaInad*POR_PAGINA_INAD, totalInad)} de {totalInad} títulos
setPaginaInad(p => p - 1)}> ← Anterior {Array.from({ length: Math.min(5, totalPaginasInad) }, (_, i) => { let p; if (totalPaginasInad <= 5) p = i + 1; else if (paginaInad <= 3) p = i + 1; else if (paginaInad >= totalPaginasInad - 2) p = totalPaginasInad - 4 + i; else p = paginaInad - 2 + i; return ( ); })} setPaginaInad(p => p + 1)}> Próxima →
)}
)} {/* Ranking de Empresas */} {abaRelatorio === 'ranking' && (
Ranking de Empresas
Ordenado por: {rankCols.find(c => c.v === rankSort)?.label}
{rankCols.map(o => ( ))}
{ranking.map((e, i) => ( ))}
# Empresa Segmento Recebido Pago Saldo Vencidos Status
{i + 1}º {e.nome} {e.segmento} {formatBRL(e.st.recebido)} {formatBRL(e.st.pago)} = 0 ? '#16a34a' : '#dc2626' }}>{formatBRL(e.st.saldo)} 0 ? '#dc2626' : 'var(--c-text-muted)', fontWeight: e.st.vencidos > 0 ? 600 : 400, fontVariantNumeric: 'tabular-nums' }}>{formatBRL(e.st.vencidos)}
{ranking.length === 0 && }
)} {/* Análises Cruzadas: Top categorias + Resumo + Tabela cruzada */} {abaRelatorio === 'cruzadas' && ( <> {/* Top gastos + Resumo por empresa */}
Top 8 Categorias de Gasto
Onde mais saiu dinheiro (filtros aplicados)
{topGastos.length === 0 ? (
Sem saídas no período.
) : (
{topGastos.map((g, i) => { const max = topGastos[0].value; const pct = (g.value / max) * 100; return (
{g.label} {formatBRL(g.value)}
); })}
)}
Resumo por Empresa
Saldo realizado por cliente
{empresasFiltradas.map(e => { const stats = empresaStats(e, lancsPorEmpresa[e.id] || [], hoje); return (
{e.nome.charAt(0)}
{e.nome}
= 0 ? '#16a34a' : '#dc2626' }}>{formatBRL(stats.saldo)}
); })}
{/* Tabela cruzada */}
Tabela Cruzada — {tabelaCruzada.label}
Selecione o cruzamento para análise
{[ { v: 'portador-empresa', label: 'Portador × Empresa' }, { v: 'centro-empresa', label: 'Centro × Empresa' }, { v: 'forma-empresa', label: 'Forma × Empresa' }, ].map(o => ( ))}
{tabelaCruzada.header.map((h, i) => ( ))} {tabelaCruzada.rows.map(r => ( {r.cells.map((v, i) => ( ))} ))}
Item{h}TOTAL
{r.label} {r.sub &&
{r.sub}
}
{v === 0 ? '—' : formatBRL(v).replace('R$', '').trim()} {formatBRL(r.total).replace('R$', '').trim()}
)}
); } Object.assign(window, { LancamentosGlobais, RelatoriosConsolidados });