// Finance screen — wallet, transactions, charts, cofrinhos const FinanceScreen = ({ state, setState, navigate, showToast }) => { const { finance } = state; const [tab, setTab] = React.useState('geral'); const [hideValues, setHideValues] = React.useState(false); const [showNew, setShowNew] = React.useState(false); const [showNewCofrinho, setShowNewCofrinho] = React.useState(false); const [showAnalise, setShowAnalise] = React.useState(false); const [showNewCard, setShowNewCard] = React.useState(false); const addCard = (card) => { setState(s => ({ ...s, finance: { ...s.finance, cards: [...(s.finance.cards || []), { ...card, id: 'card' + Date.now(), fatura: 0 }], } })); setShowNewCard(false); showToast('Cartão adicionado'); }; const deleteCard = (cardId) => { if (!window.confirm || window.confirm('Excluir este cartão? As transações registradas continuam no histórico.')) { setState(s => ({ ...s, finance: { ...s.finance, cards: (s.finance.cards || []).filter(c => c.id !== cardId), } })); showToast('Cartão removido'); } }; const addCofrinho = (cof) => { setState(s => ({ ...s, finance: { ...s.finance, cofrinhos: [...s.finance.cofrinhos, { ...cof, id: 'c' + Date.now(), saved: 0 }] } })); setShowNewCofrinho(false); showToast('Cofrinho criado'); }; const addTransaction = (tx) => { const value = (tx.type === 'gasto' ? -1 : 1) * Math.abs(Number((tx.value || '0').toString().replace(',', '.')) || 0); setState(s => ({ ...s, finance: { ...s.finance, saldo: s.finance.saldo + value, entradas: value > 0 ? s.finance.entradas + value : s.finance.entradas, saidas: value < 0 ? s.finance.saidas + Math.abs(value) : s.finance.saidas, transactions: [ { id: 'tx' + Date.now(), title: tx.desc || (tx.type === 'gasto' ? 'Saída' : 'Entrada'), cat: 'outros', value, time: 'agora', icon: tx.type === 'gasto' ? 'arrowUp' : 'arrowDown' }, ...s.finance.transactions, ], } })); setShowNew(false); showToast('Movimentação adicionada'); }; const fmt = (v) => { if (hideValues) return '••••'; return `R$ ${Math.abs(v).toLocaleString('pt-BR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }; // Intent: abrir modal de nova movimentação via evento global React.useEffect(() => { const handler = () => setShowNew(true); window.addEventListener('cdv:open-new-transaction', handler); return () => window.removeEventListener('cdv:open-new-transaction', handler); }, []); return (
} /> {/* Balance hero card */}
Saldo total
+12% este mês
{fmt(finance.saldo)}
{/* Tab switcher */}
{[ { id: 'geral', label: 'Geral' }, { id: 'cartao', label: 'Cartão' }, { id: 'cofrinhos', label: 'Cofrinhos' }, ].map(t => ( ))}
{tab === 'geral' && ( <> {/* Spending by category */} setShowAnalise(true)} />
{/* AI insight */}
Alerta da IA
Você gastou 23% a mais com iFood esta semana. Posso te sugerir um teto semanal de R$ 200?
{/* Recent transactions */}
{finance.transactions.map(tx => ( ))}
)} {tab === 'cartao' && setShowNewCard(true)} onDelete={deleteCard} />} {tab === 'cofrinhos' && setShowNewCofrinho(true)} />} setShowNew(false)} title="Nova movimentação"> setShowNewCofrinho(false)} title="Novo cofrinho"> setShowAnalise(false)} title="Análise detalhada"> setShowNewCard(false)} title="Adicionar cartão">
); }; const NewCofrinhoForm = ({ onSubmit }) => { const [name, setName] = React.useState(''); const [target, setTarget] = React.useState(''); const icons = ['plane', 'car', 'shield', 'home', 'star', 'book']; const colors = ['#FF8FB1', '#6FB8FF', '#5EE3A8', '#FFB547', '#B197FC', '#FF7A6B']; const [icon, setIcon] = React.useState(icons[0]); const [color, setColor] = React.useState(colors[0]); const canSave = name.trim() && Number(target) > 0; return (
setName(e.target.value)} placeholder="Nome do cofrinho (ex: Viagem)" style={{ width: '100%', height: 54, borderRadius: 16, border: `1px solid ${CDV.stroke}`, background: CDV.surfaceHi, color: CDV.text, padding: '0 16px', fontSize: 15, outline: 'none', fontFamily: 'inherit', marginBottom: 12 }} />
META
setTarget(e.target.value.replace(/[^\d]/g, ''))} placeholder="0" style={{ width: '100%', background: 'transparent', border: 'none', outline: 'none', color, fontSize: 34, fontWeight: 700, textAlign: 'center', marginTop: 4, fontFamily: 'inherit' }} />
R$
Ícone
{icons.map(ic => ( ))}
Cor
{colors.map(c => (
); }; const AnaliseGastos = ({ finance, hide }) => { const total = finance.categoriaGastos.reduce((a, b) => a + b.value, 0); const top = [...finance.categoriaGastos].sort((a, b) => b.value - a.value); return (
TOTAL DO MÊS
{hide ? '••••' : `R$ ${total.toLocaleString('pt-BR')}`}
Média diária: {hide ? '••••' : `R$ ${Math.round(total / 30).toLocaleString('pt-BR')}`}
Ranking por categoria
{top.map((c, i) => (
{i + 1}
{c.cat}
{hide ? '••••' : `R$ ${c.value.toLocaleString('pt-BR')}`}
{c.percent}% do total
))}
Observação
Sua maior categoria ({top[0].cat}) representa {top[0].percent}% do total. Cortar 10% aqui sobraria {hide ? '••' : `R$ ${Math.round(top[0].value * 0.1)}`} no mês.
); }; const FinSplit = ({ label, value, color, icon, iconRot }) => (
{label}
{value}
); const SpendingChart = ({ cats, hideValues }) => { const total = cats.reduce((a, b) => a + b.value, 0); const ringSize = 130; const stroke = 22; const r = (ringSize - stroke) / 2; const c = 2 * Math.PI * r; let offset = 0; return (
{cats.map((cat, i) => { const len = (cat.percent / 100) * c; const o = c - offset - len; offset += len; return ( ); })}
Mês
{hideValues ? '••••' : 'R$ ' + total.toLocaleString('pt-BR')}
{cats.map((cat, i) => (
{cat.cat}
{cat.percent}%
))}
); }; const TransactionRow = ({ tx, hide }) => { const color = CDV.cats[tx.cat] || CDV.brand; const pos = tx.value > 0; const valueColor = pos ? CDV.mint : CDV.coral; return (
{tx.title}
{tx.cat} · {tx.time}
{hide ? '••••' : `${pos ? '+' : '−'} R$ ${Math.abs(tx.value).toLocaleString('pt-BR', { minimumFractionDigits: 2 })}`}
); }; // Constrói lista de cartões: usa state.finance.cards, e se vazio mas tiver // dados legados (cartao/cartaoLimite/vencimento), mostra como fallback. function getCards(finance) { if (Array.isArray(finance.cards) && finance.cards.length > 0) return finance.cards; if (finance.cartao && finance.cartaoLimite) { return [{ id: '_legacy', apelido: 'Cartão principal', bandeira: 'mastercard', cor: '#2A1B4A', corGrad: 'linear-gradient(135deg, #2A1B4A 0%, #4A1B33 100%)', limite: finance.cartaoLimite, fatura: finance.cartao, vencimento: finance.vencimento, digitos: '••••', }]; } return []; } const CardTab = ({ finance, hide, fmt, onAdd, onDelete }) => { const cards = getCards(finance); return (
Seus cartões {cards.length > 0 && `· ${cards.length}`}
{cards.length === 0 ? (
Nenhum cartão cadastrado
Adicione seus cartões pra acompanhar fatura e limite.
) : (
{cards.map(c => ( onDelete(c.id)} /> ))}
)} {[ { mes: 'Julho 2026', val: 2840, status: 'previsto' }, { mes: 'Agosto 2026', val: 2200, status: 'previsto' }, ].map((p, i) => (
{p.mes}
Previsão da IA
{fmt(p.val)}
))}
); }; const CreditCardItem = ({ card, hide, fmt, onDelete }) => { const usedPct = card.limite > 0 ? (card.fatura / card.limite) * 100 : 0; const grad = card.corGrad || `linear-gradient(135deg, ${card.cor} 0%, ${card.cor}cc 100%)`; return (
Fatura atual
{fmt(card.fatura || 0)}
{card.apelido} · {bandeiraLabel(card.bandeira)} {card.digitos && `· •• ${card.digitos}`}
{onDelete && ( )}
Usado {hide ? '••' : `${Math.round(usedPct)}%`}
Limite {fmt(card.limite || 0)}
Vence {typeof card.vencimento === 'number' ? `dia ${card.vencimento}` : card.vencimento || '—'}
); }; function bandeiraLabel(b) { const map = { visa: 'Visa', mastercard: 'Mastercard', elo: 'Elo', amex: 'Amex', hipercard: 'Hipercard', outra: 'Outro' }; return map[b] || 'Cartão'; } const NewCardForm = ({ onSubmit }) => { const [apelido, setApelido] = React.useState(''); const [digitos, setDigitos] = React.useState(''); const [bandeira, setBandeira] = React.useState('mastercard'); const [limite, setLimite] = React.useState(''); const [vencimento, setVencimento] = React.useState('10'); const [cor, setCor] = React.useState('#2A1B4A'); const presets = [ { name: 'Nubank', cor: '#820AD1' }, { name: 'Itaú', cor: '#EC7000' }, { name: 'Bradesco', cor: '#CC092F' }, { name: 'Santander', cor: '#EC0000' }, { name: 'BB', cor: '#FAE128' }, { name: 'C6', cor: '#222328' }, { name: 'Inter', cor: '#FF7A00' }, { name: 'Roxo', cor: '#2A1B4A' }, ]; const bandeiras = ['visa', 'mastercard', 'elo', 'amex', 'hipercard', 'outra']; const canSave = apelido.trim() && digitos.length === 4 && Number(limite) > 0; return (
{/* Preview */}
{apelido || 'Apelido do cartão'}
{bandeiraLabel(bandeira)}
•••• •••• •••• {digitos || '••••'}
setApelido(e.target.value)} placeholder="Apelido (ex: Nubank pessoal)" style={{ width: '100%', height: 50, borderRadius: 14, border: `1px solid ${CDV.stroke}`, background: CDV.surfaceHi, color: CDV.text, padding: '0 16px', fontSize: 14, outline: 'none', fontFamily: 'inherit', marginBottom: 10 }} />
setDigitos(e.target.value.replace(/[^\d]/g, '').slice(0, 4))} placeholder="Últimos 4 dígitos" inputMode="numeric" style={{ height: 50, borderRadius: 14, border: `1px solid ${CDV.stroke}`, background: CDV.surfaceHi, color: CDV.text, padding: '0 16px', fontSize: 14, outline: 'none', fontFamily: 'inherit', letterSpacing: 2 }} /> setVencimento(e.target.value.replace(/[^\d]/g, '').slice(0, 2))} placeholder="Vencimento (dia)" inputMode="numeric" style={{ height: 50, borderRadius: 14, border: `1px solid ${CDV.stroke}`, background: CDV.surfaceHi, color: CDV.text, padding: '0 16px', fontSize: 14, outline: 'none', fontFamily: 'inherit' }} />
setLimite(e.target.value.replace(/[^\d.,]/g, ''))} placeholder="Limite total (R$)" inputMode="decimal" style={{ width: '100%', height: 50, borderRadius: 14, border: `1px solid ${CDV.stroke}`, background: CDV.surfaceHi, color: CDV.text, padding: '0 16px', fontSize: 14, outline: 'none', fontFamily: 'inherit', marginBottom: 14 }} />
Bandeira
{bandeiras.map(b => { const active = bandeira === b; return ( ); })}
Cor do banco
{presets.map(p => (
Não armazenamos número completo nem CVV — apenas os últimos 4 dígitos pra você identificar.
); }; const CofrinhosTab = ({ cofrinhos, hide, onAdd }) => (
{cofrinhos.map(c => { const pct = (c.saved / c.target) * 100; return (
{c.name}
{hide ? '••••' : `R$ ${c.saved.toLocaleString('pt-BR')} de R$ ${c.target.toLocaleString('pt-BR')}`}
{Math.round(pct)}%
); })}
); const NewTransactionForm = ({ onDone }) => { const [type, setType] = React.useState('gasto'); const [value, setValue] = React.useState(''); const [desc, setDesc] = React.useState(''); const submit = () => { if (typeof onDone === 'function') { onDone({ type, value, desc }); } }; return (
{[{ id: 'gasto', label: 'Saída', c: CDV.coral }, { id: 'receita', label: 'Entrada', c: CDV.mint }].map(t => ( ))}
VALOR
setValue(e.target.value)} placeholder="0,00" style={{ border: 'none', outline: 'none', background: 'transparent', color: type === 'gasto' ? CDV.coral : CDV.mint, fontSize: 40, fontWeight: 700, textAlign: 'center', width: '100%', marginTop: 6, fontFamily: 'inherit', }} />
R$
setDesc(e.target.value)} placeholder="Descrição" style={{ width: '100%', height: 50, borderRadius: 14, border: `1px solid ${CDV.stroke}`, background: CDV.surfaceHi, color: CDV.text, padding: '0 16px', fontSize: 14, outline: 'none', fontFamily: 'inherit', marginBottom: 10, }} />
A categoria será detectada automaticamente
); }; Object.assign(window, { FinanceScreen });