// Relatório — Diagnóstico completo (orçamento, patrimônio, proteção, sucessão) — imprimível
// ===== Diagnóstico do consultor: inconsistências e insights gerados dos dados =====
function gerarDiagnostico(state, year, month) {
const out = [];
const add = (nivel, titulo, texto) => out.push({ nivel, titulo, texto });
const fmt = window.fmtBRLString;
const hoje = new Date().toISOString().slice(0, 10);
const plano = window.computePlano(state.transactions, state.aggregates, year, month);
const tot = window.patrimonioTotais(state.patrimonio, state.dividas);
const custo = window.custoFixoDetalhe(state.transactions, 3, (state.reserva || {}).base || "todos");
const mesLabel = window.MESES_FULL[month] + "/" + year;
// --- Orçamento / plano 70·10·10·10 ---
if (plano.income === 0) {
add("atencao", "Sem receita lançada em " + mesLabel, "O plano 70·10·10·10 só funciona com a receita do mês registrada. Lance as entradas no Checklist para ativar os tetos e metas.");
} else {
const g = plano.gastos;
if (g.over) {
add("alerta", "Gastos " + fmt(g.overBy) + " acima do teto de 70%",
"Em " + mesLabel + " os gastos comprometidos somam " + fmt(g.used) + " para um teto de " + fmt(g.target) + ". Há " + fmt(g.redutivelPendente) + " em despesas redutíveis ainda não pagas — é onde o corte não afeta o essencial.");
} else {
add("ok", "Gastos dentro do teto de 70%", "Folga de " + fmt(g.target - g.used) + " em " + mesLabel + ". Manter os gastos abaixo do teto é o que libera os 30% para caridade, projetos e investimentos.");
}
for (const b of plano.buckets) {
if (b.key === "gastos") continue;
if (b.used + 0.5 < b.target) {
add("atencao", b.icon + " " + b.label + " abaixo da meta de 10%",
"Lançado " + fmt(b.used) + " de " + fmt(b.target) + ". Faltam " + fmt(b.target - b.used) + " para cumprir o balde em " + mesLabel + ".");
}
}
const sobra = plano.income - plano.gastos.used - plano.buckets.filter(b => b.key !== "gastos").reduce((s, b) => s + b.used, 0);
if (sobra < 0) add("alerta", "Mês fechando no vermelho", "As saídas de " + mesLabel + " superam a receita em " + fmt(-sobra) + ". Priorize os cortes em redutíveis antes de comprometer a reserva.");
}
// --- Reserva de emergência ---
const saldosReserva = (state.patrimonio.saldos || []).filter(s => s.reserva);
const reservaSaldo = state.reserva && state.reserva.saldoManual != null ? state.reserva.saldoManual : saldosReserva.reduce((s, x) => s + Number(x.valor || 0), 0);
const mesesAlvo = (state.reserva || {}).mesesAlvo || 6;
if (custo.media > 0) {
const meses = reservaSaldo / custo.media;
if (reservaSaldo <= 0) add("alerta", "Sem reserva de emergência", "Com custo mensal de " + fmt(custo.media) + ", o alvo de " + mesesAlvo + " meses é " + fmt(custo.media * mesesAlvo) + ". Marque ativos líquidos como reserva ou comece um aporte mensal dedicado.");
else if (meses < mesesAlvo) add("atencao", "Reserva cobre " + meses.toFixed(1) + " de " + mesesAlvo + " meses", "Faltam " + fmt(custo.media * mesesAlvo - reservaSaldo) + " para o colchão completo. Reserva vem antes de investimentos de maior risco.");
else add("ok", "Reserva de emergência completa", meses.toFixed(1) + " meses de custo cobertos (" + fmt(reservaSaldo) + "). Excedentes podem migrar para objetivos e aposentadoria.");
}
// --- Dívidas ---
const dividas = state.dividas || [];
const vencidas = dividas.filter(d => d.status !== "Finalizado" && d.vencimento && d.vencimento < hoje);
if (vencidas.length) add("alerta", vencidas.length + " dívida(s) com vencimento estourado", vencidas.map(d => d.descricao + " (venceu " + d.vencimento.split("-").reverse().join("/") + ")").join(" · ") + ". Renegociar evita juros e restrições.");
const semNeg = dividas.filter(d => d.status === "Sem Negociação");
if (semNeg.length) add("atencao", "Dívida sem negociação", semNeg.map(d => d.descricao + " — " + fmt(d.saldoDevedor)).join(" · ") + ". Sem acordo formal, o passivo tende a crescer e a travar o planejamento.");
if (tot.totalDividas > 0 && tot.total > 0) {
const pctDiv = (tot.totalDividas / tot.total) * 100;
add(pctDiv > 30 ? "atencao" : "insight", "Dívidas equivalem a " + pctDiv.toFixed(0) + "% do patrimônio",
fmt(tot.totalDividas) + " devidos contra " + fmt(tot.total) + " de patrimônio bruto — patrimônio líquido de " + fmt(tot.liquido) + ".");
}
// --- Proteção ---
const seguros = state.seguros || [];
const vidas = seguros.filter(s => s.tipo === "Vida");
const temFilhos = (state.familia.filhos || []).length > 0;
if (!vidas.length && temFilhos) add("alerta", "Família com filhos e sem seguro de vida", "Uma referência didática: capital segurado de 3 a 5 anos do custo da família" + (custo.media > 0 ? " (entre " + fmt(custo.media * 36) + " e " + fmt(custo.media * 60) + ")" : "") + " protege a transição em caso de perda do provedor.");
else if (vidas.length && custo.media > 0) {
const capital = vidas.reduce((s, v) => s + Number(v.capital || 0), 0);
const anos = capital / (custo.media * 12);
if (anos < 3) add("atencao", "Capital de vida cobre " + anos.toFixed(1) + " ano(s) de custo", "Capital segurado de " + fmt(capital) + " ante um custo anual de " + fmt(custo.media * 12) + ". Referência usual: 3 a 5 anos.");
else add("ok", "Seguro de vida em patamar adequado", "Capital de " + fmt(capital) + " cobre " + anos.toFixed(1) + " anos do custo da família.");
}
const segVencidos = seguros.filter(s => s.vigencia && s.vigencia < hoje);
if (segVencidos.length) add("alerta", "Apólice(s) com vigência vencida", segVencidos.map(s => s.nome + " (até " + s.vigencia.split("-").reverse().join("/") + ")").join(" · ") + ". Confirme a renovação para não ficar descoberto.");
const naoSegurados = [...(state.patrimonio.imoveis || []), ...(state.patrimonio.veiculos || [])].filter(a => !a.segurado);
if (naoSegurados.length) add("atencao", "Bens sem seguro", naoSegurados.map(a => a.nome + " (" + window.fmtCompact(a.valor) + ")").join(" · ") + ". Avalie o custo do seguro contra o impacto de uma perda total.");
// --- Aposentadoria ---
const apos = window.calcAposentadoria(state.aposentadoria, tot.financeiro);
if ((state.aposentadoria.rendaMensalDesejada || 0) > 0) {
const aporteAtual = Number(state.aposentadoria.aporteMensal || 0);
if (apos.aporteNecessario > aporteAtual + 1) add("atencao", "Aporte de aposentadoria abaixo do necessário",
"Para " + fmt(state.aposentadoria.rendaMensalDesejada) + "/mês aos " + state.aposentadoria.idadeAlvo + " anos, o aporte estimado é " + fmt(apos.aporteNecessario) + "/mês — hoje são " + fmt(aporteAtual) + ".");
else add("ok", "Plano de aposentadoria no ritmo", "Aporte atual de " + fmt(aporteAtual) + "/mês projeta " + window.fmtCompact(apos.projecao) + " aos " + state.aposentadoria.idadeAlvo + " anos, para um alvo de " + window.fmtCompact(apos.alvo) + ".");
}
// --- Sucessão / liquidez ---
const suc = window.calcSucessao(state);
const capitalVidaSuc = seguros.filter(s => s.tipo === "Vida" || s.tipo === "Invalidez").reduce((s, v) => s + Number(v.capital || 0), 0);
if (suc.custoExtrajudicial > 0 && tot.total > 0) {
const cob = capitalVidaSuc / suc.custoExtrajudicial;
if (cob >= 1) add("ok", "Custo do inventário coberto pelo seguro", "O capital segurado de " + fmt(capitalVidaSuc) + " cobre integralmente o custo estimado do inventário extrajudicial (" + fmt(suc.custoExtrajudicial) + ").");
else add("alerta", "Seguro cobre " + Math.round(cob * 100) + "% do custo do inventário", "O inventário extrajudicial custa ~" + fmt(suc.custoExtrajudicial) + " e o capital segurado é " + fmt(capitalVidaSuc) + ". Sugestão: aumentar a proteção em " + fmt(suc.custoExtrajudicial - capitalVidaSuc) + " para dar liquidez imediata aos herdeiros (na via judicial o custo sobe para " + fmt(suc.custoJudicial) + ").");
}
if (suc.custoExtrajudicial > tot.financeiro + capitalVidaSuc && tot.total > 0) add("atencao", "Custo sucessório maior que a liquidez disponível",
"Somando patrimônio financeiro (" + fmt(tot.financeiro) + ") e seguro (" + fmt(capitalVidaSuc) + "), ainda falta caixa para os ~" + fmt(suc.custoExtrajudicial) + " do inventário. Sem liquidez, herdeiros podem ter que vender bens às pressas.");
if (tot.previdencia === 0 && tot.total > 0) add("insight", "Nenhum recurso livre de inventário", "Previdência (PGBL/VGBL) não passa por inventário e dá liquidez imediata aos beneficiários — instrumento útil no planejamento sucessório.");
if (tot.total > 0 && tot.naoFinanceiro / tot.total > 0.8) add("insight", "Patrimônio concentrado em bens ilíquidos", ((tot.naoFinanceiro / tot.total) * 100).toFixed(0) + "% do patrimônio está em imóveis, veículos e participações. Diversificar em ativos financeiros dá flexibilidade para emergências e oportunidades.");
// --- Objetivos ---
for (const o of state.objetivos || []) {
if (o.prazo && o.prazo < hoje && Number(o.acumulado || 0) < Number(o.alvo || 0)) {
add("atencao", "Objetivo \"" + o.nome + "\" com prazo vencido", "Acumulado " + fmt(o.acumulado) + " de " + fmt(o.alvo) + " até " + o.prazo.split("-").reverse().join("/") + ". Redefina o prazo ou o aporte mensal.");
}
}
return out;
}
const DIAG_NIVEIS = {
alerta: { icon: "🚨", label: "Alerta" },
atencao: { icon: "⚠️", label: "Atenção" },
ok: { icon: "✅", label: "Em ordem" },
insight: { icon: "💡", label: "Insight" },
};
function RelatorioPage({ state, year, month = 0 }) {
const [atualizadoEm, setAtualizadoEm] = React.useState(() => new Date());
const [flash, setFlash] = React.useState(false);
function atualizar() {
setAtualizadoEm(new Date());
setFlash(true);
setTimeout(() => setFlash(false), 1200);
}
const p = state.patrimonio;
const fam = state.familia;
const tot = window.patrimonioTotais(p, state.dividas);
const suc = window.calcSucessao(state);
const saldosClasse = window.saldosPorClasse(p.saldos);
const fmt = window.fmtBRLString;
const mesLabel = window.MESES_FULL[month] + "/" + year;
const plano = window.computePlano(state.transactions, state.aggregates, year, month);
const classesMes = window.byClasse(window.txByMonth(state.transactions, year, month));
const totalClasses = classesMes.reduce((s, c) => s + c.value, 0);
const custo = window.custoFixoDetalhe(state.transactions, 3, (state.reserva || {}).base || "todos");
const saldosReserva = (p.saldos || []).filter(s => s.reserva);
const reservaSaldo = state.reserva && state.reserva.saldoManual != null ? state.reserva.saldoManual : saldosReserva.reduce((s, x) => s + Number(x.valor || 0), 0);
const mesesAlvo = (state.reserva || {}).mesesAlvo || 6;
const apos = window.calcAposentadoria(state.aposentadoria, tot.financeiro);
const diag = gerarDiagnostico(state, year, month);
const DIAG_ORDEM = { alerta: 0, atencao: 1, insight: 2, ok: 3 };
const diagOrdenado = [...diag].sort((a, b) => DIAG_ORDEM[a.nivel] - DIAG_ORDEM[b.nivel]);
const diagCriticos = diagOrdenado.filter(d => d.nivel === "alerta");
const diagResto = diagOrdenado.filter(d => d.nivel !== "alerta");
const catItems = [
{ label: "Imóveis", valor: tot.imoveis, color: "#311131" },
{ label: "Veículos", valor: tot.veiculos, color: "#0663AA" },
{ label: "Participações societárias", valor: tot.participacoes, color: "#7fb8e0" },
{ label: "Saldos e Investimentos", valor: tot.financeiro, color: "#EA404D" },
{ label: "Outros bens", valor: tot.outros, color: "#bcdcf3" },
];
const maxCat = Math.max(...catItems.map(c => c.valor), 1);
function Sec({ title, scope, explain, children }) {
return (
{explain}{title}
{scope && {scope}}
Diagnóstico Financeiro, Patrimonial e Familiar · {mesLabel}
Gerado com os dados de {atualizadoEm.toLocaleDateString("pt-BR")} às {atualizadoEm.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} — o relatório reflete automaticamente tudo que foi lançado no app.
{d.texto}
Sem dados suficientes para o diagnóstico. Lance receitas e gastos no Checklist.
}| Gastos por classe | Valor | % do total |
|---|---|---|
| {c.icon} {c.label} | {fmt(c.value)} | {Math.round((c.value / totalClasses) * 100)}% |
| Saldos e Investimentos | Valor |
|---|---|
| {s.classe} | {fmt(s.valor)} |
| Descrição | Credor | Saldo devedor | Vencimento | Status |
|---|---|---|---|---|
| {d.descricao} | {d.credor || "—"} | {fmt(d.saldoDevedor)} | {d.vencimento ? d.vencimento.split("-").reverse().join("/") : "—"} | {d.status} |
Nenhuma dívida cadastrada.
}| Tipo | Apólice | Seguradora | Capital | Prêmio/mês | Vigência |
|---|---|---|---|---|---|
| {s.tipo} | {s.nome} | {s.seguradora || "—"} | {fmt(s.capital)} | {fmt(s.premioMensal)} | {s.vigencia ? s.vigencia.split("-").reverse().join("/") : "—"} |
Nenhum seguro cadastrado.
}| Custo mensal por classe (média) | Valor |
|---|---|
| {c.icon} {c.label} | {fmt(c.valor)} |
| Total | {fmt(custo.media)} |
| Ativos que compõem a reserva | Classe | Instituição | Valor |
|---|---|---|---|
| {s.nome} | {s.classe} | {s.instituicao || "—"} | {fmt(window.valorBRL(s))} |
Nenhum ativo marcado como reserva de emergência.
}| Objetivo | Acumulado | Alvo | Progresso | Prazo |
|---|---|---|---|---|
| {o.icone} {o.nome} | {fmt(o.acumulado)} | {fmt(o.alvo)} | {o.alvo > 0 ? Math.round((o.acumulado / o.alvo) * 100) : 0}% | {o.prazo ? o.prazo.split("-").reverse().join("/") : "—"} |
Nenhum objetivo cadastrado.
}| Entidade | CNPJ | % | Capital social | Valor patrimonial |
|---|---|---|---|---|
| {e.nome} | {e.cnpj || "—"} | {e.pct}% | {fmt(e.capitalSocial)} | {fmt(e.valorPatrimonial)} |
Nenhuma participação societária.
}*O valor do honorário é sugerido conforme a recomendação da tabela OAB de cada estado.
*Partilha simulada para o falecimento do cliente, conforme o regime de bens ({suc.regime}) — CC arts. 1.829 e 1.832. Estimativa didática; não substitui aconselhamento jurídico.
Este estudo é dirigido exclusivamente ao destinatário indicado na capa e pode conter informações confidenciais. Seu propósito é fornecer informações para auxiliar o destinatário no planejamento financeiro, patrimonial e sucessório. As informações pessoais são fornecidas pelo destinatário levando em consideração seus dados cadastrais, familiares e patrimoniais, bem como suas necessidades financeiras e objetivos pessoais. O relatório tem natureza informativa e orientadora, e não deve ser interpretado como aconselhamento jurídico ou financeiro. Não nos responsabilizamos por decisões de investimento ou quaisquer decisões pessoais tomadas com base nas informações apresentadas neste relatório.