// ============================================================
// FINAFAMILY — Relatórios, Notificações, Configurações
// ============================================================
const { useState, useMemo, useEffect } = React;
// ── Relatórios ─────────────────────────────────────────────
function Relatorios({ onNavigate }) {
const { expenses, incomes, members, categories, balancete, exportToCSV, currentMonth, currentMember, paymentMethods, fetchMonthExpenses, fetchMonthIncomes } = useApp();
const [toast, setToast] = useState(null);
// Garante que os últimos 6 meses (incluindo o atual) estejam carregados —
// o resto do app só pré-carrega meses vizinhos.
useEffect(() => {
for (let i = 0; i < 6; i++) {
const d = new Date(currentMonth.year, currentMonth.month - 1 - i, 1);
fetchMonthExpenses(d.getFullYear(), d.getMonth() + 1);
fetchMonthIncomes(d.getFullYear(), d.getMonth() + 1);
}
}, [currentMonth.year, currentMonth.month]);
// Histórico real dos últimos 6 meses do membro logado (antes eram números
// fixos de exemplo, com meses fixos "Dez…Mai"). Despesa = regra canônica do
// mês (o que sai do bolso dele), receita = receitas dele no mês.
const MONTHS_SHORT_PT = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'];
const history = useMemo(() => Array.from({ length: 6 }, (_, idx) => {
const d = new Date(currentMonth.year, currentMonth.month - 1 - (5 - idx), 1);
const y = d.getFullYear(), m = d.getMonth() + 1;
const exp = monthExpensesTotal(expenses, currentMember?.id, paymentMethods, y, m, 0);
const inc = incomes.filter(i => {
const di = parseLocalDate(i.date);
return i.memberId === currentMember?.id && di.getFullYear() === y && di.getMonth() + 1 === m;
}).reduce((s, i) => s + (Number(i.value) || 0), 0);
return { label: MONTHS_SHORT_PT[m - 1], exp, inc };
}), [expenses, incomes, currentMonth.year, currentMonth.month, currentMember?.id, paymentMethods]);
const last6Months = history.map(h => h.label);
const expenseHistory = history.map(h => h.exp);
const incomeHistory = history.map(h => h.inc);
const balanceHistory = incomeHistory.map((v, i) => v - expenseHistory[i]);
const maxBar = Math.max(...expenseHistory, ...incomeHistory, 1);
// Janela dos últimos 6 meses (incluindo o mês em exibição), início/fim inclusive.
const sixMonthWindow = useMemo(() => {
const start = new Date(currentMonth.year, currentMonth.month - 1 - 5, 1);
const end = new Date(currentMonth.year, currentMonth.month, 0); // último dia do mês atual
return { start, end };
}, [currentMonth.year, currentMonth.month]);
const expensesInWindow = useMemo(() => expenses.filter(e => {
const d = parseLocalDate(e.date);
return d >= sixMonthWindow.start && d <= sixMonthWindow.end;
}), [expenses, sixMonthWindow]);
// Ranking por categoria: total do mês em exibição + média mensal dos últimos 6 meses.
const catRanking = categories.map(cat => {
const inMonth = expenses.filter(e =>
e.categoryId === cat.id
&& parseLocalDate(e.date).getFullYear() === currentMonth.year
&& parseLocalDate(e.date).getMonth() + 1 === currentMonth.month
);
const inWindow = expensesInWindow.filter(e => e.categoryId === cat.id);
return {
...cat,
total: inMonth.reduce((s, e) => s + e.value, 0),
avg6m: inWindow.reduce((s, e) => s + e.value, 0) / 6,
};
}).filter(c => c.total > 0 || c.avg6m > 0).sort((a, b) => b.total - a.total);
const maxCat = catRanking[0]?.total || 1;
return (
{toast &&
setToast(null)} />}
{/* Export header */}
{
const inMonth = expenses.filter(e => {
const d = parseLocalDate(e.date);
return !e.isProjected && d.getFullYear() === currentMonth.year && d.getMonth() + 1 === currentMonth.month;
});
const data = inMonth.map(e => ({
data: parseLocalDate(e.date).toLocaleDateString('pt-BR'),
descricao: e.desc,
categoria: categories.find(c => c.id === e.categoryId)?.name || '',
valor: e.value.toFixed(2),
status: e.status,
tipo: e.type,
}));
exportToCSV(data, `relatorio-despesas-${currentMonth.year}-${String(currentMonth.month).padStart(2, '0')}.csv`);
setToast('Relatório exportado!');
}} style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '9px 14px', background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 10, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 13 }}>
Exportar CSV
{/* Income vs Expense comparison */}
{last6Months.map((month, i) => (
))}
{/* Balance evolution */}
Saldo Familiar Mensal
{(() => {
const W2 = 300, H2 = 100;
const minV = Math.min(...balanceHistory), maxV = Math.max(...balanceHistory);
const range = maxV - minV || 1;
const pts = balanceHistory.map((v, i) => {
const x = (i / (balanceHistory.length - 1)) * W2;
const y = H2 - ((v - minV) / range) * (H2 - 16) - 8;
return `${x},${y}`;
}).join(' ');
const areaPoints = `0,${H2} ` + pts + ` ${W2},${H2}`;
return (
{balanceHistory.map((v, i) => {
const x = (i / (balanceHistory.length - 1)) * W2;
const y = H2 - ((v - minV) / range) * (H2 - 16) - 8;
return ;
})}
{last6Months.map((m, i) => {
const x = (i / (last6Months.length - 1)) * W2;
return {m} ;
})}
);
})()}
{/* Category ranking */}
Ranking de Gastos por Categoria
Total no mês em exibição · média mensal nos últimos 6 meses
{catRanking.map((cat, i) => (
#{i + 1}
{cat.name}
{fmtBRL(cat.total)}
média 6m: {fmtBRL(cat.avg6m)}
))}
{/* Shared expenses report */}
Quem Deve a Quem (Balancete)
{balancete.length === 0 ? (
) : (
balancete.map((item, i) => {
const fromM = members.find(m => m.id === item.from);
const toM = members.find(m => m.id === item.to);
if (!fromM || !toM) return null;
return (
{fromM.name.split(' ')[0]} deve a {toM.name.split(' ')[0]}
Saldo líquido consolidado
{fmtBRL(item.amount)}
);
})
)}
{ if (window.__openBalancete) window.__openBalancete(); }} style={{ width: '100%', marginTop: 12, padding: '8px', background: 'var(--accent-tint)', border: 'none', borderRadius: 8, color: 'var(--accent)', fontSize: 12, fontWeight: 600, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
Abrir Balancete Completo
);
}
// ── Notificações ───────────────────────────────────────────
function Notificacoes() {
const { notifications, markNotificationRead, markAllRead, unreadCount, notifPrefs, setNotifPrefs } = useApp();
const typeIcon = { warning: 'AlertTriangle', alert: 'Bell', info: 'Info' };
const typeColor = { warning: '#F59E0B', alert: '#EF4444', info: '#3B82F6' };
const [showPrefs, setShowPrefs] = useState(false);
const ALERT_TYPES = [
{ key: 'cardDue', icon: 'CreditCard', label: 'Cartão próximo ao vencimento', desc: 'Aviso 3 dias antes do vencimento da fatura' },
{ key: 'installmentDue', icon: 'CalendarClock', label: 'Parcelas vencendo em 7 dias', desc: 'Alerta de parcelas a vencer na semana' },
{ key: 'diaDia80', icon: 'Zap', label: 'Dia a Dia acima de 80%', desc: 'Quando o limite mensal estiver quase esgotado' },
{ key: 'investmentStale', icon: 'TrendingUp', label: 'Investimento sem atualização', desc: 'Investimento parado há mais de 30 dias' },
];
function togglePref(key) {
setNotifPrefs({ ...notifPrefs, [key]: !notifPrefs[key] });
}
function Toggle({ on, onToggle }) {
return (
);
}
return (
{/* Header */}
{unreadCount} não lida{unreadCount !== 1 ? 's' : ''}
{unreadCount > 0 && (
Marcar todas como lidas
)}
setShowPrefs(v => !v)} style={{ fontSize: 12, color: ffReadableColor(showPrefs ? 'var(--accent)' : 'var(--text-3)'), background: showPrefs ? 'var(--accent-tint)' : 'var(--surface-2)', border: 'none', borderRadius: 8, padding: '6px 12px', cursor: 'pointer', fontWeight: 600, display: 'flex', alignItems: 'center', gap: 5 }}>
Configurar alertas
{/* Alert preferences panel */}
{showPrefs && (
Quais alertas você quer receber
{ALERT_TYPES.map((a, i) => (
))}
Preferências salvas automaticamente para este dispositivo
)}
{/* Notification list */}
{notifications.length === 0 &&
}
{notifications.map(n => (
markNotificationRead(n.id)} style={{
display: 'flex', gap: 14, padding: '16px 18px',
background: 'var(--surface-1)',
border: `1px solid ${n.read ? 'var(--border)' : typeColor[n.type] + '44'}`,
borderRadius: 14, cursor: 'pointer',
opacity: n.read ? 0.6 : 1,
transition: 'opacity 200ms, border-color 200ms',
}}>
{n.desc}
{new Date(n.date).toLocaleDateString('pt-BR')}
))}
);
}
// ── Shared helpers for config lists ───────────────────────
const PRESET_COLORS = ['#EF4444','#F97316','#F59E0B','#10B981','#0D9488','#3B82F6','#6366F1','#8B5CF6','#EC4899','#94A3B8'];
const PRESET_ICONS = ['ShoppingCart','Home','Car','Utensils','Heart','Briefcase','GraduationCap','Plane','Music','Smartphone','Shirt','Baby','Gamepad2','Coffee','Dumbbell','PawPrint','Book','Gift'];
function ColorDot({ color, selected, onClick }) {
return (
);
}
function IconGrid({ selected, onSelect }) {
return (
{PRESET_ICONS.map(ic => (
onSelect(ic)} style={{
width: 34, height: 34, borderRadius: 9, cursor: 'pointer',
background: selected === ic ? 'var(--action-bg)' : 'var(--surface-3)',
border: selected === ic ? '2px solid var(--accent)' : '1.5px solid var(--border)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: ffReadableColor(selected === ic ? '#FFFFFF' : 'var(--text-2)'),
transition: 'all 120ms',
boxShadow: selected === ic ? '0 0 0 3px var(--accent-tint)' : 'none',
}}
onMouseEnter={e => { if (selected !== ic) { e.currentTarget.style.background = 'var(--accent-tint)'; e.currentTarget.style.color = 'var(--accent)'; e.currentTarget.style.borderColor = 'var(--accent)'; } }}
onMouseLeave={e => { if (selected !== ic) { e.currentTarget.style.background = 'var(--surface-3)'; e.currentTarget.style.color = 'var(--text-2)'; e.currentTarget.style.borderColor = 'var(--border)'; } }}
>
))}
);
}
// ── Configurações ─────────────────────────────────────────
function Configuracoes() {
const {
currentMember, members,
categories, addCategory, updateCategory, deleteCategory,
paymentMethods, addPaymentMethod, updatePaymentMethod, deletePaymentMethod,
investmentTypes, setInvestmentTypes,
investmentPurposes, setInvestmentPurposes,
incomeSources, setIncomeSources,
updateEmail, updatePassword, deleteAccount,
addMember, updateMember,
} = useApp();
const { theme, setTheme } = useTheme();
const [activeTab, setActiveTab] = useState('perfil');
const [diaDiaVal, setDiaDiaVal] = useState(currentMember.diaDiaLimit);
const [notifyCardDueDaysVal, setNotifyCardDueDaysVal] = useState(currentMember.notifyCardDueDays ?? 3);
const [notifyInstallmentDueDaysVal, setNotifyInstallmentDueDaysVal] = useState(currentMember.notifyInstallmentDueDays ?? 7);
const [profileName, setProfileName] = useState(currentMember.name);
const [profileSaving, setProfileSaving] = useState(false);
const [toast, setToast] = useState(null);
const [toastType, setToastType] = useState('success');
function showToast(msg, type = 'success') { setToast(msg); setToastType(type); }
async function handleSaveProfile() {
setProfileSaving(true);
try {
await updateMember(currentMember.id, {
name: profileName.trim() || currentMember.name,
diaDiaLimit: parseFloat(diaDiaVal) || currentMember.diaDiaLimit,
notifyCardDueDays: parseInt(notifyCardDueDaysVal, 10) >= 0 ? parseInt(notifyCardDueDaysVal, 10) : currentMember.notifyCardDueDays,
notifyInstallmentDueDays: parseInt(notifyInstallmentDueDaysVal, 10) >= 0 ? parseInt(notifyInstallmentDueDaysVal, 10) : currentMember.notifyInstallmentDueDays,
});
showToast('Perfil salvo com sucesso!');
} catch {
showToast('Erro ao salvar perfil. Tente novamente.', 'error');
} finally {
setProfileSaving(false);
}
}
// ── Category state ──────────────────────────────────────
const [editingCatId, setEditingCatId] = useState(null);
const [editCatForm, setEditCatForm] = useState({});
const [showNewCat, setShowNewCat] = useState(false);
const [newCat, setNewCat] = useState({ name: '', color: PRESET_COLORS[0], icon: 'ShoppingCart' });
// ── Investment types state ──────────────────────────────
const [newInvType, setNewInvType] = useState('');
// ── Investment purposes state ───────────────────────────
const [newPurpose, setNewPurpose] = useState({ label: '', color: PRESET_COLORS[0] });
const [showNewPurpose, setShowNewPurpose] = useState(false);
// ── Income sources state ────────────────────────────────
const [newSource, setNewSource] = useState('');
// ── Payment method state ────────────────────────────────
const [showNewPM, setShowNewPM] = useState(false);
const [newPM, setNewPM] = useState({ name: '', type: 'credit', bank: '', limit: '', closingDay: '', dueDay: '', color: '#0D9488' });
const [editingPM, setEditingPM] = useState(null); // id do cartão em edição
const [editPMForm, setEditPMForm] = useState({});
const [pmDeleteConfirm, setPmDeleteConfirm] = useState(null);
// ── Members state ───────────────────────────────────────
const [showNewMember, setShowNewMember] = useState(false);
const [newMember, setNewMember] = useState({ name: '', email: '', password: '', role: 'MASTER2' });
const [memberSaving, setMemberSaving] = useState(false);
const [memberErr, setMemberErr] = useState(null);
const [editingMemberId, setEditingMemberId] = useState(null);
const [editMemberForm, setEditMemberForm] = useState({});
// ── Security state ──────────────────────────────────────
const [emailForm, setEmailForm] = useState({ newEmail: '', password: '' });
const [emailErr, setEmailErr] = useState(null);
const [emailLoading, setEmailLoading] = useState(false);
const [passForm, setPassForm] = useState({ current: '', next: '', confirm: '' });
const [passErr, setPassErr] = useState(null);
const [passLoading, setPassLoading] = useState(false);
const [deleteAccountConfirm, setDeleteAccountConfirm] = useState(false);
async function handleChangeEmail() {
setEmailErr(null);
if (!emailForm.newEmail.trim() || !/\S+@\S+\.\S+/.test(emailForm.newEmail)) {
setEmailErr('Digite um e-mail válido.'); return;
}
if (!emailForm.password) { setEmailErr('Digite sua senha atual para confirmar.'); return; }
setEmailLoading(true);
try {
await updateEmail(emailForm.newEmail.trim(), emailForm.password);
setEmailForm({ newEmail: '', password: '' });
showToast('E-mail atualizado com sucesso!');
} catch (err) {
setEmailErr(err.message || 'Não foi possível alterar o e-mail. Verifique sua senha.');
} finally {
setEmailLoading(false);
}
}
async function handleChangePassword() {
setPassErr(null);
if (!passForm.current) { setPassErr('Digite sua senha atual.'); return; }
if (passForm.next.length < 8) { setPassErr('A nova senha deve ter no mínimo 8 caracteres.'); return; }
if (passForm.next !== passForm.confirm) { setPassErr('As senhas não coincidem.'); return; }
setPassLoading(true);
try {
await updatePassword(passForm.current, passForm.next);
setPassForm({ current: '', next: '', confirm: '' });
showToast('Senha alterada com sucesso!');
} catch (err) {
setPassErr(err.message || 'Senha atual incorreta. Tente novamente.');
} finally {
setPassLoading(false);
}
}
const tabs = [
{ id: 'perfil', label: 'Perfil', icon: 'User' },
{ id: 'seguranca', label: 'Segurança', icon: 'ShieldCheck' },
{ id: 'membros', label: 'Membros', icon: 'Users' },
{ id: 'categorias', label: 'Categorias', icon: 'Tag' },
{ id: 'pagamentos', label: 'Pagamentos', icon: 'CreditCard' },
{ id: 'investimentos', label: 'Investimentos', icon: 'TrendingUp' },
{ id: 'receitas', label: 'Fontes de Receita', icon: 'DollarSign' },
{ id: 'aparencia', label: 'Aparência', icon: 'Sun' },
];
// ── Section title helper ────────────────────────────────
function SectionTitle({ children }) {
return {children}
;
}
function Divider() {
return
;
}
// ── Inline list item ────────────────────────────────────
function ListItem({ left, right }) {
return (
{left}
{right}
);
}
// ── Delete button ───────────────────────────────────────
function DelBtn({ onClick }) {
return (
e.currentTarget.style.color = '#EF4444'}
onMouseLeave={e => e.currentTarget.style.color = 'var(--text-3)'}
>
);
}
return (
{toast &&
setToast(null)} />}
{/* Sidebar */}
{tabs.map(tab => (
setActiveTab(tab.id)} style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '10px 14px', borderRadius: 10, border: 'none', cursor: 'pointer',
background: activeTab === tab.id ? 'var(--accent-tint)' : 'transparent',
color: ffReadableColor(activeTab === tab.id ? 'var(--accent)' : 'var(--text-2)'),
fontWeight: activeTab === tab.id ? 600 : 500, fontSize: 13.5, textAlign: 'left', width: '100%',
}}>
{tab.label}
))}
{/* Content */}
{/* ── PERFIL ── */}
{activeTab === 'perfil' && (
Meu Perfil
{currentMember.isLocalAccount && (
Conta local — os dados são salvos apenas neste dispositivo. O registro de novas famílias via app está sendo implantado no servidor.
)}
{currentMember.name}
{currentMember.role}
{currentMember.email}
)}
{/* ── MEMBROS ── */}
{activeTab === 'membros' && (
Membros da Família
{ setShowNewMember(true); setMemberErr(null); setNewMember({ name: '', email: '', role: 'MASTER2' }); }}
style={{ display: 'flex', alignItems: 'center', gap: 5, padding: '7px 12px', background: "var(--action-bg)", border: 'none', borderRadius: 8, color: 'white', fontWeight: 600, fontSize: 12, cursor: 'pointer' }}>
Adicionar membro
{/* Formulário para novo membro */}
{showNewMember && (
Novo Membro
{memberErr && (
{memberErr}
)}
setNewMember(f => ({ ...f, name: e.target.value }))} placeholder="Nome do membro" />
setNewMember(f => ({ ...f, role: e.target.value }))}>
👫 Cônjuge / Parceiro(a)
👶 Filho(a) / Dependente
setNewMember(f => ({ ...f, email: e.target.value }))} placeholder="email@exemplo.com" />
{newMember.email.trim() && (
setNewMember(f => ({ ...f, password: e.target.value }))} placeholder="Mínimo 8 caracteres" />
)}
{ setShowNewMember(false); setMemberErr(null); }}
style={{ flex: 1, padding: '9px', background: 'var(--surface-3)', border: '1px solid var(--border)', borderRadius: 8, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 13 }}>
Cancelar
{
setMemberErr(null);
if (!newMember.name.trim()) { setMemberErr('Informe o nome do membro.'); return; }
if (newMember.email.trim() && newMember.password.length > 0 && newMember.password.length < 8) {
setMemberErr('A senha deve ter pelo menos 8 caracteres.'); return;
}
setMemberSaving(true);
try {
await addMember({
name: newMember.name.trim(),
role: newMember.role,
email: newMember.email.trim() || undefined,
password: newMember.email.trim() && newMember.password ? newMember.password : undefined,
});
setShowNewMember(false);
setNewMember({ name: '', email: '', password: '', role: 'MASTER2' });
showToast('Membro adicionado com sucesso!');
} catch (err) {
setMemberErr(err.message || 'Erro ao adicionar membro.');
} finally {
setMemberSaving(false);
}
}} style={{ flex: 2, padding: '9px', background: memberSaving ? 'var(--surface-3)' : "var(--action-bg)", border: 'none', borderRadius: 8, color: 'white', fontWeight: 700, cursor: memberSaving ? 'default' : 'pointer', fontSize: 13, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6 }}>
{memberSaving
? <> Salvando...>
: <> Adicionar>
}
)}
{/* Lista de membros */}
{members.map(m => {
const isMe = m.id === currentMember.id;
const isEditing = editingMemberId === m.id;
return (
{m.name}
{m.role}
{m.email && <>• {m.email} >}
{isMe && Você }
{
if (isEditing) { setEditingMemberId(null); }
else { setEditingMemberId(m.id); setEditMemberForm({ name: m.name, diaDiaLimit: String(m.diaDiaLimit || 300) }); }
}} style={{ background: 'var(--surface-3)', border: '1px solid var(--border)', borderRadius: 7, padding: '5px 8px', cursor: 'pointer', color: 'var(--text-2)', display: 'flex', alignItems: 'center', gap: 4, fontSize: 12 }}>
{/* Edição inline */}
{isEditing && (
setEditMemberForm(f => ({ ...f, name: e.target.value }))} />
{isMe && (
setEditMemberForm(f => ({ ...f, diaDiaLimit: e.target.value }))} />
)}
setEditingMemberId(null)}
style={{ flex: 1, padding: '8px', background: 'var(--surface-3)', border: '1px solid var(--border)', borderRadius: 8, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 12 }}>
Cancelar
{
await updateMember(m.id, {
name: editMemberForm.name.trim(),
...(isMe ? { diaDiaLimit: parseFloat(editMemberForm.diaDiaLimit) || m.diaDiaLimit } : {}),
});
setEditingMemberId(null);
showToast('Membro atualizado!');
}} style={{ flex: 2, padding: '8px', background: "var(--action-bg)", border: 'none', borderRadius: 8, color: 'white', fontWeight: 700, cursor: 'pointer', fontSize: 12, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 5 }}>
Salvar
)}
);
})}
{/* Info sobre login dos membros */}
Acesso dos membros: a credencial é definida somente na criação e depois administrada pelo próprio titular. Um master nunca redefine a senha de outra pessoa.
)}
{/* ── CATEGORIAS ── */}
{activeTab === 'categorias' && (
Categorias de Despesa
{ setShowNewCat(true); setEditingCatId(null); }} style={{ display: 'flex', alignItems: 'center', gap: 5, padding: '7px 12px', background: "var(--action-bg)", border: 'none', borderRadius: 8, color: 'white', fontWeight: 600, fontSize: 12, cursor: 'pointer' }}>
Nova Categoria
{/* New category form */}
{showNewCat && (
Nova Categoria
setNewCat(c => ({ ...c, name: e.target.value }))} placeholder="Ex: Saúde" />
Cor
{PRESET_COLORS.map(c => setNewCat(f => ({ ...f, color: c }))} />)}
Ícone
setNewCat(f => ({ ...f, icon: ic }))} />
setShowNewCat(false)} style={{ flex: 1, padding: '9px', background: 'var(--surface-3)', border: 'none', borderRadius: 8, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 13 }}>Cancelar
{
if (!newCat.name.trim()) return;
addCategory(newCat);
setNewCat({ name: '', color: PRESET_COLORS[0], icon: 'ShoppingCart' });
setShowNewCat(false);
setToast('Categoria criada!');
}} style={{ flex: 2, padding: '9px', background: "var(--action-bg)", border: 'none', borderRadius: 8, color: 'white', fontWeight: 700, cursor: 'pointer', fontSize: 13 }}>Salvar
)}
{/* Category list */}
{categories.map(cat => (
{editingCatId === cat.id ? (
setEditCatForm(f => ({ ...f, name: e.target.value }))} />
Cor
{PRESET_COLORS.map(c => setEditCatForm(f => ({ ...f, color: c }))} />)}
Ícone
setEditCatForm(f => ({ ...f, icon: ic }))} />
setEditingCatId(null)} style={{ flex: 1, padding: '8px', background: 'var(--surface-3)', border: 'none', borderRadius: 8, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 13 }}>Cancelar
{ updateCategory(cat.id, editCatForm); setEditingCatId(null); setToast('Categoria atualizada!'); }} style={{ flex: 2, padding: '8px', background: "var(--action-bg)", border: 'none', borderRadius: 8, color: 'white', fontWeight: 700, cursor: 'pointer', fontSize: 13 }}>Salvar
) : (
{cat.name}
{ setEditingCatId(cat.id); setEditCatForm({ name: cat.name, color: cat.color, icon: cat.icon }); setShowNewCat(false); }} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-3)', padding: 4, borderRadius: 6, display: 'flex', flexShrink: 0 }}
onMouseEnter={e => e.currentTarget.style.color = 'var(--accent)'}
onMouseLeave={e => e.currentTarget.style.color = 'var(--text-3)'}
>
{ deleteCategory(cat.id); setToast('Categoria removida!'); }} />
)}
))}
)}
{/* ── PAGAMENTOS ── */}
{activeTab === 'pagamentos' && (
Meios de Pagamento
setShowNewPM(v => !v)} style={{ display: 'flex', alignItems: 'center', gap: 5, padding: '7px 12px', background: "var(--action-bg)", border: 'none', borderRadius: 8, color: 'white', fontWeight: 600, fontSize: 12, cursor: 'pointer' }}>
Adicionar
{/* Formulário de criação */}
{showNewPM && (
Novo Meio de Pagamento
setNewPM(f => ({ ...f, name: e.target.value }))} placeholder="Ex: Nubank" />
setNewPM(f => ({ ...f, type: e.target.value }))}>
Cartão de Crédito
Cartão de Débito
Conta Corrente
Pix
Dinheiro
setNewPM(f => ({ ...f, bank: e.target.value }))} placeholder="Ex: Itaú Visa" />
{newPM.type === 'credit' && (
setNewPM(f => ({ ...f, limit: e.target.value }))} placeholder="0,00" />
)}
{newPM.type === 'credit' && (
setNewPM(f => ({ ...f, closingDay: e.target.value }))} placeholder="Ex: 5" />
setNewPM(f => ({ ...f, dueDay: e.target.value }))} placeholder="Ex: 15" />
)}
{/* Cor */}
setShowNewPM(false)} style={{ flex: 1, padding: '9px', background: 'var(--surface-3)', border: 'none', borderRadius: 8, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 13 }}>Cancelar
{
if (!newPM.name.trim()) return;
addPaymentMethod({ ...newPM, color: newPM.color || '#0D9488', limit: newPM.limit ? parseFloat(newPM.limit) : undefined, closingDay: newPM.closingDay ? parseInt(newPM.closingDay) : undefined, dueDay: newPM.dueDay ? parseInt(newPM.dueDay) : undefined });
setNewPM({ name: '', type: 'credit', bank: '', limit: '', closingDay: '', dueDay: '', color: '#0D9488' });
setShowNewPM(false);
setToast('Meio de pagamento adicionado!');
}} style={{ flex: 2, padding: '9px', background: "var(--action-bg)", border: 'none', borderRadius: 8, color: 'white', fontWeight: 700, cursor: 'pointer', fontSize: 13 }}>Salvar
)}
{/* Grid de cards */}
{paymentMethods.map(pm => {
const TYPE_ICON = { credit: 'CreditCard', debit: 'CreditCard', account: 'Building2', pix: 'Zap', cash: 'Banknote' };
const TYPE_LABEL = { credit: 'Crédito', debit: 'Débito', account: 'Conta', pix: 'Pix', cash: 'Dinheiro' };
const member = members.find(m => m.id === pm.memberId);
const isEditing = editingPM === pm.id;
const color = pm.color || '#0D9488';
return (
{/* Cabeçalho colorido estilo cartão */}
{!pmDeleteConfirm && !isEditing && (
{ setEditingPM(pm.id); setEditPMForm({ name: pm.name, type: pm.type, bank: pm.bank || '', limit: pm.limit ? String(pm.limit) : '', closingDay: pm.closingDay ? String(pm.closingDay) : '', dueDay: pm.dueDay ? String(pm.dueDay) : '', color: pm.color || '#0D9488' }); }}
style={{ background: 'rgba(255,255,255,0.2)', border: 'none', borderRadius: 6, padding: '5px 7px', cursor: 'pointer', color: 'white', display: 'flex', alignItems: 'center' }}
>
setPmDeleteConfirm(pm.id)}
style={{ background: 'rgba(239,68,68,0.3)', border: 'none', borderRadius: 6, padding: '5px 7px', cursor: 'pointer', color: 'white', display: 'flex', alignItems: 'center' }}
>
)}
{pmDeleteConfirm === pm.id && (
Excluir?
{ deletePaymentMethod(pm.id); setPmDeleteConfirm(null); setToast('Removido.'); }} style={{ padding: '3px 10px', background: "var(--danger-action)", border: 'none', borderRadius: 6, color: 'white', fontWeight: 700, cursor: 'pointer', fontSize: 12 }}>Sim
setPmDeleteConfirm(null)} style={{ padding: '3px 8px', background: 'rgba(255,255,255,0.25)', border: 'none', borderRadius: 6, color: 'white', cursor: 'pointer', fontSize: 12 }}>Não
)}
{pm.name}
{TYPE_LABEL[pm.type] || pm.type}
{pm.bank && <>· {pm.bank} >}
{member && <>· {member.name.split(' ')[0]} >}
{/* Rodapé do card */}
{pm.limit ? (
Limite
{fmtBRL(pm.limit)}
) :
}
{pm.closingDay &&
Fecha dia {pm.closingDay}
}
{pm.dueDay &&
Vence dia {pm.dueDay}
}
{/* Formulário de edição inline */}
{isEditing && (
setEditPMForm(f => ({ ...f, name: e.target.value }))} />
setEditPMForm(f => ({ ...f, type: e.target.value }))}>
Cartão de Crédito
Cartão de Débito
Conta Corrente
Pix
Dinheiro
setEditPMForm(f => ({ ...f, bank: e.target.value }))} placeholder="Ex: Itaú Visa" />
{editPMForm.type === 'credit' && (
setEditPMForm(f => ({ ...f, limit: e.target.value }))} placeholder="0,00" />
)}
{editPMForm.type === 'credit' && (
setEditPMForm(f => ({ ...f, closingDay: e.target.value }))} placeholder="Ex: 5" />
setEditPMForm(f => ({ ...f, dueDay: e.target.value }))} placeholder="Ex: 15" />
)}
{/* Cor na edição */}
setEditingPM(null)} style={{ flex: 1, padding: '8px', background: 'var(--surface-3)', border: 'none', borderRadius: 8, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 13 }}>Cancelar
{
if (!editPMForm.name.trim()) return;
updatePaymentMethod(pm.id, {
name: editPMForm.name.trim(),
type: editPMForm.type,
bank: editPMForm.bank.trim() || undefined,
color: editPMForm.color || '#0D9488',
limit: editPMForm.limit ? parseFloat(editPMForm.limit) : undefined,
closingDay: editPMForm.closingDay ? parseInt(editPMForm.closingDay) : undefined,
dueDay: editPMForm.dueDay ? parseInt(editPMForm.dueDay) : undefined,
});
setEditingPM(null);
setToast('Meio de pagamento atualizado!');
}} style={{ flex: 2, padding: '8px', background: 'var(--action-bg)', border: 'none', borderRadius: 8, color: 'white', fontWeight: 700, cursor: 'pointer', fontSize: 13 }}>Salvar
)}
);
})}
)}
{/* ── INVESTIMENTOS config ── */}
{activeTab === 'investimentos' && (
{/* Types */}
Tipos de Investimento
{investmentTypes.map((t, i) => (
{t}
{ setInvestmentTypes(investmentTypes.filter((_, j) => j !== i)); setToast('Tipo removido!'); }} style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-3)', padding: 0, display: 'flex' }}
onMouseEnter={e => e.currentTarget.style.color = '#EF4444'}
onMouseLeave={e => e.currentTarget.style.color = 'var(--text-3)'}
>
))}
setNewInvType(e.target.value)} placeholder="Novo tipo (Ex: Debêntures)" onKeyDown={e => {
if (e.key === 'Enter' && newInvType.trim()) {
setInvestmentTypes([...investmentTypes, newInvType.trim()]);
setNewInvType('');
setToast('Tipo adicionado!');
}
}} />
{
if (!newInvType.trim()) return;
setInvestmentTypes([...investmentTypes, newInvType.trim()]);
setNewInvType('');
setToast('Tipo adicionado!');
}} style={{ padding: '0 16px', background: "var(--action-bg)", border: 'none', borderRadius: 10, color: 'white', fontWeight: 700, cursor: 'pointer', fontSize: 13, flexShrink: 0, display: 'flex', alignItems: 'center', gap: 5 }}>
Adicionar
{/* Purposes */}
Finalidades
setShowNewPurpose(v => !v)} style={{ display: 'flex', alignItems: 'center', gap: 5, padding: '7px 12px', background: "var(--action-bg)", border: 'none', borderRadius: 8, color: 'white', fontWeight: 600, fontSize: 12, cursor: 'pointer' }}>
Nova Finalidade
{showNewPurpose && (
setNewPurpose(f => ({ ...f, label: e.target.value }))} placeholder="Ex: Fundo Casamento" />
Cor
{PRESET_COLORS.map(c => setNewPurpose(f => ({ ...f, color: c }))} />)}
setShowNewPurpose(false)} style={{ flex: 1, padding: '8px', background: 'var(--surface-3)', border: 'none', borderRadius: 8, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 13 }}>Cancelar
{
if (!newPurpose.label.trim()) return;
const key = newPurpose.label.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '');
setInvestmentPurposes([...investmentPurposes, { key, label: newPurpose.label.trim(), color: newPurpose.color }]);
setNewPurpose({ label: '', color: PRESET_COLORS[0] });
setShowNewPurpose(false);
setToast('Finalidade adicionada!');
}} style={{ flex: 2, padding: '8px', background: "var(--action-bg)", border: 'none', borderRadius: 8, color: 'white', fontWeight: 700, cursor: 'pointer', fontSize: 13 }}>Salvar
)}
{investmentPurposes.map((p, i) => (
{p.label}
{ setInvestmentPurposes(investmentPurposes.filter((_, j) => j !== i)); setToast('Finalidade removida!'); }} />
))}
)}
{/* ── RECEITAS (fontes) ── */}
{activeTab === 'receitas' && (
Fontes de Receita
{incomeSources.map((s, i) => (
{s.label}
{ setIncomeSources(incomeSources.filter((_, j) => j !== i)); setToast('Fonte removida!'); }} />
))}
setNewSource(e.target.value)} placeholder="Nova fonte (Ex: Pensão)" onKeyDown={e => {
if (e.key === 'Enter' && newSource.trim()) {
const key = newSource.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '');
setIncomeSources([...incomeSources, { key, label: newSource.trim() }]);
setNewSource('');
setToast('Fonte adicionada!');
}
}} />
{
if (!newSource.trim()) return;
const key = newSource.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, '');
setIncomeSources([...incomeSources, { key, label: newSource.trim() }]);
setNewSource('');
setToast('Fonte adicionada!');
}} style={{ padding: '0 16px', background: "var(--action-bg)", border: 'none', borderRadius: 10, color: 'white', fontWeight: 700, cursor: 'pointer', fontSize: 13, flexShrink: 0, display: 'flex', alignItems: 'center', gap: 5 }}>
Adicionar
)}
{/* ── SEGURANÇA ── */}
{activeTab === 'seguranca' && (
{/* Alterar E-mail */}
Alterar E-mail
Atual: {currentMember.email}
{/* Alterar Senha */}
Alterar Senha
Use no mínimo 8 caracteres com letras e números
{passErr && (
{passErr}
)}
setPassForm(f => ({ ...f, current: e.target.value }))} placeholder="••••••••" />
setPassForm(f => ({ ...f, next: e.target.value }))} placeholder="Mínimo 8 caracteres" />
setPassForm(f => ({ ...f, confirm: e.target.value }))} placeholder="Repita a nova senha" />
{passForm.next.length > 0 && (
{[
{ ok: passForm.next.length >= 8, label: '8+ chars' },
{ ok: /[A-Z]/.test(passForm.next), label: 'Maiúscula' },
{ ok: /[0-9]/.test(passForm.next), label: 'Número' },
{ ok: passForm.next === passForm.confirm && passForm.confirm.length > 0, label: 'Coincidem' },
].map(r => (
{r.label}
))}
)}
{passLoading ? <> Salvando...> : <> Alterar Senha>}
{/* Informações LGPD */}
Privacidade & LGPD
Lei 13.709/2018 — Lei Geral de Proteção de Dados
{[
{ icon: 'Database', title: 'Dados coletados', text: 'Nome, e-mail, dados financeiros inseridos por você. Nenhum dado bancário real é acessado automaticamente.' },
{ icon: 'Lock', title: 'Segurança', text: 'Senhas protegidas com criptografia. Tokens JWT com expiração. Toda comunicação via HTTPS.' },
{ icon: 'Eye', title: 'Sem compartilhamento', text: 'Seus dados NÃO são vendidos, compartilhados ou usados para publicidade.' },
{ icon: 'Settings', title: 'Seus direitos', text: 'Você pode corrigir seus dados (aba Perfil), exportar via CSV (Relatórios) ou excluir permanentemente sua conta abaixo (LGPD art. 18).' },
].map(item => (
))}
{/* Exclusão de Conta */}
Excluir Conta
Direito à eliminação de dados — LGPD art. 18, VI
Ao excluir sua conta, todos os seus dados financeiros serão permanentemente removidos. Esta ação não pode ser desfeita.
{!deleteAccountConfirm ? (
setDeleteAccountConfirm(true)} style={{ padding: '10px 16px', background: "var(--danger-bg)", border: '1px solid #EF444440', borderRadius: 10, color: "var(--danger)", fontWeight: 600, cursor: 'pointer', fontSize: 13, display: 'flex', alignItems: 'center', gap: 8 }}>
Solicitar exclusão da conta
) : (
⚠️ Tem certeza? Esta ação é irreversível.
setDeleteAccountConfirm(false)} style={{ flex: 1, padding: '9px', background: 'var(--surface-2)', border: '1px solid var(--border)', borderRadius: 8, color: 'var(--text-2)', fontWeight: 600, cursor: 'pointer', fontSize: 13 }}>
Cancelar
{ setDeleteAccountConfirm(false); await deleteAccount(); }} style={{ flex: 2, padding: '9px', background: "var(--danger-action)", border: 'none', borderRadius: 8, color: 'white', fontWeight: 700, cursor: 'pointer', fontSize: 13 }}>
Sim, excluir minha conta
)}
)}
{/* ── APARÊNCIA ── */}
{activeTab === 'aparencia' && (
Aparência
Tema visual
Escolha o design system do app
{[
{ value: 'light', label: 'Finafamily Claro', swatch: 'linear-gradient(135deg, #EEF2F8, #2167D5)' },
{ value: 'dark', label: 'Finafamily Escuro', swatch: 'linear-gradient(135deg, #101827, #9EC8FF)' },
{ value: 'nubank-light', label: 'Finafamily Roxo', swatch: 'linear-gradient(135deg, #FFFFFF, #8A05BE)' },
].map(opt => (
setTheme(opt.value)} style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6,
padding: '12px 6px', borderRadius: 10,
border: `1.5px solid ${theme === opt.value ? 'var(--accent)' : 'var(--border)'}`,
background: theme === opt.value ? 'var(--accent-tint)' : 'var(--surface-1)',
cursor: 'pointer', transition: 'all 150ms',
}}>
{opt.label}
{theme === opt.value && }
))}
Dados e Privacidade
Exportar Dados da Família
)}
);
}
Object.assign(window, { Relatorios, Notificacoes, Configuracoes });