// ============================================================
// FINAFAMILY — Onboarding Wizard (5 passos)
// ============================================================
const { useState: useStateOB } = React;
// ── Step progress indicator ────────────────────────────────
const OB_STEPS = [
{ num: 1, label: 'Sua Conta' },
{ num: 2, label: 'Família' },
{ num: 3, label: 'Rendas' },
{ num: 4, label: 'Pagamentos' },
{ num: 5, label: 'Dia a Dia' },
];
function OBProgress({ current }) {
return (
{OB_STEPS.map((s, i) => {
const done = current > s.num;
const active = current === s.num;
return (
{i > 0 && (
)}
{done
?
: {s.num}
}
{s.label}
);
})}
);
}
// ── Step 1: Account ────────────────────────────────────────
function OBStepAccount({ account, set, error }) {
return (
Crie sua conta
Você será o administrador principal da família
set('name', e.target.value)} placeholder="Ex: Carlos Silva" />
set('email', e.target.value)} placeholder="carlos@familia.com" />
set('password', e.target.value)} placeholder="Mínimo 8 caracteres" />
set('confirm', e.target.value)} placeholder="Repita a senha" />
{account.password.length > 0 && (
{[
{ ok: account.password.length >= 8, label: '8+ caracteres' },
{ ok: /[A-Z]/.test(account.password), label: 'Maiúscula' },
{ ok: /[0-9]/.test(account.password), label: 'Número' },
{ ok: account.password === account.confirm && account.confirm.length > 0, label: 'Coincidem' },
].map(r => (
{r.label}
))}
)}
{error && (
{error}
)}
);
}
// ── Step 2: Members ────────────────────────────────────────
function OBStepMembers({ masterMember, extras, setExtras }) {
function addExtra(role) {
setExtras(prev => [...prev, { name: '', role, email: '', password: '', showPass: false, _id: Date.now() }]);
}
function updExtra(idx, k, v) {
setExtras(prev => prev.map((m, i) => i === idx ? { ...m, [k]: v } : m));
}
function delExtra(idx) {
setExtras(prev => prev.filter((_, i) => i !== idx));
}
const hasMaster2 = extras.some(m => m.role === 'MASTER2');
return (
Quem faz parte da família?
Adicione os membros que vão usar o Finafamily
{/* Master 1 — non-removable */}
{masterMember && (
{masterMember.name}
Master 1 — Administrador
)}
{/* Extra members */}
{extras.map((m, idx) => (
{m.role === 'MASTER2' ? '👫 Cônjuge / Master 2' : '👶 Filho(a)'}
delExtra(idx)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-3)', padding: 2 }}>
updExtra(idx, 'name', e.target.value)} placeholder="Nome" />
updExtra(idx, 'email', e.target.value)} placeholder="email@exemplo.com" />
updExtra(idx, 'password', e.target.value)} placeholder="Mínimo 8 caracteres" />
updExtra(idx, 'showPass', !m.showPass)}
style={{ padding: '9px 12px', background: 'var(--surface-3)', border: '1px solid var(--border)', borderRadius: 10, color: 'var(--text-2)', fontSize: 12, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 6, width: '100%', justifyContent: 'center' }}
>
{m.showPass ? 'Ocultar' : 'Mostrar'} senha
{!m.email && !m.password && (
💡 Sem e-mail/senha, o acesso poderá ser configurado depois em Configurações → Membros
)}
))}
{/* Add buttons */}
{!hasMaster2 && (
addExtra('MASTER2')} style={{ flex: 1, padding: '10px', background: 'var(--surface-2)', border: '1px dashed var(--border)', borderRadius: 10, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 12.5, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
Adicionar cônjuge
)}
addExtra('CHILD')} style={{ flex: 1, padding: '10px', background: 'var(--surface-2)', border: '1px dashed var(--border)', borderRadius: 10, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 12.5, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
Adicionar filho(a)
Pode pular — adicione membros depois em Configurações
);
}
// ── Step 3: Incomes ────────────────────────────────────────
function OBStepIncomes({ allMembers, incomes, setIncomes, incomeSources }) {
function addInc(memberId) {
setIncomes(prev => [...prev, { memberId, desc: '', value: '', source: incomeSources[0]?.key || 'salary', recurrence: 'monthly', _id: Date.now() }]);
}
function updInc(idx, k, v) {
setIncomes(prev => prev.map((i, j) => j === idx ? { ...i, [k]: v } : i));
}
function delInc(idx) {
setIncomes(prev => prev.filter((_, i) => i !== idx));
}
return (
Fontes de renda
Cadastre a renda mensal de cada membro
{incomes.map((inc, idx) => {
const member = allMembers.find(m => m.id === inc.memberId);
return (
{member &&
}
{member?.name?.split(' ')[0] || 'Membro'}
delInc(idx)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-3)', padding: 2 }}>
updInc(idx, 'desc', e.target.value)} placeholder="Ex: Salário maio" />
updInc(idx, 'value', e.target.value)} placeholder="0,00" />
updInc(idx, 'source', e.target.value)}>
{incomeSources.map(s => {s.label} )}
updInc(idx, 'recurrence', e.target.value)}>
Mensal
Única
);
})}
{allMembers.map(m => (
addInc(m.id)} style={{ padding: '8px 14px', background: 'var(--surface-2)', border: '1px dashed var(--border)', borderRadius: 20, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 12, display: 'flex', alignItems: 'center', gap: 6 }}>
Renda de {m.name.split(' ')[0]}
))}
Pode pular — cadastre depois em Receitas
);
}
// ── Step 4: Payments ───────────────────────────────────────
function OBStepPayments({ payments, setPayments }) {
function addPM() {
setPayments(prev => [...prev, { name: '', type: 'credit', bank: '', limit: '', closingDay: '', dueDay: '', _id: Date.now() }]);
}
function updPM(idx, k, v) {
setPayments(prev => prev.map((p, i) => i === idx ? { ...p, [k]: v } : p));
}
function delPM(idx) {
setPayments(prev => prev.filter((_, i) => i !== idx));
}
return (
Meios de pagamento
Cartões e contas que você usa no dia a dia
{payments.map((pm, idx) => (
delPM(idx)} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-3)', padding: 2 }}>
updPM(idx, 'name', e.target.value)} placeholder="Ex: Nubank" />
updPM(idx, 'type', e.target.value)}>
Cartão de Crédito
Cartão de Débito
Conta Corrente
Pix
Dinheiro
updPM(idx, 'bank', e.target.value)} placeholder="Ex: Itaú Visa" />
{pm.type === 'credit' && (
updPM(idx, 'limit', e.target.value)} placeholder="0,00" />
)}
{pm.type === 'credit' && (
<>
updPM(idx, 'closingDay', e.target.value)} placeholder="Ex: 5" />
updPM(idx, 'dueDay', e.target.value)} placeholder="Ex: 15" />
>
)}
))}
Adicionar cartão ou conta
Pode pular — adicione depois em Configurações → Pagamentos
);
}
// ── Step 5: Dia a Dia ──────────────────────────────────────
function OBStepDiaDia({ allMembers, diaDiaLimits, setDiaDiaLimits }) {
return (
Limite Dia a Dia
Quanto cada membro pode gastar em pequenas despesas por mês?
{allMembers.map(m => (
{m.name.split(' ')[0]}
{m.role}
R$
setDiaDiaLimits(prev => ({ ...prev, [m.id]: e.target.value }))}
placeholder="300"
style={{ width: 76, padding: '8px 10px', background: 'var(--surface-1)', border: '1px solid var(--border)', borderRadius: 8, color: 'var(--text-1)', fontSize: 14, fontWeight: 700, outline: 'none', textAlign: 'right', boxSizing: 'border-box' }}
/>
/mês
))}
O Dia a Dia é um limite para pequenos gastos não planejados — lanches, corridas de app, compras rápidas. Ideal para manter o controle sem complicar.
);
}
// ── Main Onboarding component ──────────────────────────────
function Onboarding({ onComplete, onGoToLogin }) {
const { register, addMember, addIncome, addPaymentMethod, updateMember, members: ctxMembers, incomeSources, loadData } = useApp();
const [step, setStep] = useStateOB(0); // 0 = welcome, 1-5 = wizard
const [saving, setSaving] = useStateOB(false);
const [error, setError] = useStateOB('');
// Step 1
const [account, setAccount] = useStateOB({ name: '', email: '', password: '', confirm: '' });
function setAcct(k, v) { setAccount(f => ({ ...f, [k]: v })); setError(''); }
// Step 2
const [masterMember, setMasterMember] = useStateOB(null);
const [extras, setExtras] = useStateOB([]);
// Step 3
const [incomes, setIncomes] = useStateOB([]);
// Step 4
const [payments, setPayments] = useStateOB([]);
// Step 5
const [diaDiaLimits, setDiaDiaLimits] = useStateOB({});
// All members after registration
const allMembers = ctxMembers.length > 0
? ctxMembers
: (masterMember ? [masterMember, ...extras.filter(m => m.name.trim()).map((m, i) => ({ ...m, id: 'tmp_' + i }))] : []);
// ── Navigation ─────────────────────────────────────────
async function handleNext() {
setError('');
if (step === 0) { setStep(1); return; }
// ── Step 1: Register ──────────────────────────────────
if (step === 1) {
if (!account.name.trim()) { setError('Informe seu nome completo'); return; }
if (!account.email.trim()) { setError('Informe seu e-mail'); return; }
if (account.password.length < 8) { setError('A senha deve ter no mínimo 8 caracteres'); return; }
if (account.password !== account.confirm) { setError('As senhas não conferem'); return; }
setSaving(true);
try {
const member = await register(account.name.trim(), account.email.trim(), account.password);
setMasterMember(member);
setDiaDiaLimits({ [member.id]: '300' });
setStep(2);
} catch (err) {
// Erros específicos da API
if (err.status === 409) {
setError('Este e-mail já está cadastrado. Volte e faça login.');
} else if (err.status === 400 && err.message) {
setError(err.message);
} else {
setError(err.message || 'Erro ao criar conta. Tente novamente.');
}
} finally {
setSaving(false);
}
return;
}
// ── Step 2: Add members ───────────────────────────────
if (step === 2) {
setSaving(true);
try {
for (const m of extras.filter(m => m.name.trim())) {
const created = await addMember({
name: m.name.trim(),
role: m.role,
email: m.email || undefined,
password: m.password && m.password.length >= 8 ? m.password : undefined,
});
setDiaDiaLimits(prev => ({ ...prev, [created.id]: '200' }));
}
await loadData(new Date().getFullYear(), new Date().getMonth() + 1);
setStep(3);
} catch (err) {
setError(err.message || 'Erro ao salvar membros');
} finally {
setSaving(false);
}
return;
}
// ── Step 3: Incomes ───────────────────────────────────
if (step === 3) {
setSaving(true);
try {
const today = todayLocalISO();
for (const inc of incomes.filter(i => i.desc.trim() && i.value)) {
await addIncome({ desc: inc.desc.trim(), value: parseFloat(inc.value), date: today, source: inc.source || 'salary', recurrence: inc.recurrence || 'monthly', memberId: inc.memberId, status: 'received' });
}
} catch { /* continue */ }
setSaving(false);
setStep(4);
return;
}
// ── Step 4: Payments ──────────────────────────────────
if (step === 4) {
setSaving(true);
try {
for (const pm of payments.filter(p => p.name.trim())) {
await addPaymentMethod({
name: pm.name.trim(), type: pm.type || 'credit', bank: pm.bank || '',
limit: pm.limit ? parseFloat(pm.limit) : undefined,
closingDay: pm.closingDay ? parseInt(pm.closingDay) : undefined,
dueDay: pm.dueDay ? parseInt(pm.dueDay) : undefined,
memberId: masterMember?.id || (ctxMembers[0]?.id || ''),
});
}
} catch { /* continue */ }
setSaving(false);
setStep(5);
return;
}
// ── Step 5: Dia a Dia + Finish ────────────────────────
if (step === 5) {
setSaving(true);
try {
for (const [memberId, limit] of Object.entries(diaDiaLimits)) {
if (limit) await updateMember(memberId, { diaDiaLimit: parseFloat(limit) });
}
await loadData(new Date().getFullYear(), new Date().getMonth() + 1);
onComplete();
} catch {
onComplete();
} finally {
setSaving(false);
}
}
}
function handleBack() {
setError('');
if (step > 1) setStep(s => s - 1);
else if (step === 1) setStep(0);
}
// ── Layout helpers ──────────────────────────────────────
const outerStyle = {
minHeight: '100vh',
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: 'var(--canvas-bg)', padding: '20px',
boxSizing: 'border-box',
};
const cardStyle = {
width: '100%', maxWidth: 520, background: 'var(--card-bg)',
border: '1px solid var(--border)', borderRadius: 18, padding: 28,
boxSizing: 'border-box',
};
// ── STEP 0: Welcome ─────────────────────────────────────
if (step === 0) {
return (
{/* Logo */}
Finafamily .
Suas finanças. Sua família. Em ordem.
Organize receitas, despesas, investimentos e limites de gastos para toda a família — num só lugar.
{/* Feature grid */}
{[
['LayoutDashboard', 'Dashboard familiar', 'Visão completa em tempo real'],
['Wallet', 'Controle de despesas','Compartilhadas e parceladas'],
['TrendingUp', 'Investimentos', 'Acompanhe sua carteira'],
['Zap', 'Dia a Dia', 'Limite de gastos diários'],
].map(([icon, title, desc]) => (
))}
Criar minha conta →
Já tenho conta — Entrar
);
}
// ── Steps 1-5 ──────────────────────────────────────────
return (
{/* Mini header */}
Finafamily .
{/* Step content */}
{step === 1 &&
}
{step === 2 &&
}
{step === 3 &&
}
{step === 4 &&
}
{step === 5 &&
}
{/* General error (non-step-1) */}
{error && step !== 1 && (
{error}
)}
{/* Navigation */}
{step >= 1 && (
← Voltar
)}
{saving
? <> Salvando…>
: step === 5
? <> Concluir e entrar>
: <>Próximo →>
}
{step === 1 && (
Já tem conta? Entrar
)}
);
}
Object.assign(window, { Onboarding });