// Relatório — Budget da família (orçamento mensal) — imprimível
function RelatorioBudgetPage({ 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 fmt = window.fmtBRLString;
const fam = state.familia;
const mesLabel = window.MESES_FULL[month] + "/" + year;
const mt = window.monthTotals(state.transactions, state.aggregates, year, month);
const receita = mt.income, despesa = mt.expense, saldo = receita - despesa;
const txPoupanca = receita > 0 ? (saldo / receita) * 100 : 0;
const prevM = month === 0 ? { m: 11, y: year - 1 } : { m: month - 1, y: year };
const prev = window.monthTotals(state.transactions, state.aggregates, prevM.y, prevM.m);
const prevLabel = window.MESES_FULL[prevM.m] + "/" + prevM.y;
const plano = window.computePlano(state.transactions, state.aggregates, year, month);
const monthTxs = window.txByMonth(state.transactions, year, month);
const classes = window.byClasse(monthTxs);
const totalClasses = classes.reduce((s, c) => s + c.value, 0);
// Despesas por categoria: lançamentos detalhados primeiro, senão agregados
let cats = window.byCategory(monthTxs);
if (!cats.length) cats = window.aggCategoryBreakdown(state.aggregates, year, month);
const totalCats = cats.reduce((s, c) => s + c.value, 0);
// Receitas por fonte
const recMap = {};
for (const t of monthTxs) {
if (t.type !== "Receita") continue;
const k = t.category || t.description || "Receita";
recMap[k] = (recMap[k] || 0) + Number(t.amount || 0);
}
for (const [nome, arr] of Object.entries(state.aggregates?.[year]?.receitas || {})) {
const v = Number((arr || [])[month] || 0);
if (v > 0) recMap[nome] = (recMap[nome] || 0) + v;
}
const receitas = Object.entries(recMap).map(([nome, valor]) => ({ nome, valor })).sort((a, b) => b.valor - a.valor);
const custo = window.custoFixoDetalhe(state.transactions, 3, (state.reserva || {}).base || "todos");
const membros = [fam.cliente.nome, fam.conjuge?.nome].filter(Boolean).concat((fam.filhos || []).map(f => f.nome)).filter(Boolean);
function Sec({ title, explain, children }) {
return (
{explain}{title}
Orçamento mensal · {mesLabel}
Gerado com os dados de {atualizadoEm.toLocaleDateString("pt-BR")} às {atualizadoEm.toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" })} — reflete tudo que foi lançado no app.
| Fonte | Valor | % da receita |
|---|---|---|
| {r.nome} | {fmt(r.valor)} | {receita > 0 ? Math.round((r.valor / receita) * 100) : 0}% |
| Total | {fmt(receita)} | 100% |
Nenhuma receita lançada em {mesLabel}.
}| Classe | Valor | % do total |
|---|---|---|
| {c.icon} {c.label} | {fmt(c.value)} | {Math.round((c.value / totalClasses) * 100)}% |
| Total | {fmt(totalClasses)} | 100% |
Nenhum lançamento classificado em {mesLabel}.
}| Categoria | Valor | % das despesas |
|---|---|---|
| {c.category} | {fmt(c.value)} | {totalCats > 0 ? Math.round((c.value / totalCats) * 100) : 0}% |
| Total | {fmt(totalCats)} | 100% |
Nenhuma despesa lançada em {mesLabel}.
}Este estudo é dirigido exclusivamente ao destinatário indicado na capa e pode conter informações confidenciais. Tem natureza informativa e orientadora, e não deve ser interpretado como aconselhamento jurídico ou financeiro.