// 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 (
setHideValues(!hideValues)} style={{
background: CDV.surfaceHi, border: `1px solid ${CDV.stroke}`, width: 38, height: 38, borderRadius: 99,
color: CDV.textDim, display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
setShowNew(true)} style={{
background: CDV.brandGrad, border: 'none', width: 38, height: 38, borderRadius: 99,
display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff',
}}>
} />
{/* 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 => (
setTab(t.id)} style={{
flex: 1, height: 36, borderRadius: 12,
background: tab === t.id ? CDV.surfaceHi : 'transparent',
color: tab === t.id ? CDV.text : CDV.textDim,
border: `1px solid ${tab === t.id ? CDV.strokeHi : 'transparent'}`,
fontSize: 13, fontWeight: 600, fontFamily: 'inherit',
}}>{t.label}
))}
{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 => (
setIcon(ic)} style={{
width: 46, height: 46, borderRadius: 12,
background: icon === ic ? color + '24' : CDV.surfaceHi,
border: `1.5px solid ${icon === ic ? color : CDV.stroke}`,
color: icon === ic ? color : CDV.textDim, display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
))}
Cor
{colors.map(c => (
setColor(c)} style={{
width: 34, height: 34, borderRadius: 99, background: c,
border: color === c ? `2.5px solid ${CDV.text}` : '2.5px solid transparent',
}} />
))}
canSave && onSubmit({ name, target: Number(target), icon, color })} style={{
width: '100%', height: 54, borderRadius: 16, background: CDV.brandGrad,
border: 'none', color: '#fff', fontWeight: 600, fontSize: 15, opacity: canSave ? 1 : 0.4,
}}>Criar cofrinho
);
};
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 }) => (
);
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) => (
))}
);
};
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}`}
Cartão
{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) => (
))}
);
};
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 (
setBandeira(b)} style={{
padding: '8px 12px', borderRadius: 99,
background: active ? CDV.brandSoft : CDV.surfaceHi,
border: `1.5px solid ${active ? CDV.brand : CDV.stroke}`,
color: active ? CDV.brand : CDV.textDim,
fontSize: 12, fontWeight: 600, fontFamily: 'inherit',
}}>{bandeiraLabel(b)}
);
})}
Cor do banco
{presets.map(p => (
setCor(p.cor)} title={p.name} style={{
width: 38, height: 38, borderRadius: 12, background: p.cor,
border: cor === p.cor ? `2.5px solid ${CDV.text}` : `2.5px solid transparent`,
cursor: 'pointer',
}} />
))}
canSave && onSubmit({
apelido,
digitos,
bandeira,
limite: Number((limite || '0').replace(',', '.')),
vencimento: vencimento ? Number(vencimento) : null,
cor,
})} style={{
width: '100%', height: 54, borderRadius: 16,
background: CDV.brandGrad, border: 'none', color: '#fff', fontWeight: 600, fontSize: 15,
opacity: canSave ? 1 : 0.4, cursor: canSave ? 'pointer' : 'not-allowed',
}}>Salvar cartão
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 => (
setType(t.id)} style={{
flex: 1, height: 48, borderRadius: 14,
background: type === t.id ? t.c + '22' : CDV.surfaceHi,
border: `1.5px solid ${type === t.id ? t.c : CDV.stroke}`,
color: type === t.id ? t.c : CDV.textDim,
fontSize: 14, fontWeight: 600, fontFamily: 'inherit',
}}>{t.label}
))}
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
Salvar
);
};
Object.assign(window, { FinanceScreen });