// SGAB v2 — App Shell: Login, Sidebar, Header, Router const { useState, useEffect, useCallback } = React; // Mobile detection hook function useIsMobile(breakpoint = 900) { const [mobile, setMobile] = useState(() => typeof window !== 'undefined' && window.innerWidth < breakpoint); useEffect(() => { const onResize = () => setMobile(window.innerWidth < breakpoint); window.addEventListener('resize', onResize); window.addEventListener('orientationchange', onResize); return () => { window.removeEventListener('resize', onResize); window.removeEventListener('orientationchange', onResize); }; }, [breakpoint]); return mobile; } const NAV_GROUPS = [ { label:'GERAL', items:[ { id:'dashboard', label:'Dashboard', icon:'dashboard' }, { id:'agenda', label:'Agenda', icon:'agenda' }, ]}, { label:'OPERAÇÃO', items:[ { id:'clientes', label:'Clientes', icon:'clientes' }, { id:'equipamentos', label:'Equipamentos', icon:'equipamentos' }, { id:'ordens', label:'Ordens de Serviço',icon:'ordens' }, { id:'calibracao', label:'Calibração', icon:'calibracao' }, { id:'propostas', label:'Propostas (PTC)', icon:'propostas' }, ]}, { label:'GESTÃO', items:[ { id:'financeiro', label:'Financeiro', icon:'financeiro' }, { id:'estoque', label:'Estoque', icon:'estoque' }, { id:'relatorios', label:'Relatórios', icon:'relatorios' }, ]}, { label:'SISTEMA', items:[ { id:'config', label:'Configurações', icon:'config' }, ]}, ]; const NAV_ITEMS = NAV_GROUPS.flatMap(g => g.items); // ── LOGIN SCREEN ───────────────────────────────────────────────── function LoginScreen({ onLogin, mobile }) { const [email, setEmail] = useState(''); const [senha, setSenha] = useState(''); const [err, setErr] = useState(''); const [loading, setLoading] = useState(false); async function handleLogin(e) { e.preventDefault(); setErr(''); setLoading(true); if (!window.SGAB_SB) { setErr('Cliente Supabase não carregou. Verifique sua conexão.'); setLoading(false); return; } const { error } = await window.SGAB_SB.signIn(email, senha); if (error) { const msg = (error.message || '').toLowerCase().includes('invalid') ? 'E-mail ou senha incorretos.' : (error.message || 'Falha no login.'); setErr(msg); setLoading(false); return; } const profile = await window.SGAB_SB.currentProfile(); if (!profile) { setErr('Login OK, mas o perfil não foi encontrado. Avise o administrador.'); setLoading(false); return; } if (!profile.ativo) { setErr('Usuário desativado. Avise o administrador.'); await window.SGAB_SB.signOut(); setLoading(false); return; } // Carrega snapshot de todas as tabelas antes de entrar no app try { await window.SGAB_SB.loadAll(); } catch (err) { console.error('[SGAB] Falha ao carregar dados:', err); setErr('Login OK, mas falha ao carregar dados. ' + (err.message || '')); setLoading(false); return; } onLogin(profile); } return (
{/* LEFT PANEL */}
{/* BG circles */}
{/* LOGO */}
Levi Balanças

Sistema de Gestão para
Assistência Técnica

Agenda, Calibrações RBC, Certificados, Propostas e Financeiro — tudo em um só lugar.

{/* Feature pills */} {[['calibracao','Calibrações RBC e Rastreada'],['propostas','Propostas PDF automáticas'],['financeiro','Controle financeiro em tempo real'],['agenda','Agenda mensal visual']].map(([icon,txt])=>(
{txt}
))}

Acreditação CGCRE • CAL 0830 • ABNT NBR ISO/IEC 17025:2017

{/* RIGHT PANEL */}
{mobile && (
Levi Balanças
)}

Bem-vindo de volta

Acesse sua conta para continuar

{err &&
{err}
} {loading ? 'Entrando...' : 'Entrar no Sistema'}

Desenvolvido por JuFlow Digital · 2026

); } // ── SIDEBAR ────────────────────────────────────────────────────── function Sidebar({ route, onNav, user, onLogout, collapsed, onToggle, mobile, mobileOpen }) { const perms = { administrador: NAV_ITEMS.map(n=>n.id), tecnico: ['dashboard','agenda','clientes','equipamentos','ordens','calibracao'], administrativo: ['dashboard','agenda','clientes','equipamentos','ordens','propostas','config'], financeiro: ['dashboard','financeiro','relatorios'], visualizador: ['dashboard','relatorios'] }; const allowed = perms[user.perfil] || []; const visible = NAV_ITEMS.filter(n => allowed.includes(n.id)); return (
{/* Decorative gradient blob */}
{/* LOGO */}
{collapsed ? (
Levi Balanças
) : (
Levi Balanças
)} {!collapsed && (
SGAB v2 ● Online
)}
{/* DIVIDER */}
{/* NAV with groups */} {/* USER FOOTER */}
{/* Collapse toggle (desktop only) */} {!mobile && ( )} {/* User card */}
{user.iniciais}
{!collapsed && ( <>
{user.nome}
{user.perfil}
)}
); } // ── HEADER ─────────────────────────────────────────────────────── function AppHeader({ route, user, alertCount, mobile, onMenu, onNav, onLogout }) { const labels = { dashboard:'Dashboard', agenda:'Agenda Mensal', clientes:'Clientes', equipamentos:'Equipamentos', ordens:'Ordens de Serviço', calibracao:'Calibração RBC / Rastreada', propostas:'Propostas (PTC)', financeiro:'Financeiro', estoque:'Estoque', relatorios:'Relatórios', config:'Configurações' }; const today = new Date(); const todayStr = today.toLocaleDateString('pt-BR',{weekday:'long',day:'2-digit',month:'long',year:'numeric'}); const shortDateStr = today.toLocaleDateString('pt-BR',{day:'2-digit',month:'short'}); const [menuOpen, setMenuOpen] = useState(false); const [bellOpen, setBellOpen] = useState(false); const alertas = bellOpen ? SGAB.getAlertas(SGAB.getDB()) : []; // Fecha popovers ao clicar fora useEffect(() => { if (!menuOpen && !bellOpen) return; const onDoc = e => { if (!e.target.closest('[data-sgab-popover]')) { setMenuOpen(false); setBellOpen(false); } }; document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, [menuOpen, bellOpen]); function handleNotifClick(al) { const target = {calibracao:'equipamentos',receber:'financeiro',pagar:'financeiro',estoque:'estoque'}[al.tipo]; setBellOpen(false); if (target && onNav) onNav(target); } return (
{mobile && ( )}
{labels[route]||route}
{mobile ? shortDateStr : todayStr}
{/* Alert bell */}
{bellOpen && (
Notificações
{alertas.length === 0 ? 'Nenhum alerta no momento' : `${alertas.length} alerta(s) ativos`}
{alertas.length === 0 ? (

Tudo em dia!

) : alertas.map((al, i) => { const cols = { danger:{ bg:'#FEF2F2', border:'#FECACA', ic:'#DC2626', c:'#991B1B' }, warning:{ bg:'#FFFBEB', border:'#FDE68A', ic:'#D97706', c:'#92400E' }, info:{ bg:'#EFF6FF', border:'#BFDBFE', ic:'#3B82F6', c:'#1E40AF' } }; const cc = cols[al.nivel] || cols.info; const icons = { calibracao:'scale', receber:'financeiro', pagar:'arrowDown', estoque:'pkg' }; return (
handleNotifClick(al)} onMouseEnter={e=>e.currentTarget.style.background='#F8FAFF'} onMouseLeave={e=>e.currentTarget.style.background='transparent'} style={{ padding:'12px 16px', borderBottom: i
{al.msg}
); })}
)}
{/* User chip com dropdown */}
{menuOpen && (
{/* Cabeçalho com nome/email */}
{user.iniciais}
{user.nome}
{user.email || ''}
{user.perfil}
{/* Itens do menu */} {(user.perfil==='administrador' || user.perfil==='administrativo') && onNav && ( )}
)}
); } // ── TOAST MANAGER ──────────────────────────────────────────────── function useToast() { const [toasts, setToasts] = useState([]); const show = useCallback((msg, type='success') => { const id = Date.now(); setToasts(t => [...t, { id, msg, type }]); setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 3500); }, []); const ToastContainer = () => (
{toasts.map(t => setToasts(ts => ts.filter(x => x.id !== t.id))} />)}
); return { show, ToastContainer }; } // ── APP SHELL ──────────────────────────────────────────────────── function App() { const [user, setUser] = useState(null); const [route, setRoute] = useState('dashboard'); const [collapsed, setCollapsed] = useState(false); const [mobileOpen, setMobileOpen] = useState(false); const [booting, setBooting] = useState(true); const [, forceRefresh] = useState(0); const mobile = useIsMobile(); const { show: toast, ToastContainer } = useToast(); // Expor toast globalmente para o data.js mostrar erros de sync useEffect(() => { window.__SGAB_TOAST = toast; return () => { delete window.__SGAB_TOAST; }; }, [toast]); // Re-render quando o snapshot do Supabase atualizar useEffect(() => { if (!window.SGAB_SB) return; return window.SGAB_SB.onChange(() => forceRefresh(n => n + 1)); }, []); // Boot: tenta restaurar sessão Supabase useEffect(() => { let cancelled = false; (async () => { if (!window.SGAB_SB) { if (!cancelled) setBooting(false); return; } try { const profile = await window.SGAB_SB.currentProfile(); if (profile && profile.ativo) { await window.SGAB_SB.loadAll(); if (!cancelled) setUser(profile); } } catch (e) { console.error('[SGAB] Boot:', e); } finally { if (!cancelled) setBooting(false); } })(); return () => { cancelled = true; }; }, []); // Close drawer when leaving mobile useEffect(() => { if (!mobile) setMobileOpen(false); }, [mobile]); function handleLogin(u) { setUser(u); } async function handleLogout() { if (window.SGAB_SB) await window.SGAB_SB.signOut(); setUser(null); } function handleNav(r) { setRoute(r); if (mobile) setMobileOpen(false); } const alertCount = user ? SGAB.getAlertas(SGAB.getDB()).length : 0; if (booting) { return (

Carregando...

); } if (!user) return ; // Render module const moduleProps = { user, toast, onNav: handleNav }; const modules = { dashboard: window.Dashboard && , agenda: window.Agenda && , clientes: window.Clientes && , equipamentos: window.Equipamentos && , ordens: window.OrdemServico && , calibracao: window.Calibracao && , propostas: window.Propostas && , financeiro: window.Financeiro && , estoque: window.Estoque && , relatorios: window.Relatorios && , config: window.Configuracoes && , }; return (
{mobile && mobileOpen &&
setMobileOpen(false)}/>} setCollapsed(c => !c)} mobile={mobile} mobileOpen={mobileOpen}/>
setMobileOpen(true)} onNav={handleNav} onLogout={handleLogout}/>
{modules[route] ||
Módulo em desenvolvimento
}
); } Object.assign(window, { App, useToast });