// Checklist mensal — substitui a tabela de Lançamentos. // Contas fixas recorrem automaticamente; o usuário só marca pago e ajusta valores. const { useState: useStateC, useMemo: useMemoC, useRef: useRefC, useEffect: useEffectC } = React; // Inline-editable money value function MoneyCell({ value, onChange, className }) { const [editing, setEditing] = useStateC(false); const [draft, setDraft] = useStateC(""); const ref = useRefC(null); useEffectC(() => { if (editing && ref.current) { ref.current.focus(); ref.current.select(); } }, [editing]); function commit() { setEditing(false); const n = window.parseValor(draft); if (!isNaN(n) && n >= 0 && Math.abs(n - value) > 0.001) onChange(n); } if (editing) { return ( setDraft(e.target.value)} onBlur={commit} onKeyDown={e => { if (e.key === "Enter") commit(); if (e.key === "Escape") setEditing(false); }} /> ); } return ( ); } // One checklist row const CK_REDUTIVEL_CLASSES = ["variavel"]; function CkRow({ t, isGasto, onToggle, onUpdate, onDelete }) { const pago = t.status === "Pago"; const classe = window.classeForTx(t); const cdef = window.CLASSES.find(c => c.id === classe) || window.CLASSES[0]; const [openCl, setOpenCl] = useStateC(false); const [menuPos, setMenuPos] = useStateC(null); const wrapRef = useRefC(null); const btnRef = useRefC(null); useEffectC(() => { if (!openCl) return; const place = () => { const r = btnRef.current?.getBoundingClientRect(); if (!r) return; const H = 215, W = 170; const pageTop = r.bottom + window.scrollY + 5; const docH = document.documentElement.scrollHeight; const top = (pageTop + H > docH - 8) ? Math.max(8, r.top + window.scrollY - H - 5) : pageTop; const left = Math.max(8, Math.min(r.right + window.scrollX - W, document.documentElement.scrollWidth - W - 8)); setMenuPos({ left, top }); }; place(); const onDoc = e => { if (wrapRef.current && !wrapRef.current.contains(e.target) && !e.target.closest(".ck-classe-menu")) setOpenCl(false); }; document.addEventListener("mousedown", onDoc); window.addEventListener("scroll", place, true); window.addEventListener("resize", place); return () => { document.removeEventListener("mousedown", onDoc); window.removeEventListener("scroll", place, true); window.removeEventListener("resize", place); }; }, [openCl]); function pick(id) { onUpdate(t.id, { classe: id, redutivel: CK_REDUTIVEL_CLASSES.includes(id) }); setOpenCl(false); } return (
{t.description} {t.category}{t.account ? " · " + t.account : ""}{t.fixa ? " · 📍 fixa" : ""}
{t.type === "Despesa" ? (
{openCl && menuPos && ReactDOM.createPortal(
e.stopPropagation()}> {window.CLASSES.map(c => ( ))}
, document.body )}
) : } onUpdate(t.id, { amount: v })} className={t.type === "Receita" ? "in" : ""}/>
); } // Quick-add row: description + value, Enter saves. No modal. function CkQuickAdd({ bucket, categorias, contas, onAdd, year, month }) { const [open, setOpen] = useStateC(false); const [desc, setDesc] = useStateC(""); const [val, setVal] = useStateC(""); const gastoCats = categorias.filter(c => !["Receita","Caridade","Capital Ativo","Capital Passivo"].includes(c.tipo)).map(c => c.nome); const defaultCat = bucket === "receita" ? "💸 Receita" : bucket === "caridade" ? "🙏 Caridade" : bucket === "ativo" ? "🚀 Capital Ativo" : bucket === "passivo" ? "📈 Investimento" : (gastoCats[0] || "🤷 Outros"); const [cat, setCat] = useStateC(defaultCat); function save() { const n = window.parseValor(val); if (!desc.trim() || isNaN(n) || n <= 0) return; const day = String(Math.min(new Date().getDate(), 28)).padStart(2, "0"); onAdd({ id: window.uid(), description: desc.trim(), date: `${year}-${String(month + 1).padStart(2, "0")}-${day}`, amount: n, account: contas[0] || "PIX", category: bucket === "gastos" ? cat : defaultCat, type: bucket === "receita" ? "Receita" : "Despesa", status: "Pendente", redutivel: false, fixa: false, }); setDesc(""); setVal(""); } if (!open) { return ; } return (
setDesc(e.target.value)} onKeyDown={e => { if (e.key === "Enter") save(); if (e.key === "Escape") setOpen(false); }}/> {bucket === "gastos" && ( )} setVal(e.target.value)} onKeyDown={e => { if (e.key === "Enter") save(); if (e.key === "Escape") setOpen(false); }}/>
); } // Group of rows with subtotal vs. meta function CkGroup({ id, icon, title, sub, txs, target, isGasto, onToggle, onUpdate, onDelete, quickAdd }) { const [fCl, setFCl] = useStateC(null); const [fCat, setFCat] = useStateC(""); const total = txs.reduce((s, t) => s + Number(t.amount || 0), 0); const pagoCount = txs.filter(t => t.status === "Pago").length; const over = target != null && total > target + 0.5; const fill = target > 0 ? Math.min(total / target, 1) * 100 : 0; const clCounts = {}; if (isGasto) for (const t of txs) { const c = window.classeForTx(t); clCounts[c] = (clCounts[c] || 0) + 1; } const cats = isGasto ? [...new Set(txs.map(t => t.category))].sort() : []; const shown = isGasto ? txs.filter(t => (!fCl || window.classeForTx(t) === fCl) && (!fCat || t.category === fCat)) : txs; const filtered = fCl || fCat; const shownTotal = shown.reduce((s, t) => s + Number(t.amount || 0), 0); return (
{icon}

{title}

{sub}
{pagoCount}/{txs.length} validados
{window.fmtBRLString(total)} {target != null && de {window.fmtBRLString(target)}}
{target != null && (
)} {isGasto && txs.length > 0 && (
{window.CLASSES.filter(c => clCounts[c.id]).map(c => ( ))} {filtered && {shown.length} {shown.length === 1 ? "item" : "itens"} · {window.fmtBRLString(shownTotal)}}
)}
{shown.map(t => ( ))} {txs.length === 0 &&
Nada lançado ainda.
} {txs.length > 0 && shown.length === 0 &&
Nada com esse filtro.
}
{quickAdd}
); } // ---------- LANÇAMENTOS = CHECKLIST DO MÊS ---------- function LancamentosPage({ txs, month, year, onChangeMonth, plano, categorias, contas, onToggle, onUpdate, onDelete, onAdd, onCopyPrev, canCopyPrev, segurosFaltando, onAddSeguros, confronto, onConfronto, fechados, onFecharMes }) { const monthTxs = useMemoC(() => window.txByMonth(txs, year, month), [txs, year, month]); const receitas = monthTxs.filter(t => t.type === "Receita"); const despesas = monthTxs.filter(t => t.type === "Despesa"); const byBucket = k => despesas.filter(t => window.bucketForCategory(t.category) === k); const g = plano.gastos; // Projection: if everything pending gets paid, where do gastos land vs the 70% ceiling? const projected = g.used; // used already includes pending (lançado = comprometido) const pagoSoFar = g.gastosPago; // Barra de status: escala 0 → max(gasto total, receita); segmentada por classe de gasto const pctR = plano.income > 0 ? (projected / plano.income) * 100 : 0; const gastosCls = window.byClasse(despesas.filter(t => window.bucketForCategory(t.category) === "gastos")); const scaleMax = Math.max(projected, plano.income) || 1; const metaPct = (plano.income * 0.70 / scaleMax) * 100; const recPct = (plano.income / scaleMax) * 100; const buckets = [ { key: "receita", icon: "💸", title: "Receitas", sub: "entradas do mês", txs: receitas, target: null }, { key: "gastos", icon: "🏠", title: "Gastos", sub: "teto de 70% da receita", txs: byBucket("gastos"), target: plano.income * 0.70, isGasto: true }, { key: "caridade", icon: "🙏", title: "Caridade", sub: "10% da receita", txs: byBucket("caridade"), target: plano.income * 0.10 }, { key: "ativo", icon: "🚀", title: "Capital Ativo", sub: "10% · projetos pessoais", txs: byBucket("ativo"), target: plano.income * 0.10 }, { key: "passivo", icon: "📈", title: "Capital Passivo", sub: "10% · investimentos", txs: byBucket("passivo"), target: plano.income * 0.10 }, ]; return (

✔️ Checklist de {window.MESES_FULL[month]}

Valide cada lançamento (✓), ajuste valores clicando neles e feche o balanço do mês no final da página.
{/* Projeção dos 70% */} {plano.income > 0 && (
{g.over ? ( 🚨 Com tudo lançado, seus gastos fecham {window.fmtBRLString(g.overBy)} acima do teto de 70%. Corte nos ✂️ redutíveis ainda não validados: {window.fmtBRLString(g.redutivelPendente)} disponíveis. ) : ( ✅ Com tudo que está lançado, o mês fecha {window.fmtBRLString(g.target - projected)} abaixo do teto de 70%. )}
Validado {window.fmtBRLString(pagoSoFar)} Lançado {window.fmtBRLString(projected)} Meta ≤ 70% · {window.fmtBRLString(g.target)}
{gastosCls.map(c => (
))} {recPct < 99.5 && }
meta 70%
receita
Gastos {window.fmtBRLString(projected)} · {Math.round(pctR)}% da receita Receita {window.fmtBRLString(plano.income)}
{gastosCls.map(c => ( {c.icon} {c.label} {window.fmtBRLString(c.value)} ))}
)} {monthTxs.length === 0 && canCopyPrev && (
Este mês ainda está vazio.
)} {buckets.map(b => ( 0 ? b.target : null} isGasto={b.isGasto} onToggle={onToggle} onUpdate={onUpdate} onDelete={onDelete} quickAdd={} /> ))}
); } // ---------- FECHAMENTO DO MÊS ---------- // O usuário valida cada lançamento e então fecha o balanço do mês. // Receitas e despesas lançadas são obrigatórias para fechar. function FechamentoMesCard({ monthTxs, receitas, despesas, monthLabel, mKey, fechados, onFecharMes, plano }) { const fmt = window.fmtBRLString; const fechado = fechados[mKey]; const total = monthTxs.length; const validados = monthTxs.filter(t => t.status === "Pago").length; const temReceita = receitas.length > 0; const temDespesa = despesas.length > 0; const tudoValidado = total > 0 && validados === total; const pronto = temReceita && temDespesa && tudoValidado; const receita = receitas.reduce((s, t) => s + Number(t.amount || 0), 0); const despesa = despesas.reduce((s, t) => s + Number(t.amount || 0), 0); const saldo = receita - despesa; const metaOk = plano.income > 0 && !plano.gastos.over; if (fechado) { return (

🔒 {monthLabel} fechado

em {new Date(fechado.em).toLocaleDateString("pt-BR")}
Receitas {fmt(fechado.receita)} Despesas {fmt(fechado.despesa)} Saldo = 0 ? "in" : "out"}>{fmt(fechado.saldo)} {fechado.metaOk != null && (fechado.metaOk ? ✅ Meta 70% cumprida : 🚨 Acima da meta de 70%)}
); } const reqs = [ { ok: temReceita, label: "Receitas do mês lançadas" }, { ok: temDespesa, label: "Despesas do mês lançadas" }, { ok: tudoValidado, label: `Lançamentos validados (${validados}/${total})` }, ]; return (

🏁 Fechamento de {monthLabel}

Valide cada lançamento e feche o balanço do mês.
{reqs.map((r, i) => ( {r.ok ? "✅" : "○"} {r.label} ))}
{total > 0 && (
)}
Balanço: {fmt(receita)}{fmt(despesa)} = = 0 ? "in" : "out"}>{fmt(saldo)}
); } Object.assign(window, { LancamentosPage });