// ============================================================
// FINAFAMILY — Layout: Sidebar, Header, BottomNav
// ============================================================
const { useState, useEffect, useRef } = React;
// Ícone e rótulo de CADA tema (não do próximo) — o botão de troca rápida
// cicla light → dark → nubank-light → light ao ser clicado.
const THEME_META = {
'light': { icon: 'Sun', label: 'Finafamily Claro' },
'dark': { icon: 'Moon', label: 'Finafamily Escuro' },
'nubank-light': { icon: 'Zap', label: 'Finafamily Roxo' },
};
const NAV_ITEMS = [
{ id: 'dashboard', label: 'Dashboard', icon: 'LayoutDashboard' },
{ id: 'despesas', label: 'Despesas', icon: 'Wallet' },
{ id: 'receitas', label: 'Receitas', icon: 'DollarSign' },
{ id: 'entre-membros', label: 'Entre membros', icon: 'Users' },
{ id: 'cartoes', label: 'Pagamentos', icon: 'CreditCard' },
{ id: 'investimentos', label: 'Investimentos', icon: 'TrendingUp' },
{ id: 'orcamento', label: 'Orçamento', icon: 'Wallet' },
{ id: 'relatorios', label: 'Relatórios', icon: 'BarChart2' },
{ id: 'importar', label: 'Importar Fatura', icon: 'Upload' },
{ id: 'configuracoes', label: 'Configurações', icon: 'Settings' },
];
const SCREEN_LABELS = {
dashboard: 'Dashboard',
despesas: 'Despesas',
receitas: 'Receitas',
'entre-membros': 'Valores entre membros',
cartoes: 'Meios de Pagamento',
investimentos: 'Investimentos',
orcamento: 'Orçamento mensal',
relatorios: 'Relatórios',
importar: 'Importar Fatura',
notificacoes: 'Notificações',
configuracoes: 'Configurações',
};
function Logo({ collapsed = false }) {
return (
{!collapsed && (
Fina
family
.
)}
);
}
function Sidebar({ currentScreen, onNavigate }) {
const { theme, toggle } = useTheme();
const { currentMember, logout } = useApp();
const [collapsed, setCollapsed] = useState(false);
return (
{/* Logo area */}
{!collapsed && (
setCollapsed(true)} aria-label="Recolher menu" title="Recolher menu" style={{ background: 'none', border: 'none', cursor: 'pointer', color: 'var(--text-3)', padding: 4, display: 'flex', borderRadius: 6 }}>
)}
{collapsed && (
setCollapsed(false)} aria-label="Expandir menu" title="Expandir menu" style={{ position: 'absolute', right: -10, top: 28, background: 'var(--surface-1)', border: '1px solid var(--border)', borderRadius: '50%', width: 20, height: 20, cursor: 'pointer', color: 'var(--text-3)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 10 }}>
)}
{/* Aviso de conta local */}
{currentMember.isLocalAccount && !collapsed && (
Conta local — sem sincronização
)}
{/* Nav items */}
{NAV_ITEMS.map(item => {
const active = currentScreen === item.id;
return (
onNavigate(item.id)} title={collapsed ? item.label : ''} style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: collapsed ? '10px' : '10px 12px',
borderRadius: 10, border: 'none', cursor: 'pointer',
background: active ? 'var(--nav-active-bg)' : 'transparent',
color: ffReadableColor(active ? 'var(--accent)' : 'var(--text-2)'),
fontWeight: active ? 600 : 500, fontSize: 13.5,
justifyContent: collapsed ? 'center' : 'flex-start',
transition: 'all 150ms', position: 'relative',
whiteSpace: 'nowrap', width: '100%',
}}
onMouseEnter={e => { if (!active) e.currentTarget.style.background = 'var(--surface-2)'; }}
onMouseLeave={e => { if (!active) e.currentTarget.style.background = 'transparent'; }}
>
{!collapsed && {item.label} }
);
})}
{/* Bottom: dark mode + member + logout */}
e.currentTarget.style.background = 'var(--surface-2)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
>
{!collapsed && {THEME_META[theme].label} }
{/* Identidade da sessão: somente informativa, nunca intercambiável. */}
{!collapsed && (
{currentMember.name.split(' ')[0]}
{currentMember.role}
)}
{/* Botão Sair — sempre visível */}
e.currentTarget.style.background = '#EF444415'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}
>
{!collapsed && Sair da conta }
);
}
// Pra onde a notificação leva o usuário quando ele clica nela — link vem do
// backend (ex.: 'despesas', 'cartoes', 'dashboard'); sem link, cai no dashboard.
function notificationTargetScreen(n) {
return n.link === 'member-operations' ? 'entre-membros' : (n.link || 'dashboard');
}
function NotificationPopup({ onNavigate, onClose }) {
const { notifications, markNotificationRead } = useApp();
const ref = useRef(null);
useEffect(() => {
function handleClickOutside(e) { if (ref.current && !ref.current.contains(e.target)) onClose(); }
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [onClose]);
function handleClick(n) {
if (!n.read) markNotificationRead(n.id);
onClose();
onNavigate(notificationTargetScreen(n));
}
const TYPE_META = {
warning: { icon: 'AlertTriangle', color: '#F59E0B' },
alert: { icon: 'Bell', color: '#EF4444' },
info: { icon: 'Info', color: '#3B82F6' },
};
return (
Notificações
{notifications.length === 0 && (
Nenhuma notificação por aqui.
)}
{notifications.slice(0, 20).map(n => {
const meta = TYPE_META[n.type] || TYPE_META.info;
return (
handleClick(n)} style={{
display: 'flex', gap: 10, width: '100%', textAlign: 'left',
padding: '10px 16px', border: 'none', borderBottom: '1px solid var(--border)',
background: n.read ? 'transparent' : 'var(--accent-tint)', cursor: 'pointer',
}}>
);
})}
{ onClose(); onNavigate('notificacoes'); }} style={{
width: '100%', padding: '10px', border: 'none', borderTop: '1px solid var(--border)',
background: 'var(--surface-1)', color: 'var(--accent)', fontWeight: 600, fontSize: 12.5, cursor: 'pointer',
}}>
Ver todas as notificações
);
}
function Header({ currentScreen, onNavigate }) {
const { currentMonth, setCurrentMonth, currentMember, unreadCount, notifAlert, markAllRead } = useApp();
const { theme, toggle } = useTheme();
const [showNotifPopup, setShowNotifPopup] = useState(false);
// O sino pisca com a contagem até o usuário abrir o popup e ver a lista —
// ao fechar (clicando fora, num item, ou no próprio sino de novo) tudo é
// dado como visto e a contagem zera.
function closeNotifPopup() {
setShowNotifPopup(false);
markAllRead();
}
function toggleNotifPopup() {
if (showNotifPopup) closeNotifPopup();
else setShowNotifPopup(true);
}
function prevMonth() {
setCurrentMonth(m => {
if (m.month === 1) return { year: m.year - 1, month: 12 };
return { ...m, month: m.month - 1 };
});
}
function nextMonth() {
setCurrentMonth(m => {
if (m.month === 12) return { year: m.year + 1, month: 1 };
return { ...m, month: m.month + 1 };
});
}
const monthNames = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'];
const MONTHS_SHORT = ['Jan','Fev','Mar','Abr','Mai','Jun','Jul','Ago','Set','Out','Nov','Dez'];
return (
{/* Logo — só aparece no mobile (sidebar oculta) */}
{/* Título da tela — oculto no mobile, substituído pelo logo */}
{SCREEN_LABELS[currentScreen]}
{/* Month nav */}
{['dashboard','despesas','receitas'].includes(currentScreen) && (
{/* Desktop: mês completo / Mobile: abreviado */}
{monthNames[currentMonth.month - 1]} {currentMonth.year}
{MONTHS_SHORT[currentMonth.month - 1]}/{String(currentMonth.year).slice(-2)}
)}
{/* Dark mode — oculto no mobile (está na sidebar / settings) */}
e.currentTarget.style.color = 'var(--text-1)'}
onMouseLeave={e => e.currentTarget.style.color = 'var(--text-3)'}
>
e.currentTarget.style.color = 'var(--text-1)'}
onMouseLeave={e => e.currentTarget.style.color = 'var(--text-3)'}
>
{unreadCount > 0 && (
{unreadCount > 9 ? '9+' : unreadCount}
)}
{showNotifPopup && }
{currentMember.name.split(' ')[0]}
);
}
function BottomNav({ currentScreen, onNavigate }) {
const { currentMember, logout } = useApp();
const [menuOpen, setMenuOpen] = useState(false);
const MOBILE_NAV = [
{ id: '__menu__', icon: 'Menu', label: 'Menu' },
{ id: 'dashboard', icon: 'LayoutDashboard', label: 'Início' },
{ id: 'despesas', icon: 'Wallet', label: 'Despesas' },
{ id: 'receitas', icon: 'DollarSign', label: 'Receitas' },
{ id: 'cartoes', icon: 'CreditCard', label: 'Cartões' },
];
// Itens do drawer — o que não está na barra principal
const DRAWER_ITEMS = [
{ id: 'entre-membros', icon: 'Users', label: 'Entre membros', desc: 'Valores a pagar e receber', color: '#5264de' },
{ id: 'investimentos', icon: 'TrendingUp', label: 'Investimentos', desc: 'Portfólio e rendimentos', color: '#7C3AED' },
{ id: 'orcamento', icon: 'Wallet', label: 'Orçamento', desc: 'Plano mensal e divisões', color: 'var(--accent)' },
{ id: 'relatorios', icon: 'BarChart2', label: 'Relatórios', desc: 'Análise e tendências', color: '#0284C7' },
{ id: 'importar', icon: 'Upload', label: 'Importar Fatura', desc: 'Lançar despesas do cartão', color: '#059669' },
{ id: 'configuracoes', icon: 'Settings', label: 'Configurações', desc: 'Perfil e preferências', color: '#64748B' },
];
function handleNav(id) {
onNavigate(id);
setMenuOpen(false);
}
return (
<>
{/* ── Bottom Navigation Bar ── */}
{MOBILE_NAV.map(item => {
const isMenu = item.id === '__menu__';
const active = !isMenu && currentScreen === item.id;
const menuActive = isMenu && menuOpen;
return (
isMenu ? setMenuOpen(v => !v) : handleNav(item.id)}
style={{
display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 3,
background: menuActive ? 'var(--accent-tint)' : 'none',
border: 'none', cursor: 'pointer',
color: ffReadableColor((active || menuActive) ? 'var(--accent)' : 'var(--text-3)'),
padding: '4px 16px', borderRadius: 10, position: 'relative',
transition: 'color 150ms, background 150ms',
}}>
{item.label}
{active &&
}
);
})}
{/* ── Menu Drawer (Bottom Sheet) ── */}
{menuOpen && (
setMenuOpen(false)}
style={{
position: 'fixed', inset: 0, zIndex: 450, // acima do botão "+" (400), abaixo dos modais
background: 'var(--overlay-bg)',
backdropFilter: 'blur(2px)',
animation: 'fadeIn 180ms ease-out',
}}
/>
)}
{/* Handle + cabeçalho */}
{/* Perfil do membro */}
{currentMember && (
{currentMember.initials || currentMember.name?.slice(0,2).toUpperCase()}
{currentMember.name}
{currentMember.email}
)}
{/* Grid de opções */}
{DRAWER_ITEMS.map(item => {
const active = currentScreen === item.id;
return (
handleNav(item.id)} style={{
display: 'flex', alignItems: 'center', gap: 12,
padding: '14px 14px',
background: active ? item.color + '18' : 'var(--surface-1)',
border: `1px solid ${active ? item.color + '44' : 'var(--border)'}`,
borderRadius: 14, cursor: 'pointer',
transition: 'background 150ms',
textAlign: 'left',
}}>
);
})}
{/* Separador + Sair */}
{ setMenuOpen(false); logout && logout(); }}
style={{ width: '100%', padding: '12px 16px', display: 'flex', alignItems: 'center', gap: 10, background: "var(--danger-bg)", border: '1px solid #EF444430', borderRadius: 12, cursor: 'pointer', color: "var(--danger)" }}
>
Sair da conta
>
);
}
// ── Popup diário: alertas de vencimento + situação do dia a dia ──────────
// Aparece uma vez por dia, no primeiro acesso (ver useEffect em data.jsx).
function DailyDigestModal({ onNavigate }) {
const { dailyDigest, dismissDailyDigest, markNotificationRead } = useApp();
if (!dailyDigest || dailyDigest.length === 0) return null;
const TYPE_META = {
warning: { icon: 'AlertTriangle', color: '#F59E0B' },
alert: { icon: 'Bell', color: '#EF4444' },
info: { icon: 'Info', color: '#3B82F6' },
};
function handleClick(n) {
if (!n.read) markNotificationRead(n.id);
dismissDailyDigest();
onNavigate(n.link || 'dashboard');
}
return (
Vencimentos próximos e o ritmo dos seus gastos do dia a dia:
{dailyDigest.map(n => {
const meta = TYPE_META[n.type] || TYPE_META.info;
return (
handleClick(n)} style={{
display: 'flex', gap: 10, width: '100%', textAlign: 'left',
padding: '10px 12px', border: '1px solid var(--border)', borderRadius: 12,
background: 'var(--surface-1)', cursor: 'pointer',
}}>
);
})}
Entendi
);
}
Object.assign(window, { Sidebar, Header, BottomNav, DailyDigestModal, NAV_ITEMS, SCREEN_LABELS });