// 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 (
{ setDraft(String(value).replace(".", ",")); setEditing(true); }}>
{window.fmtBRLString(value)}
);
}
// 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 (
onToggle(t.id, "status")}
title={pago ? "Remover validação" : "Validar lançamento"}>
{pago && (
)}
{t.description}
{t.category}{t.account ? " · " + t.account : ""}{t.fixa ? " · 📍 fixa" : ""}
{t.type === "Despesa" ? (
setOpenCl(o => !o)}
title="Classe do gasto">
{cdef.icon} {cdef.label}
{openCl && menuPos && ReactDOM.createPortal(
e.stopPropagation()}>
{window.CLASSES.map(c => (
pick(c.id)}>{c.icon} {c.label}
))}
,
document.body
)}
) :
}
onUpdate(t.id, { amount: v })} className={t.type === "Receita" ? "in" : ""}/>
onDelete(t.id)} title="Excluir">
);
}
// 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 setOpen(true)}>+ Adicionar ;
}
return (
);
}
// 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 (
{pagoCount}/{txs.length} validados
{window.fmtBRLString(total)}
{target != null && de {window.fmtBRLString(target)} }
{target != null && (
)}
{isGasto && txs.length > 0 && (
setFCl(null)}>Todas
{window.CLASSES.filter(c => clCounts[c.id]).map(c => (
setFCl(fCl === c.id ? null : c.id)}>{c.icon} {c.label} ({clCounts[c.id]})
))}
setFCat(e.target.value)}>
Todas as categorias
{cats.map(c => {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.
Trazer contas fixas do mês anterior
)}
{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% )}
onFecharMes(mKey, null)}>Reabrir mês
);
}
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)}
onFecharMes(mKey, { em: new Date().toISOString(), receita, despesa, saldo, metaOk })}>
Fechar o mês
);
}
Object.assign(window, { LancamentosPage });