// 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 */}
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])=>(
))}
Acreditação CGCRE • CAL 0830 • ABNT NBR ISO/IEC 17025:2017
{/* RIGHT PANEL */}
{mobile && (
)}
Bem-vindo de volta
Acesse sua conta para continuar
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 ? (
) : (
)}
{!collapsed && (
SGAB v2
● Online
)}
{/* DIVIDER */}
{/* NAV with groups */}
{NAV_GROUPS.map((group, gi) => {
const groupItems = group.items.filter(it => allowed.includes(it.id));
if (groupItems.length === 0) return null;
return (
{!collapsed && (
{group.label}
)}
{collapsed && gi > 0 && (
)}
{groupItems.map(item => {
const active = route === item.id;
return (
onNav(item.id)}
title={collapsed ? item.label : undefined}
style={{
width:'100%', display:'flex', alignItems:'center', gap:11,
padding: collapsed ? '11px' : '10px 12px',
border:'none', borderRadius:10,
background: active
? 'linear-gradient(135deg, rgba(37,99,235,0.35) 0%, rgba(30,111,217,0.18) 100%)'
: 'transparent',
color: active ? '#fff' : 'rgba(255,255,255,0.55)',
cursor:'pointer', marginBottom:2,
transition:'all 0.18s cubic-bezier(0.4, 0, 0.2, 1)',
fontFamily:'inherit', justifyContent: collapsed ? 'center' : 'flex-start',
position:'relative', overflow:'hidden',
boxShadow: active ? 'inset 0 1px 0 rgba(255,255,255,0.08), 0 2px 8px rgba(30,111,217,0.2)' : 'none'
}}
onMouseEnter={e => {
if (!active) {
e.currentTarget.style.background = 'rgba(255,255,255,0.06)';
e.currentTarget.style.color = 'rgba(255,255,255,0.92)';
}
}}
onMouseLeave={e => {
if (!active) {
e.currentTarget.style.background = 'transparent';
e.currentTarget.style.color = 'rgba(255,255,255,0.55)';
}
}}>
{active &&
}
{!collapsed && {item.label} }
{!collapsed && active && (
)}
);
})}
);
})}
{/* USER FOOTER */}
{/* Collapse toggle (desktop only) */}
{!mobile && (
{ e.currentTarget.style.background='rgba(255,255,255,0.08)'; e.currentTarget.style.color='rgba(255,255,255,0.85)'; }}
onMouseLeave={e => { e.currentTarget.style.background='rgba(255,255,255,0.03)'; e.currentTarget.style.color='rgba(255,255,255,0.45)'; }}>
{!collapsed && Recolher menu }
)}
{/* User card */}
{!collapsed && (
<>
{user.nome}
{user.perfil}
{ e.currentTarget.style.background='rgba(248,113,113,0.15)'; e.currentTarget.style.color='#F87171'; }}
onMouseLeave={e => { e.currentTarget.style.background='transparent'; e.currentTarget.style.color='rgba(255,255,255,0.4)'; }}>
>
)}
);
}
// ── 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 */}
{setBellOpen(o=>!o); setMenuOpen(false);}} title="Notificações"
style={{ width:36, height:36, borderRadius:9, background:bellOpen?'#EFF6FF':'#F1F5F9', border:'none', display:'flex', alignItems:'center', justifyContent:'center', cursor:'pointer', position:'relative', transition:'background 0.12s' }}>
{alertCount > 0 &&
{alertCount}
}
{bellOpen && (
Notificações
{alertas.length === 0 ? 'Nenhum alerta no momento' : `${alertas.length} alerta(s) ativos`}
setBellOpen(false)} style={{ background:'none', border:'none', cursor:'pointer', padding:4, color:'#9CA3AF', display:'flex' }}>
{alertas.length === 0 ? (
) : 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
);
})}
)}
{/* User chip com dropdown */}
{setMenuOpen(o=>!o); setBellOpen(false);}}
style={{ background:menuOpen?'#EFF6FF':(mobile?(user.cor||'#1E6FD9'):'#F8FAFF'), border: mobile?'none':'1px solid #E8EDF4', borderRadius:mobile?9:10, padding: mobile?0:'6px 12px 6px 6px', display:'flex', alignItems:'center', gap:8, cursor:'pointer', fontFamily:'inherit', transition:'background 0.12s' }}>
{user.iniciais}
{!mobile && (
<>
{user.nome}
{user.perfil}
>
)}
{menuOpen && (
{/* Cabeçalho com nome/email */}
{user.iniciais}
{user.nome}
{user.email || ''}
{user.perfil}
{/* Itens do menu */}
{(user.perfil==='administrador' || user.perfil==='administrativo') && onNav && (
{ setMenuOpen(false); onNav('config'); }}
onMouseEnter={e=>e.currentTarget.style.background='#F8FAFF'}
onMouseLeave={e=>e.currentTarget.style.background='transparent'}
style={{ width:'100%', padding:'10px 16px', border:'none', background:'transparent', display:'flex', alignItems:'center', gap:10, cursor:'pointer', fontFamily:'inherit', fontSize:13, color:'#374151', transition:'background 0.1s' }}>
Configurações
)}
{ setMenuOpen(false); onLogout && onLogout(); }}
onMouseEnter={e=>{e.currentTarget.style.background='#FEF2F2'; e.currentTarget.style.color='#B91C1C';}}
onMouseLeave={e=>{e.currentTarget.style.background='transparent'; e.currentTarget.style.color='#374151';}}
style={{ width:'100%', padding:'10px 16px', border:'none', background:'transparent', display:'flex', alignItems:'center', gap:10, cursor:'pointer', fontFamily:'inherit', fontSize:13, color:'#374151', transition:'all 0.1s' }}>
Sair
)}
);
}
// ── 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 (
);
}
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 });