// 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
>}>
);
}
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" />
| Empresa |
Vencimento |
Descrição |
Tipo |
Centro Custo |
Portador |
Valor |
Status |
Ações |
{paginados.map((l, i) => {
const s = lancStatus(l, hoje);
return (
setNovoLanc(l)}>
| {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'}
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}
))}
| Mês |
Entradas previstas |
Saídas previstas |
Saldo projetado |
{fluxoProjetado.map((m, i) => (
| {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)}
);
})}
| Forma |
Entradas |
Saídas |
Total |
% do total |
{formasAnalise.rows.map(r => (
| {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
| Empresa |
Descrição |
Vencimento |
Dias em atraso |
Valor |
Portador |
{vencidosPaginados.map(l => (
| {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 => (
))}
| # |
Empresa |
Segmento |
Recebido |
Pago |
Saldo |
Vencidos |
Status |
{ranking.map((e, i) => (
| {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 => (
))}
| Item |
{tabelaCruzada.header.map((h, i) => (
{h} |
))}
TOTAL |
{tabelaCruzada.rows.map(r => (
|
{r.label}
{r.sub && {r.sub} }
|
{r.cells.map((v, i) => (
{v === 0 ? '—' : formatBRL(v).replace('R$', '').trim()}
|
))}
{formatBRL(r.total).replace('R$', '').trim()} |
))}
>
)}
);
}
Object.assign(window, { LancamentosGlobais, RelatoriosConsolidados });