// Receitas and Despesas pages - editable monthly grids matching the spreadsheet structure const { useState: useStateG, useMemo: useMemoG } = React; // ---------- YEAR SWITCH ---------- function YearSwitch({ year, onChange, years = [2025, 2026, 2027] }) { return (
{years.map(y => ( ))}
); } // ---------- EDITABLE CELL ---------- function EditCell({ value, onChange, dim }) { const [editing, setEditing] = useStateG(false); const [draft, setDraft] = useStateG(""); function start() { setDraft(value > 0 ? String(value).replace(".", ",") : ""); setEditing(true); } function commit() { const parsed = window.parseValor(draft); onChange(isNaN(parsed) ? 0 : parsed); setEditing(false); } function onKey(e) { if (e.key === "Enter") { e.preventDefault(); commit(); e.target.blur(); } else if (e.key === "Escape") { setEditing(false); } else if (e.key === "Tab") { commit(); /* tab default behavior moves focus */ } } if (editing) { return ( setDraft(e.target.value)} onBlur={commit} onKeyDown={onKey} /> ); } return ( ); } // ---------- GRID TABLE ---------- function GridTable({ headers, rows, summary, hint }) { return (
{headers.map((h, i) => )} {rows} {summary && {summary}}
{h}
{hint &&
{hint}
}
); } // ---------- RECEITAS PAGE ---------- function ReceitasPage({ aggregates, txs, year, onUpdate, onChangeYear }) { const data = (aggregates[year] || aggregates[2026] || { receitas: {}, despesas: { cards: {}, fixos: {} } }); const sources = Object.keys(data.receitas); const [newSource, setNewSource] = useStateG(""); // Add detailed transactions per source per month for context const txBySourceMonth = useMemoG(() => { const map = {}; for (const t of txs) { if (t.type !== "Receita" || !t.date.startsWith(String(year))) continue; const m = Number(t.date.slice(5, 7)) - 1; const key = `${t.description.split(" — ")[0]}|${m}`; map[key] = (map[key] || 0) + Number(t.amount || 0); } return map; }, [txs, year]); function setCell(src, monthIdx, val) { onUpdate(prev => { const aggs = window.ensureYear(prev.aggregates, year); const arr = aggs[year].receitas[src].slice(); arr[monthIdx] = val; return { ...prev, aggregates: { ...aggs, [year]: { ...aggs[year], receitas: { ...aggs[year].receitas, [src]: arr } } } }; }); } function addSource() { const name = newSource.trim(); if (!name || data.receitas[name]) return; onUpdate(prev => { const aggs = window.ensureYear(prev.aggregates, year); return { ...prev, aggregates: { ...aggs, [year]: { ...aggs[year], receitas: { ...aggs[year].receitas, [name]: new Array(12).fill(0) } } } }; }); setNewSource(""); } function delSource(src) { if (!confirm(`Remover linha "${src}"?`)) return; onUpdate(prev => { const aggs = window.ensureYear(prev.aggregates, year); const { [src]: _, ...rest } = aggs[year].receitas; return { ...prev, aggregates: { ...aggs, [year]: { ...aggs[year], receitas: rest } } }; }); } const monthTotals = new Array(12).fill(0); const sourceTotals = {}; for (const src of sources) { sourceTotals[src] = 0; for (let m = 0; m < 12; m++) { const v = data.receitas[src][m] || 0; monthTotals[m] += v; sourceTotals[src] += v; } } const grandTotal = monthTotals.reduce((s, v) => s + v, 0); const headers = ["Origem", ...window.MESES, "Total"]; const rows = sources.map(src => ( {src} {window.MESES.map((_, m) => ( setCell(src, m, v)}/> {txBySourceMonth[`${src}|${m}`] > 0 && ( + R$ {txBySourceMonth[`${src}|${m}`].toFixed(0)} )} ))} {window.fmtBRLString(sourceTotals[src]).replace("R$ ", "")} )); const summary = ( Total {monthTotals.map((v, i) => ( {v > 0 ? window.fmtBRLString(v).replace("R$ ", "") : "—"} ))} {window.fmtBRLString(grandTotal).replace("R$ ", "")} ); return (

💰 Receitas

Clique numa célula para editar

Receitas {year}

Total do ano: {window.fmtBRLString(grandTotal)}
setNewSource(e.target.value)} onKeyDown={e => e.key === "Enter" && addSource()} />
); } // ---------- DESPESAS PAGE ---------- function DespesasPage({ aggregates, year, onUpdate, onChangeYear }) { const data = (aggregates[year] || aggregates[2026] || { receitas: {}, despesas: { cards: {}, fixos: {} } }); const cards = Object.keys(data.despesas.cards); const fixos = Object.keys(data.despesas.fixos); const [newCard, setNewCard] = useStateG(""); const [newFixo, setNewFixo] = useStateG(""); function setCardCell(card, pessoa, m, v) { onUpdate(prev => { const aggs = window.ensureYear(prev.aggregates, year); const arr = aggs[year].despesas.cards[card][pessoa].slice(); arr[m] = v; return { ...prev, aggregates: { ...aggs, [year]: { ...aggs[year], despesas: { ...aggs[year].despesas, cards: { ...aggs[year].despesas.cards, [card]: { ...aggs[year].despesas.cards[card], [pessoa]: arr } } } } } }; }); } function setFixoCell(name, m, v) { onUpdate(prev => { const aggs = window.ensureYear(prev.aggregates, year); const arr = aggs[year].despesas.fixos[name].slice(); arr[m] = v; return { ...prev, aggregates: { ...aggs, [year]: { ...aggs[year], despesas: { ...aggs[year].despesas, fixos: { ...aggs[year].despesas.fixos, [name]: arr } } } } }; }); } function addCard() { const name = newCard.trim(); if (!name || data.despesas.cards[name]) return; onUpdate(prev => { const aggs = window.ensureYear(prev.aggregates, year); return { ...prev, aggregates: { ...aggs, [year]: { ...aggs[year], despesas: { ...aggs[year].despesas, cards: { ...aggs[year].despesas.cards, [name]: { Lucas: new Array(12).fill(0), Larissa: new Array(12).fill(0) } } } } } }; }); setNewCard(""); } function delCard(card) { if (!confirm(`Remover cartão "${card}"?`)) return; onUpdate(prev => { const aggs = window.ensureYear(prev.aggregates, year); const { [card]: _, ...rest } = aggs[year].despesas.cards; return { ...prev, aggregates: { ...aggs, [year]: { ...aggs[year], despesas: { ...aggs[year].despesas, cards: rest } } } }; }); } function addFixo() { const name = newFixo.trim(); if (!name || data.despesas.fixos[name]) return; onUpdate(prev => { const aggs = window.ensureYear(prev.aggregates, year); return { ...prev, aggregates: { ...aggs, [year]: { ...aggs[year], despesas: { ...aggs[year].despesas, fixos: { ...aggs[year].despesas.fixos, [name]: new Array(12).fill(0) } } } } }; }); setNewFixo(""); } function delFixo(name) { if (!confirm(`Remover linha "${name}"?`)) return; onUpdate(prev => { const aggs = window.ensureYear(prev.aggregates, year); const { [name]: _, ...rest } = aggs[year].despesas.fixos; return { ...prev, aggregates: { ...aggs, [year]: { ...aggs[year], despesas: { ...aggs[year].despesas, fixos: rest } } } }; }); } // monthly grand totals const monthTotals = new Array(12).fill(0); for (const card of cards) { for (const pessoa of Object.keys(data.despesas.cards[card])) { for (let m = 0; m < 12; m++) monthTotals[m] += data.despesas.cards[card][pessoa][m] || 0; } } for (const name of fixos) { for (let m = 0; m < 12; m++) monthTotals[m] += data.despesas.fixos[name][m] || 0; } const grandTotal = monthTotals.reduce((s, v) => s + v, 0); const headers = ["", ...window.MESES, "Total"]; // Build rows: per card, Lucas/Larissa/Total; then a section of fixos const cardRows = []; for (const card of cards) { const pessoas = Object.keys(data.despesas.cards[card]); const cardTotalByMonth = new Array(12).fill(0); let cardTotalAll = 0; cardRows.push( 💳 {card} ); for (const pessoa of pessoas) { let total = 0; cardRows.push( {pessoa} {window.MESES.map((_, m) => { const v = data.despesas.cards[card][pessoa][m] || 0; total += v; cardTotalByMonth[m] += v; return ( setCardCell(card, pessoa, m, nv)}/> ); })} {total > 0 ? window.fmtBRLString(total).replace("R$ ", "") : "—"} ); cardTotalAll += total; } cardRows.push( Total {card} {cardTotalByMonth.map((v, m) => ( {v > 0 ? window.fmtBRLString(v).replace("R$ ", "") : "—"} ))} {window.fmtBRLString(cardTotalAll).replace("R$ ", "")} ); } // Fixos rows const fixoRows = []; if (fixos.length > 0) { fixoRows.push( 📌 Despesas Fixas ); for (const name of fixos) { let total = 0; fixoRows.push( {name} {window.MESES.map((_, m) => { const v = data.despesas.fixos[name][m] || 0; total += v; return ( setFixoCell(name, m, nv)}/> ); })} {total > 0 ? window.fmtBRLString(total).replace("R$ ", "") : "—"} ); } } const summary = ( Total Geral {monthTotals.map((v, i) => ( {v > 0 ? window.fmtBRLString(v).replace("R$ ", "") : "—"} ))} {window.fmtBRLString(grandTotal).replace("R$ ", "")} ); return (

💸 Despesas

Clique numa célula para editar

Despesas {year}

Total do ano: {window.fmtBRLString(grandTotal)}
setNewCard(e.target.value)} onKeyDown={e => e.key === "Enter" && addCard()} />
setNewFixo(e.target.value)} onKeyDown={e => e.key === "Enter" && addFixo()} />
); } Object.assign(window, { ReceitasPage, DespesasPage });