// Main App component const { useState: useStateA, useEffect: useEffectA, useMemo: useMemoA } = React; const STORAGE_KEY = "lrfin:state:v5"; const OLD_STORAGE_KEY = "lrfin:state:v4"; // ===== Multi-cliente (múltiplos planejamentos) ===== const CLIENTS_KEY = "lrfin:clients:v1"; const ACTIVE_KEY = "lrfin:activeClient:v1"; const clientStateKey = (id) => `${STORAGE_KEY}:${id}`; function loadState() { try { const raw = localStorage.getItem(STORAGE_KEY); if (raw) return JSON.parse(raw); const old = localStorage.getItem(OLD_STORAGE_KEY); if (old) return JSON.parse(old); // migrado abaixo (slices novas vêm do seed) } catch (e) {} return null; } function loadClientState(id) { try { const raw = localStorage.getItem(clientStateKey(id)); if (raw) return JSON.parse(raw); } catch (e) {} return null; } function saveClientState(id, state) { try { localStorage.setItem(clientStateKey(id), JSON.stringify(state)); } catch (e) {} } function saveClientsRegistry(clients, activeId) { try { localStorage.setItem(CLIENTS_KEY, JSON.stringify(clients)); if (activeId) localStorage.setItem(ACTIVE_KEY, activeId); } catch (e) {} } // Detect if saved state is from old (flat aggregates) schema function isValidState(s) { return s && s.aggregates && typeof s.aggregates === "object" && s.aggregates[2026]; } function App({ authUser, onLogout }) { const seed = window.SEED_DATA; // Novas fatias de estado (diagnóstico patrimonial) — preenchidas do seed se ausentes const CAT_RENAME = { "💃 Larissa": "💃 Esposa", "🐶 Milu": "🐶 Pet" }; function withNewSlices(s) { if (Array.isArray(s.transactions)) { s = { ...s, transactions: s.transactions.map(t => CAT_RENAME[t.category] ? { ...t, category: CAT_RENAME[t.category] } : t) }; } return { ...s, user: s.user || { name: seed.user.name, year: seed.user.year }, planoCheck: s.planoCheck || {}, patrimonio: s.patrimonio || JSON.parse(JSON.stringify(seed.patrimonio)), familia: s.familia || JSON.parse(JSON.stringify(seed.familia)), seguros: s.seguros || JSON.parse(JSON.stringify(seed.seguros)), objetivos: s.objetivos || JSON.parse(JSON.stringify(seed.objetivos)), objetivosPessoais: s.objetivosPessoais || {}, fechamento: s.fechamento || { dia: 5, entregas: {} }, etapas: s.etapas || {}, reserva: s.reserva || { ...seed.reserva }, aposentadoria: s.aposentadoria || { ...seed.aposentadoria }, sucessao: s.sucessao || { ...seed.sucessao }, }; } function buildFreshState(name) { return withNewSlices({ user: { name, year: 2026 }, transactions: window.buildInitialTransactions(seed), aggregates: window.buildInitialAggregates(seed), dividas: JSON.parse(JSON.stringify(seed.dividas)), }); } // Estado 100% vazio (sem dados da planilha) — para preencher do zero function buildBlankState(name) { const emptyAgg = {}; for (const y of [2025, 2026]) emptyAgg[y] = { receitas: {}, despesas: { cards: {}, fixos: {} } }; return { user: { name, year: 2026 }, transactions: [], aggregates: emptyAgg, dividas: [], planoCheck: {}, patrimonio: { imoveis: [], veiculos: [], participacoes: [], saldos: [], outros: [] }, familia: { cliente: { nome: name, nascimento: "", estadoCivil: "", regimeBens: "", email: "" }, conjuge: { nome: "", nascimento: "" }, filhos: [] }, seguros: [], objetivos: [], objetivosPessoais: {}, fechamento: { dia: 5, entregas: {} }, etapas: {}, reserva: { mesesAlvo: 6, saldoManual: null }, aposentadoria: { idadeAtual: 30, idadeAlvo: 60, rendaMensalDesejada: 0, taxaRealAA: 4, aporteMensal: 0 }, sucessao: { ...seed.sucessao }, }; } // Bootstrap: na primeira execução cria "Lucas" (migra estado atual) e duplica em "Mauro". function bootstrapClients() { let clients = null; try { clients = JSON.parse(localStorage.getItem(CLIENTS_KEY)); } catch (e) {} if (Array.isArray(clients) && clients.length) return clients; const existing = loadState(); const lucasState = isValidState(existing) ? withNewSlices({ ...existing, user: { name: "Lucas", year: 2026 } }) : buildFreshState("Lucas"); lucasState.user = { ...(lucasState.user || {}), name: "Lucas" }; const mauroState = JSON.parse(JSON.stringify(lucasState)); mauroState.user = { ...(mauroState.user || {}), name: "Mauro" }; saveClientState("lucas", lucasState); saveClientState("mauro", mauroState); clients = [{ id: "lucas", name: "Lucas" }, { id: "mauro", name: "Mauro" }]; saveClientsRegistry(clients, "lucas"); return clients; } const isAdmin = !!authUser.admin; const [clients, setClients] = useStateA(() => { let cs = bootstrapClients(); // Vincula e-mails às contas e garante que a conta do usuário logado exista cs = cs.map(c => { const u = window.AUTH.users.find(x => x.id === c.id); return u ? { ...c, email: u.email } : c; }); if (!cs.some(c => c.id === authUser.id)) { saveClientState(authUser.id, buildBlankState(authUser.name)); cs = [...cs, { id: authUser.id, name: authUser.name, email: authUser.email }]; } return cs; }); // Isolamento: usuário comum só acessa a própria conta; admin acessa todas const [activeClient, setActiveClient] = useStateA(() => clients.some(c => c.id === authUser.id) ? authUser.id : clients[0].id); const [state, setState] = useStateA(() => { const st = loadClientState(activeClient); const s = st ? withNewSlices(st) : buildFreshState(authUser.name); if (s.familia && s.familia.cliente && !s.familia.cliente.email) { s.familia = { ...s.familia, cliente: { ...s.familia.cliente, email: authUser.email } }; } return s; }); // Default view: open on the month that holds the detailed sample data so the // 70/10/10/10 plan is populated. New entries default to this month too. const [view, setView] = useStateA("overview"); const [orcSub, setOrcSub] = useStateA("lancamentos"); const LEGACY_ORC = ["lancamentos", "receitas", "despesas", "analise", "insights", "dividas", "cartoes"]; // goTo aceita tanto abas novas quanto ids legados de orçamento function goTo(v) { if (v === "objetivos-pessoais") v = "familia"; if (LEGACY_ORC.includes(v)) { setView("orcamento"); setOrcSub(v); } else setView(v); } const [month, setMonth] = useStateA(6); // Julho const [year, setYear] = useStateA(2026); const [period, setPeriod] = useStateA("mes"); const [relRef, setRelRef] = useStateA(null); const [relTipo, setRelTipo] = useStateA("completo"); // "budget" | "completo" const [modal, setModal] = useStateA(null); // {kind: "receita"|"despesa"} | {kind: "divida", edit?: ...} const [fx, setFx] = useStateA(window.FX); // Dólar do dia para converter investimentos offshore useEffectA(() => { let vivo = true; (async () => { const r = await window.buscarDolar(); if (vivo && r) setFx({ ...r }); })(); return () => { vivo = false; }; }, []); useEffectA(() => { saveClientState(activeClient, state); }, [state, activeClient]); useEffectA(() => { saveClientsRegistry(clients, activeClient); }, [clients, activeClient]); const currentUser = state.user || seed.user; // ---- Admin: trocar, criar, renomear, excluir contas ---- function switchClient(id) { if (id === activeClient) return; saveClientState(activeClient, state); const st = loadClientState(id); setActiveClient(id); setState(st ? withNewSlices(st) : buildFreshState("Cliente")); setView("overview"); } function createClient(name, copyFromId) { const nm = (name || "").trim() || "Novo cliente"; const id = "c" + window.uid(); const base = copyFromId ? JSON.parse(JSON.stringify(loadClientState(copyFromId) || state)) : buildBlankState(nm); base.user = { ...(base.user || {}), name: nm }; saveClientState(id, base); saveClientState(activeClient, state); setClients(cs => [...cs, { id, name: nm }]); setActiveClient(id); setState(withNewSlices(base)); setView("overview"); } function renameClient(id, name) { const nm = (name || "").trim(); if (!nm) return; setClients(cs => cs.map(c => c.id === id ? { ...c, name: nm } : c)); if (id === activeClient) setState(s => ({ ...s, user: { ...(s.user || {}), name: nm } })); else { const st = loadClientState(id); if (st) { st.user = { ...(st.user || {}), name: nm }; saveClientState(id, st); } } } function deleteClient(id) { if (clients.length <= 1) return; try { localStorage.removeItem(clientStateKey(id)); } catch (e) {} const remaining = clients.filter(c => c.id !== id); setClients(remaining); if (id === activeClient) { const next = remaining[0].id; const st = loadClientState(next); setActiveClient(next); setState(st ? withNewSlices(st) : buildFreshState("Cliente")); setView("overview"); } } // Compute view data const monthTxs = useMemoA(() => window.txByMonth(state.transactions, year, month), [state.transactions, year, month]); const yearTxs = useMemoA(() => state.transactions.filter(t => t.date.startsWith(String(year))), [state.transactions, year]); const activeTxs = period === "ano" ? yearTxs : monthTxs; // Income/expense — detailed-first (avoids double-counting detailed vs aggregates) const mt = window.monthTotals(state.transactions, state.aggregates, year, month); const monthIncome = mt.income; const monthExpense = mt.expense; const monthSavings = monthIncome - monthExpense; // Previous month for deltas const prevMonth = month === 0 ? { m: 11, y: year - 1 } : { m: month - 1, y: year }; const prevTotals = window.monthTotals(state.transactions, state.aggregates, prevMonth.y, prevMonth.m); const prevIncome = prevTotals.income; const prevExpense = prevTotals.expense; const prevSavings = prevIncome - prevExpense; const deltaIncome = prevIncome ? ((monthIncome - prevIncome) / prevIncome) * 100 : 0; const deltaExpense = prevExpense ? ((monthExpense - prevExpense) / prevExpense) * 100 : 0; const deltaSavings = prevSavings ? ((monthSavings - prevSavings) / Math.abs(prevSavings)) * 100 : 0; // Total balance: cumulative income - cumulative expense up to selected month const cutoffPrefix = `${year}-${String(month + 1).padStart(2, "0")}`; const cumulative = useMemoA(() => { const upTo = state.transactions.filter(t => t.date.slice(0,7) <= cutoffPrefix); let total = window.sumIncome(upTo) - window.sumExpense(upTo); // Add aggregates across all years up to selected month const years = Object.keys(state.aggregates || {}).map(Number).sort(); for (const y of years) { if (y > year) continue; const maxM = y === year ? month : 11; for (let m = 0; m <= maxM; m++) { total += window.aggIncomeForMonth(state.aggregates, y, m) - window.aggExpenseForMonth(state.aggregates, y, m); } } return total; }, [state.transactions, state.aggregates, cutoffPrefix, year, month]); // Saldo Total: include a notional starting balance so it looks like a wallet balance const STARTING_BALANCE = 0; const saldoTotal = STARTING_BALANCE + cumulative; const saldoPrev = STARTING_BALANCE + cumulative - monthSavings; const deltaSaldo = saldoPrev !== 0 ? ((saldoTotal - saldoPrev) / Math.abs(saldoPrev)) * 100 : 0; // Category breakdown: combine detailed tx categories with aggregate sources when no detailed data const catData = useMemoA(() => { const fromTxs = window.byCategory(activeTxs); if (period === "mes" && fromTxs.length === 0) { return window.aggCategoryBreakdown(state.aggregates, year, month); } if (period === "ano") { const map = {}; for (const c of fromTxs) map[c.category] = (map[c.category] || 0) + c.value; for (let m = 0; m < 12; m++) { for (const c of window.aggCategoryBreakdown(state.aggregates, year, m)) { map[c.category] = (map[c.category] || 0) + c.value; } } return Object.entries(map).map(([category, value]) => ({ category, value })).sort((a,b)=>b.value-a.value); } return fromTxs; }, [activeTxs, period, state.aggregates, year, month]); const totalSpent = monthExpense; // for "Spending Overview" total // Plano 70/10/10/10 (Jim Rohn) for the selected month const plano = useMemoA( () => window.computePlano(state.transactions, state.aggregates, year, month), [state.transactions, state.aggregates, year, month] ); // expose context for AI assistant useEffectA(() => { window.__appContext = () => ({ mes: window.MESES_FULL[month] + "/" + year, receita_mes: monthIncome, despesa_mes: monthExpense, economia_mes: monthSavings, top_categorias: catData.slice(0, 5).map(c => ({ nome: c.category, valor: c.value })), plano_70_10_10_10: { receita: plano.income, teto_gastos_70: plano.gastos.target, gastos_realizados: plano.gastos.used, acima_do_teto: plano.gastos.over ? plano.gastos.overBy : 0, redutivel_disponivel: plano.gastos.gastosRedutivel, metas: plano.buckets.map(b => ({ balde: b.label, meta: b.target, usado: b.used })), }, dividas: state.dividas.map(d => ({ desc: d.descricao, saldo: d.saldoDevedor, status: d.status })), total_devido: state.dividas.reduce((s, d) => s + d.saldoDevedor, 0), }); }, [month, year, monthIncome, monthExpense, monthSavings, catData, state.dividas, plano]); function changeMonth(m, y) { let nm = m, ny = y; if (m < 0) { nm = 11; ny = y - 1; } if (m > 11) { nm = 0; ny = y + 1; } setMonth(nm); setYear(ny); } function addTx(tx) { setState(s => ({ ...s, transactions: [...s.transactions, tx] })); } // Prêmios mensais dos seguros entram como lançamentos do mês (classe Proteção) function addSegurosDoMes() { setState(s => { const faltando = window.segurosFaltandoNoMes(s.seguros, s.transactions, year, month); if (!faltando.length) return s; return { ...s, transactions: [...s.transactions, ...window.lancamentosDeSeguros(faltando, year, month)] }; }); } // Remove lançamentos de seguros que não existem mais no cadastro useEffectA(() => { const ids = new Set((state.seguros || []).filter(sg => Number(sg.premioMensal || 0) > 0).map(sg => sg.id)); if (!state.transactions.some(t => t.seguroId && !ids.has(t.seguroId))) return; setState(s => ({ ...s, transactions: s.transactions.filter(t => !t.seguroId || ids.has(t.seguroId)) })); }, [state.seguros]); function deleteTx(id) { setState(s => ({ ...s, transactions: s.transactions.filter(t => t.id !== id) })); } function updateTx(id, patch) { setState(s => ({ ...s, transactions: s.transactions.map(t => t.id === id ? { ...t, ...patch } : t) })); } // ---- Recorrência: contas fixas aparecem automaticamente no mês novo como "a pagar" ---- function findPrevFixaMonth(txsAll, y, m) { for (let i = 1; i <= 12; i++) { let mm = m - i, yy = y; while (mm < 0) { mm += 12; yy -= 1; } const prev = window.txByMonth(txsAll, yy, mm).filter(t => t.fixa); if (prev.length > 0) return { txs: prev, y: yy, m: mm }; } return null; } function seedMonth(y, m) { setState(s => { if (window.txByMonth(s.transactions, y, m).length > 0) return s; const src = findPrevFixaMonth(s.transactions, y, m); if (!src) return s; const clones = src.txs.map(t => ({ ...t, id: window.uid(), date: `${y}-${String(m + 1).padStart(2, "0")}-${t.date.slice(8)}`, status: "Pendente", })); const key = `${y}-${String(m + 1).padStart(2, "0")}`; return { ...s, transactions: [...s.transactions, ...clones], seeded: { ...(s.seeded || {}), [key]: true } }; }); } // Auto-seed the viewed month once (only months after the first month with data) useEffectA(() => { const key = `${year}-${String(month + 1).padStart(2, "0")}`; if (state.seeded?.[key]) return; if (window.txByMonth(state.transactions, year, month).length > 0) return; if (!findPrevFixaMonth(state.transactions, year, month)) return; seedMonth(year, month); }, [year, month]); // Prêmios de seguro entram automaticamente no orçamento do mês visualizado useEffectA(() => { const faltando = window.segurosFaltandoNoMes(state.seguros, state.transactions, year, month); if (faltando.length) addSegurosDoMes(); }, [state.seguros, state.transactions, year, month]); function toggleTx(id, field) { setState(s => ({ ...s, transactions: s.transactions.map(t => { if (t.id !== id) return t; if (field === "status") return { ...t, status: t.status === "Pago" ? "Pendente" : "Pago" }; if (field === "redutivel") return { ...t, redutivel: !t.redutivel }; return t; }), })); } function saveDebt(d) { setState(s => { const exists = s.dividas.some(x => x.id === d.id); const dividas = exists ? s.dividas.map(x => x.id === d.id ? d : x) : [...s.dividas, d]; return { ...s, dividas }; }); } function deleteDebt(id) { setState(s => ({ ...s, dividas: s.dividas.filter(d => d.id !== id) })); } function resetData() { if (!confirm("Restaurar dados originais da planilha? Você perderá suas edições.")) return; localStorage.removeItem(STORAGE_KEY); setState(withNewSlices({ transactions: window.buildInitialTransactions(seed), aggregates: window.buildInitialAggregates(seed), dividas: seed.dividas, })); } // ---- Patrimônio / proteção / objetivos: CRUD genérico ---- const KIND_COLL = { imovel: "imoveis", veiculo: "veiculos", saldo: "saldos", participacao: "participacoes", outro: "outros" }; function saveAsset(kind, obj) { setState(s => { if (kind === "seguro") { const exists = s.seguros.some(x => x.id === obj.id); return { ...s, seguros: exists ? s.seguros.map(x => x.id === obj.id ? obj : x) : [...s.seguros, obj] }; } if (kind === "objetivo") { const exists = s.objetivos.some(x => x.id === obj.id); return { ...s, objetivos: exists ? s.objetivos.map(x => x.id === obj.id ? obj : x) : [...s.objetivos, obj] }; } const coll = KIND_COLL[kind]; const arr = s.patrimonio[coll] || []; const exists = arr.some(x => x.id === obj.id); const clean = { ...obj }; delete clean.__kind; return { ...s, patrimonio: { ...s.patrimonio, [coll]: exists ? arr.map(x => x.id === obj.id ? clean : x) : [...arr, clean] } }; }); } function deleteAsset(coll, id) { if (!confirm("Excluir este item?")) return; setState(s => { if (coll === "__seguros") return { ...s, seguros: s.seguros.filter(x => x.id !== id) }; if (coll === "__objetivos") return { ...s, objetivos: s.objetivos.filter(x => x.id !== id) }; return { ...s, patrimonio: { ...s.patrimonio, [coll]: (s.patrimonio[coll] || []).filter(x => x.id !== id) } }; }); } return (
setView("relatorio")} appState={state} onOpenFechamento={() => setModal({ kind: "fechamento" })}/>
{view === "overview" && (
goTo("lancamentos")} />
{ if (kind === "divida") setModal({ kind: "divida" }); else if (kind === "fatura") goTo("cartoes"); else setModal({ kind }); }}/>
goTo("lancamentos")} />
)} {view === "orcamento" && (
{[ { id: "lancamentos", label: "✔️ Checklist" }, { id: "analise", label: "Análise" }, { id: "receitas", label: "Receitas" }, { id: "despesas", label: "Despesas" }, { id: "cartoes", label: "Cartões" }, { id: "dividas", label: "Dívidas" }, { id: "insights", label: "Insights" }, ].map(t => ( ))}
{orcSub === "lancamentos" && ( <> seedMonth(year, month)} canCopyPrev={!!findPrevFixaMonth(state.transactions, year, month)} segurosFaltando={window.segurosFaltandoNoMes(state.seguros, state.transactions, year, month)} onAddSeguros={addSegurosDoMes} confronto={state.confronto || {}} onConfronto={(mKey, dataM) => setState(s => ({ ...s, confronto: { ...(s.confronto || {}), [mKey]: dataM } }))} fechados={state.mesesFechados || {}} onFecharMes={(mKey, dados) => setState(s => { const mf = { ...(s.mesesFechados || {}) }; if (dados) mf[mKey] = dados; else delete mf[mKey]; return { ...s, mesesFechados: mf }; })} /> )} {orcSub === "receitas" && ( )} {orcSub === "despesas" && ( )} {orcSub === "analise" && } {orcSub === "cartoes" && ( setState(s => ({ ...s, transactions: [...s.transactions, ...novas] }))} /> )} {orcSub === "dividas" && ( setModal({ kind: "divida" })} onEdit={(d) => setModal({ kind: "divida", edit: d })} onDelete={deleteDebt} /> )} {orcSub === "insights" && }
)} {view === "relatorio-mensal" && ( setView("overview")} /> )} {view === "familia" && ( setState(s => ({ ...s, objetivosPessoais: next, objetivos: window.syncMetasPessoais(s.objetivos || [], next) }))} objetivos={state.objetivos} familia={state.familia} onEditFamilia={() => setModal({ kind: "familia" })} onAsset={(kind) => setModal({ kind: "asset", asset: kind })} onEditAsset={(obj) => setModal({ kind: "asset", asset: obj.__kind, edit: obj })} onDeleteAsset={deleteAsset} /> )} {view === "patrimonio" && ( setModal({ kind: "asset", asset: kind })} onEditAsset={(obj) => setModal({ kind: "asset", asset: obj.__kind, edit: obj })} onDeleteAsset={deleteAsset} onGoTo={goTo} /> )} {view === "protecao" && ( setModal({ kind: "asset", asset: kind })} onEditAsset={(obj) => setModal({ kind: "asset", asset: obj.__kind, edit: obj })} onDeleteAsset={deleteAsset} onEditFamilia={() => setModal({ kind: "familia" })} onUpdateSucessao={(sucessao) => setState(s => ({ ...s, sucessao }))} /> )} {view === "reserva" && ( setState(s => ({ ...s, reserva }))} onAsset={(kind) => setModal({ kind: "asset", asset: kind })} onEditAsset={(obj) => setModal({ kind: "asset", asset: obj.__kind, edit: obj })} /> )} {view === "aposentadoria" && ( setState(s => ({ ...s, aposentadoria }))} /> )} {view === "imoveis" && ( setModal({ kind: "asset", asset: kind })} onEditAsset={(obj) => setModal({ kind: "asset", asset: obj.__kind, edit: obj })} onDeleteAsset={deleteAsset} /> )} {view === "automoveis" && ( setModal({ kind: "asset", asset: kind })} onEditAsset={(obj) => setModal({ kind: "asset", asset: obj.__kind, edit: obj })} onDeleteAsset={deleteAsset} /> )} {view === "irpf" && setState(s => ({ ...s, etapas: { ...(s.etapas || {}), [id]: v } }))}/>} {view === "relatorio" && (
{relTipo === "budget" ? : }
)} {view === "config" && } {view === "help" && }
setState(s => ({ ...s, planoCheck: { ...(s.planoCheck || {}), [mKey]: { ...((s.planoCheck || {})[mKey] || {}), [bucketKey]: data } }, }))} /> {modal?.kind === "receita" && ( setModal(null)} onSave={addTx} /> )} {modal?.kind === "despesa" && ( setModal(null)} onSave={addTx} /> )} {modal?.kind === "divida" && ( setModal(null)} onSave={saveDebt} /> )} {modal?.kind === "asset" && ( setModal(null)} onSave={(obj) => saveAsset(modal.asset, obj)} /> )} {modal?.kind === "familia" && ( setModal(null)} onSave={(familia) => setState(s => ({ ...s, familia }))} /> )} {modal?.kind === "fechamento" && ( setModal(null)} onRelatorio={(key) => { setRelRef(key); setView("relatorio-mensal"); }} onSave={(cfg, entrega) => setState(s => { const f = { dia: 5, entregas: {}, ...(s.fechamento || {}), ...(cfg || {}) }; if (entrega) f.entregas = { ...f.entregas, [entrega.key]: entrega.dados }; return { ...s, fechamento: f }; })} /> )}
); } // ---------- Simple secondary pages ---------- // Gastos do mês por classe (Fixo essencial · Fixo · Variável · Investimento · Proteção · Caridade) function ClassesCard({ txs, monthLabel }) { const items = window.byClasse(txs); const total = items.reduce((s, i) => s + i.value, 0); if (total === 0) return null; return (

Gastos por classe · {monthLabel}

Classifique cada lançamento na lista abaixo.
{items.map(i => (
))}
{items.map(i => ( {i.icon} {i.label} {window.fmtCompact(i.value)} · {Math.round((i.value / total) * 100)}% ))}
); } function AnalisePage({ aggregates, txs, year, month, onChangeMonth, onChangeYear }) { // Monthly receita/despesa for the selected year (aggregates + transactions) function monthDataFor(y) { return window.MESES.map((label, i) => { const monthly = window.txByMonth(txs, y, i); const txIn = window.sumIncome(monthly); const txOut = window.sumExpense(monthly); const aggIn = window.aggIncomeForMonth(aggregates, y, i); const aggOut = window.aggExpenseForMonth(aggregates, y, i); return { label, receita: txIn + aggIn, despesa: txOut + aggOut }; }); } const current = monthDataFor(year); const prev = monthDataFor(year - 1); const maxVal = Math.max( ...current.map(m => Math.max(m.receita, m.despesa)), ...prev.map(m => Math.max(m.receita, m.despesa)), 1 ); const totalReceita = current.reduce((s, m) => s + m.receita, 0); const totalDespesa = current.reduce((s, m) => s + m.despesa, 0); const totalBalanco = totalReceita - totalDespesa; const prevReceita = prev.reduce((s, m) => s + m.receita, 0); const prevDespesa = prev.reduce((s, m) => s + m.despesa, 0); const prevBalanco = prevReceita - prevDespesa; function deltaPct(curr, prv) { if (!prv) return curr > 0 ? 100 : 0; return ((curr - prv) / Math.abs(prv)) * 100; } const monthsWithData = current.filter(m => m.receita > 0 || m.despesa > 0); const worstMonth = [...current].sort((a, b) => b.despesa - a.despesa)[0]; const bestMonth = [...current].sort((a, b) => (b.receita - b.despesa) - (a.receita - a.despesa))[0]; // --- Mês selecionado: receitas × despesas --- const mt = window.monthTotals(txs, aggregates, year, month); const mSaldo = mt.income - mt.expense; const mMax = Math.max(mt.income, mt.expense, 1); const mPct = mt.income > 0 ? (mt.expense / mt.income) * 100 : 0; return (

📊 Análise Anual

Receitas {year}
= 0 ? "pill-up" : "pill-down"}`}> {deltaPct(totalReceita, prevReceita) >= 0 ? "+" : ""}{deltaPct(totalReceita, prevReceita).toFixed(1)}% vs {year - 1}
Despesas {year}
{deltaPct(totalDespesa, prevDespesa) >= 0 ? "+" : ""}{deltaPct(totalDespesa, prevDespesa).toFixed(1)}% vs {year - 1}
Balanço {year}
= prevBalanco ? "pill-up" : "pill-down"}`}> {totalBalanco >= prevBalanco ? "+" : ""}{(totalBalanco - prevBalanco).toLocaleString("pt-BR", {maximumFractionDigits: 0})} vs {year - 1}
Meses com dados{monthsWithData.length}
Maior despesa{worstMonth?.despesa > 0 ? worstMonth.label : "—"}
Melhor saldo{(bestMonth?.receita - bestMonth?.despesa) > 0 ? bestMonth.label : "—"}

Receitas × Despesas — {window.MESES_FULL[month]}/{year}

{onChangeMonth && (
)}
Receitas
{window.fmtBRLString(mt.income)}
Despesas
mt.income ? "amb-out over" : "amb-out"} style={{width: (mt.expense / mMax) * 100 + "%"}}>
{window.fmtBRLString(mt.expense)}
Saldo do mês: = 0 ? "in" : "out"}>{window.fmtBRLString(mSaldo)} {mt.income > 0 && Despesas consomem 100 ? "out" : ""}>{Math.round(mPct)}% da receita} {mt.income > 0 && (mPct > 100 ? 🚨 Gastando mais do que ganha : mSaldo >= mt.income * 0.30 - 0.5 ? ✅ Meta 70% batida : ⚠️ Acima da meta de 70%)}

Receitas vs Despesas — {year}

Receitas {year} Despesas {year} Receitas {year-1} Despesas {year-1}
{current.map((m, i) => { const p = prev[i]; return (
{m.label}
); })}

Detalhamento mensal — {year}

{current.map((m, i) => { const p = prev[i]; const bal = m.receita - m.despesa; const pBal = p.receita - p.despesa; const delta = bal - pBal; return ( ); })}
Mês Receita Despesa Balanço Receita {year-1} Despesa {year-1} Δ Balanço
{m.label} {m.receita > 0 ? window.fmtBRLString(m.receita) : "—"} {m.despesa > 0 ? window.fmtBRLString(m.despesa) : "—"} = 0 ? "in" : "out"}`}>{(m.receita + m.despesa) > 0 ? window.fmtBRLString(bal) : "—"} {p.receita > 0 ? window.fmtBRLString(p.receita) : "—"} {p.despesa > 0 ? window.fmtBRLString(p.despesa) : "—"} = 0 ? "in" : "out"}`}>{(Math.abs(delta) > 0.5) ? (delta > 0 ? "+" : "") + window.fmtBRLString(delta) : "—"}
Total {window.fmtBRLString(totalReceita)} {window.fmtBRLString(totalDespesa)} = 0 ? "in" : "out"}`}>{window.fmtBRLString(totalBalanco)} {window.fmtBRLString(prevReceita)} {window.fmtBRLString(prevDespesa)} = 0 ? "in" : "out"}`}>{(totalBalanco - prevBalanco) >= 0 ? "+" : ""}{window.fmtBRLString(totalBalanco - prevBalanco)}
); } function YearSwitchTop({ year, onChange }) { return (
{[2025, 2026, 2027].map(y => ( ))}
); } function InsightsPage({ state, month, year }) { const [insights, setInsights] = useStateA([]); const [loading, setLoading] = useStateA(false); async function generate() { setLoading(true); try { const ctx = window.__appContext(); const prompt = `Você é um consultor financeiro pessoal. Gere 4 insights acionáveis em PORTUGUÊS sobre estes dados financeiros. Cada insight deve ter um título curto (máx 6 palavras) e uma análise (máx 2 frases). Retorne SOMENTE um JSON array no formato: [{"titulo":"...","descricao":"..."},...]\n\nDados:\n${JSON.stringify(ctx, null, 2)}`; const res = await window.claude.complete(prompt); const match = res.match(/\[[\s\S]*\]/); if (match) setInsights(JSON.parse(match[0])); } catch (e) { setInsights([{titulo:"Erro", descricao:"Não consegui gerar insights agora."}]); } finally { setLoading(false); } } return (

🧠 Insights Inteligentes

{insights.length === 0 && !loading && (
Clique em "Gerar análise IA" para receber insights baseados em seus dados.
)}
{insights.map((i, idx) => (
{String(idx+1).padStart(2,"0")}

{i.titulo}

{i.descricao}

))}
); } function CartoesPage({ txs, year, month, contas, categorias, onImport }) { const cards = ["XP", "C6", "Itaú", "Cartão Larissa"]; return (

💳 Cartões de Crédito

{})} />
{cards.map(card => { const cardTxs = txs.filter(t => t.account === card && t.date.startsWith(String(year))); const total = cardTxs.reduce((s,t)=>s+t.amount,0); const monthly = window.MESES.map((label, m) => { const tt = cardTxs.filter(t => t.date.startsWith(`${year}-${String(m+1).padStart(2,"0")}`)); return { label, total: tt.reduce((s,t)=>s+t.amount,0) }; }); const maxV = Math.max(...monthly.map(m=>m.total), 1); return (
{card}
{monthly.map(m => (
{m.label}
))}
); })}
); } function MetasPage() { return (

🎯 Metas Financeiras

Em breve. Cadastre suas metas para acompanhar o progresso.
); } function ConfigPage({ onReset }) { return (

⚙️ Configurações

Dados

Seus lançamentos e dívidas são salvos no navegador. Você pode restaurar os dados originais da planilha.

); } function HelpPage() { return (

❓ Ajuda

Como usar

  • Visão Geral: resumo do mês selecionado com saldo, receita, despesa e categorias.
  • Lançamentos: lista completa, com filtros e busca. Adicione novas receitas e despesas pelo botão "+".
  • Dívidas: rastreie saldo, parcelas e vencimentos.
  • Análise: visão anual com gráfico mês a mês.
  • Insights: análise IA sobre seus dados.
  • Cartões: uso mensal de cada cartão.
); } Object.assign(window, { App, AnalisePage, InsightsPage, CartoesPage, MetasPage, ConfigPage, HelpPage, ClassesCard }); // Mount — gate de autenticação: sem sessão, só a tela de login function Root() { const [session, setSession] = React.useState(() => window.AUTH.getSession()); if (!session) return ; const authUser = window.AUTH.users.find(u => u.id === session.userId); return { window.AUTH.logout(); setSession(null); }}/>; } const root = ReactDOM.createRoot(document.getElementById("root")); root.render();