// Sidebar + top bar function Sidebar({ route, setRoute, badge, profile, session, onSignOut, savingsThisYear = 0, mobileOpen = false, onMobileClose }) { const { COMPANY, BRLk } = window.SCP; const userName = profile?.name || session?.user?.email?.split('@')[0] || COMPANY.owner; const userRole = profile?.role ? ( profile.role === 'admin' ? 'Administrador' : profile.role === 'comprador' ? 'Comprador' : profile.role === 'estoquista' ? 'Estoquista' : 'Visualizador' ) : COMPANY.role; const [menuOpen, setMenuOpen] = useState(false); const userRef = useRef(null); useEffect(() => { const onClick = (e) => { if (userRef.current && !userRef.current.contains(e.target)) setMenuOpen(false); }; document.addEventListener('mousedown', onClick); return () => document.removeEventListener('mousedown', onClick); }, []); const items = [ { k: 'dashboard', l: 'Dashboard', i: }, { k: 'quotations', l: 'Cotações', i: , badge: badge?.cotacoes }, { k: 'suppliers', l: 'Fornecedores', i: }, { k: 'products', l: 'Produtos', i: }, { k: 'orders', l: 'Pedidos de Compra', i: , badge: badge?.pedidos }, { k: 'stock', l: 'Estoque', i: , badge: badge?.estoque, tone: 'amber' }, { k: 'reports', l: 'Relatórios', i: }, ]; const ext = [ { k: 'portal', l: 'Portal do Fornecedor', i: }, { k: 'settings', l: 'Configurações', i: }, ]; return ( ); } function TopBar({ route, setRoute, openQuotation, onMenuClick }) { const titles = { dashboard: { t: 'Dashboard', s: 'Visão geral · ' + new Date().toLocaleDateString('pt-BR', { weekday: 'long', day: '2-digit', month: 'long' }) }, quotations: { t: 'Cotações', s: 'Crie, acompanhe e feche cotações com vários fornecedores em segundos.' }, quotationDetail: { t: 'Análise de Cotação', s: 'Comparativo entre fornecedores · validação manual com sugestão da IA.' }, newQuotation: { t: 'Nova Cotação', s: 'Selecione produtos, fornecedores e envie em segundos.' }, suppliers: { t: 'Fornecedores', s: 'Gerencie sua rede, scores e categorias atendidas.' }, products: { t: 'Produtos', s: 'Catálogo com estoque mínimo, código de barras e histórico de preços.' }, orders: { t: 'Pedidos de Compra', s: 'Pedidos gerados automaticamente após fechamento das cotações.' }, stock: { t: 'Estoque', s: 'Recebimentos, conferência e movimentações em tempo real.' }, reports: { t: 'Relatórios', s: 'Exporte em PDF e Excel · análise por período.' }, portal: { t: 'Portal do Fornecedor', s: 'Pré-visualize como o fornecedor vê a cotação enviada.' }, settings: { t: 'Configurações', s: 'Empresa, usuários, integrações, notificações e segurança.' }, }; const info = titles[route] || titles.dashboard; return (

{info.t}

{info.s}

} onClick={() => setRoute('newQuotation')}>Nova Cotação
); } function GlobalSearch({ setRoute, openQuotation }) { const { SUPPLIERS, PRODUCTS, QUOTATIONS, ORDERS } = window.SCP; const [q, setQ] = useState(''); const [open, setOpen] = useState(false); const [focusIdx, setFocusIdx] = useState(0); const wrapRef = useRef(null); const inputRef = useRef(null); useEffect(() => { const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); inputRef.current?.focus(); setOpen(true); } if (e.key === 'Escape') { setOpen(false); inputRef.current?.blur(); } }; const onClickOutside = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }; window.addEventListener('keydown', onKey); document.addEventListener('mousedown', onClickOutside); return () => { window.removeEventListener('keydown', onKey); document.removeEventListener('mousedown', onClickOutside); }; }, []); const term = q.trim().toLowerCase(); const results = useMemo(() => { if (!term) return []; const out = []; SUPPLIERS.forEach(s => { if ((s.name + s.cnpj + s.city + s.contact).toLowerCase().includes(term)) { out.push({ kind: 'fornecedor', icon: , label: s.name, sub: s.city + ' · score ' + s.score, route: 'suppliers' }); } }); PRODUCTS.forEach(p => { if ((p.name + p.code).toLowerCase().includes(term)) { out.push({ kind: 'produto', icon: , label: p.name, sub: p.code + ' · ' + window.SCP.BRL(p.lastPrice), route: 'products' }); } }); QUOTATIONS.forEach(qq => { if ((qq.id + qq.title).toLowerCase().includes(term)) { out.push({ kind: 'cotação', icon: , label: qq.title, sub: qq.id + ' · ' + qq.createdAt, route: 'quotationDetail', quotation: qq.id }); } }); ORDERS.forEach(o => { if ((o.id + o.supplier).toLowerCase().includes(term)) { out.push({ kind: 'pedido', icon: , label: o.id, sub: o.supplier + ' · ' + window.SCP.BRL(o.total), route: 'orders' }); } }); return out.slice(0, 12); }, [term]); const choose = (r) => { setOpen(false); setQ(''); if (r.quotation && openQuotation) openQuotation(r.quotation); else setRoute(r.route); }; const onKeyDown = (e) => { if (e.key === 'ArrowDown') { e.preventDefault(); setFocusIdx(i => Math.min(results.length - 1, i + 1)); } if (e.key === 'ArrowUp') { e.preventDefault(); setFocusIdx(i => Math.max(0, i - 1)); } if (e.key === 'Enter' && results[focusIdx]) { e.preventDefault(); choose(results[focusIdx]); } }; return (
{open && (term ? results.length > 0 : true) && (
{!term && (
Digite para buscar em produtos, fornecedores, cotações e pedidos.
)} {term && results.length === 0 && (
Nenhum resultado para “{q}”.
)} {term && results.map((r, i) => ( ))}
↑↓ navegar Enter abrir Esc fechar
)}
); } function PeriodMenu() { const [open, setOpen] = useState(false); const [active, setActive] = useState('30d'); const wrapRef = useRef(null); useEffect(() => { const onClickOutside = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', onClickOutside); return () => document.removeEventListener('mousedown', onClickOutside); }, []); const periods = [ { k: 'hoje', l: 'Hoje' }, { k: '7d', l: 'Últimos 7 dias' }, { k: '30d', l: 'Últimos 30 dias' }, { k: '90d', l: 'Últimos 90 dias' }, { k: 'mes', l: 'Este mês' }, { k: 'ano', l: 'Este ano' }, ]; const pick = (k, l) => { setActive(k); setOpen(false); window.scpToast('Período aplicado', { kind: 'emerald', sub: l }); }; return (
} title="Período" onClick={() => setOpen(o => !o)} /> {open && (
Filtrar por período
{periods.map(p => ( ))}
)}
); } function NotifyBell() { const [open, setOpen] = useState(false); const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const iconMap = { sparkle: , truck: , reply: , warn: , check: , }; const fmtAt = (iso) => { if (!iso) return ''; const d = new Date(iso); const diff = (Date.now() - d.getTime()) / 1000; if (diff < 60) return 'agora'; if (diff < 3600) return `${Math.floor(diff / 60)} min`; if (diff < 86400) return `${Math.floor(diff / 3600)}h`; if (diff < 172800) return 'ontem'; return d.toLocaleDateString('pt-BR'); }; const reload = async () => { try { const data = await window.scpDb.notifications.list(20); setItems(data); } catch (e) { /* silent */ } finally { setLoading(false); } }; useEffect(() => { reload(); let unsub = null; (async () => { const session = await window.scpAuth.getSession(); if (!session?.user) return; unsub = window.scpDb.notifications.subscribe(session.user.id, () => reload()); })(); return () => { unsub && unsub(); }; }, []); const unread = items.filter(n => n.unread).length; const handleClick = async (n) => { if (n.unread) { try { await window.scpDb.notifications.markRead(n.id); setItems(items.map(x => x.id === n.id ? { ...x, unread: false } : x)); } catch (e) { /* ignore */ } } }; const handleMarkAll = async () => { try { await window.scpDb.notifications.markAllRead(); setItems(items.map(x => ({ ...x, unread: false }))); window.scpToast('Todas marcadas como lidas', { kind: 'emerald' }); } catch (e) { window.scpToast('Erro ao marcar', { kind: 'coral', sub: e.message }); } }; return (
{open && ( <>
setOpen(false)} />

Notificações

{loading && (
Carregando…
)} {!loading && items.length === 0 && (
Sem notificações ainda. Atividade do sistema aparece aqui.
)} {!loading && items.map(n => (
handleClick(n)} style={{ cursor: 'pointer' }}> {iconMap[n.icon] || }
{n.title}
{n.sub}
{fmtAt(n.created_at)}
))}
)}
); } (function injectSideCss() { if (document.getElementById('scp-side-css')) return; const s = document.createElement('style'); s.id = 'scp-side-css'; s.textContent = ` .scp-side { width: 260px; min-width: 260px; height: 100vh; padding: max(18px, env(safe-area-inset-top, 18px)) 14px max(18px, env(safe-area-inset-bottom, 18px)) 18px; display: flex; flex-direction: column; background: linear-gradient(180deg, oklch(0.155 0.012 250), oklch(0.135 0.012 250)); border-right: 1px solid var(--border-soft); position: relative; z-index: 2; } /* Botão de fechar (mobile) */ .scp-side-close { display: none; width: 32px; height: 32px; align-items: center; justify-content: center; border-radius: 8px; color: var(--fg-2); margin-left: auto; } .scp-side-close:hover { background: var(--surface-2); color: var(--fg); } /* Backdrop quando o drawer está aberto no mobile */ .scp-mobile-backdrop { display: none; position: fixed; inset: 0; background: oklch(0.05 0.005 250 / .55); backdrop-filter: blur(2px); z-index: 40; animation: fadein .18s ease both; } /* ===== Mobile · sidebar vira drawer ===== */ @media (max-width: 900px) { .scp-side { position: fixed; left: 0; top: 0; bottom: 0; z-index: 50; transform: translateX(-100%); transition: transform .25s cubic-bezier(.2,.7,.2,1); box-shadow: 16px 0 60px -10px rgba(0,0,0,.6); width: min(300px, 86vw); } .scp-side.mobile-open { transform: translateX(0); } .scp-mobile-backdrop { display: block; } .scp-side-close { display: inline-flex; } } .scp-side-brand { display: flex; align-items: center; gap: 10px; padding: 4px 6px; } .scp-side-brand-text { display: flex; flex-direction: column; line-height: 1.1; } .scp-side-brand-text .t { font-weight: 700; letter-spacing: -0.01em; font-size: 16px; } .scp-side-brand-text .s { font-size: 11px; color: var(--fg-3); margin-top: 2px; } .scp-side-company { margin-top: 18px; padding: 10px; display: flex; align-items: center; gap: 10px; background: var(--surface-2); border: 1px solid var(--border-soft); border-radius: 12px; cursor: pointer; color: var(--fg-2); transition: background .12s; } .scp-side-company:hover { background: var(--surface-3); } .scp-user-pop { position: absolute; left: 0; right: 0; top: calc(100% + 6px); background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow-pop); z-index: 20; overflow: hidden; cursor: default; animation: fadein .15s ease both; } .scp-user-pop-head { padding: 10px 14px; border-bottom: 1px solid var(--border-soft); } .scp-user-pop-head .t { display: block; font-size: 13px; font-weight: 600; color: var(--fg); } .scp-user-pop-head .s { display: block; font-size: 11px; color: var(--fg-3); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .scp-user-pop-row { display: flex; align-items: center; gap: 10px; width: 100%; padding: 10px 14px; font-size: 13px; color: var(--fg-2); text-align: left; transition: background .12s, color .12s; } .scp-user-pop-row:hover { background: var(--surface-2); color: var(--fg); } .scp-user-pop-row.danger { color: oklch(0.82 0.14 25); } .scp-user-pop-row.danger:hover { background: oklch(0.28 0.07 25 / .35); color: oklch(0.92 0.14 25); } .scp-side-company .info { flex: 1; min-width: 0; line-height: 1.2; } .scp-side-company .n { display: block; color: var(--fg); font-weight: 600; font-size: 12.5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .scp-side-company .m { display: block; color: var(--fg-3); font-size: 11px; margin-top: 2px; } .scp-side-section { margin-top: 18px; display: flex; flex-direction: column; gap: 2px; } .scp-side-section-title { font-size: 10px; color: var(--fg-4); text-transform: uppercase; letter-spacing: .12em; font-weight: 700; padding: 4px 8px 6px; } .scp-nav { display: flex; align-items: center; gap: 10px; padding: 8px 10px; border-radius: 10px; text-align: left; color: var(--fg-3); font-size: 13px; font-weight: 500; transition: background .12s, color .12s; position: relative; } .scp-nav .ico { color: var(--fg-3); display: inline-flex; } .scp-nav:hover { background: var(--surface-2); color: var(--fg); } .scp-nav:hover .ico { color: var(--fg-2); } .scp-nav.active { background: linear-gradient(180deg, oklch(0.235 0.025 160 / .6), oklch(0.205 0.020 160 / .6)); color: var(--fg); box-shadow: inset 0 0 0 1px oklch(0.30 0.05 160 / .55); } .scp-nav.active .ico { color: oklch(0.86 0.14 160); } .scp-nav .badge { margin-left: auto; font-size: 10px; padding: 2px 7px; border-radius: 999px; font-weight: 700; background: oklch(0.30 0.06 160 / .6); color: oklch(0.92 0.14 160); } .scp-nav .badge.tone-amber { background: oklch(0.30 0.06 78 / .6); color: oklch(0.92 0.13 78); } .scp-side-foot { margin-top: auto; padding-top: 16px; } .scp-ai-card { padding: 12px; border-radius: 14px; background: radial-gradient(120% 80% at 100% 0%, oklch(0.32 0.10 160 / .65), transparent 60%), linear-gradient(180deg, oklch(0.22 0.025 250), oklch(0.18 0.020 250)); border: 1px solid oklch(0.32 0.06 160 / .35); position: relative; overflow: hidden; } .scp-ai-card-head { display: flex; align-items: center; gap: 8px; font-size: 12px; font-weight: 700; color: var(--fg); letter-spacing: -0.01em; } .scp-ai-dot { width: 22px; height: 22px; border-radius: 8px; display: inline-grid; place-items: center; background: oklch(0.4 0.10 160); color: oklch(0.96 0.14 160); } .scp-ai-card p { margin: 8px 0 10px; font-size: 11.5px; color: var(--fg-3); line-height: 1.4; } .scp-ai-card-stat { display: flex; flex-direction: column; gap: 2px; padding-top: 8px; border-top: 1px solid var(--border-soft); } .scp-ai-card-stat .v { font-size: 18px; font-weight: 700; letter-spacing: -0.02em; color: oklch(0.92 0.14 160); font-feature-settings: "tnum"; } .scp-ai-card-stat .s { font-size: 10.5px; color: var(--fg-3); } .scp-side-foot-meta { text-align: center; font-size: 10px; color: var(--fg-4); margin-top: 12px; letter-spacing: .04em; } /* TopBar */ .scp-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; padding: max(22px, calc(env(safe-area-inset-top, 0px) + 14px)) max(28px, env(safe-area-inset-right, 28px)) 22px max(28px, env(safe-area-inset-left, 28px)); border-bottom: 1px solid var(--border-soft); background: oklch(0.155 0.012 250 / .65); backdrop-filter: blur(8px); position: sticky; top: 0; z-index: 4; } .scp-top-menu { display: none; width: 38px; height: 38px; align-items: center; justify-content: center; border-radius: 10px; color: var(--fg); background: var(--surface-2); border: 1px solid var(--border-soft); flex-shrink: 0; transition: background .12s; } .scp-top-menu:hover { background: var(--surface-3); } @media (max-width: 900px) { .scp-top-menu { display: inline-flex; } } .scp-top-title { min-width: 0; flex: 1; } .scp-top-title h1 { margin: 0; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .scp-top-title p { margin: 4px 0 0; color: var(--fg-3); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .scp-top-right { display: flex; align-items: center; gap: 10px; position: relative; } .scp-top .scp-input { width: 320px; } /* ===== Mobile · ajustes do TopBar e busca ===== */ @media (max-width: 900px) { .scp-top { gap: 12px; } .scp-top-title h1 { font-size: 17px; } .scp-top-title p { display: none; } .scp-top-right { gap: 6px; } .scp-search-wrap, .scp-search-wrap .scp-input { width: auto !important; } .scp-search-wrap .scp-input { width: 200px !important; } .scp-search-wrap .scp-input input { font-size: 12px; } } @media (max-width: 640px) { .scp-top { padding-left: 14px; padding-right: 14px; } .scp-top-right .scp-btn span:not(.scp-btn-icon) { display: none; } .scp-search-wrap { display: none; } } /* Global search */ .scp-search-wrap { position: relative; } .scp-search-wrap .scp-input { width: 320px; padding-right: 8px; } .scp-input-clear { width: 20px; height: 20px; display: grid; place-items: center; border-radius: 6px; color: var(--fg-3); transition: background .12s, color .12s; } .scp-input-clear:hover { background: var(--surface-3); color: var(--fg); } .scp-search-pop { position: absolute; top: calc(100% + 8px); right: 0; left: 0; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow-pop); max-height: 460px; overflow: auto; z-index: 30; animation: fadein .15s ease both; } .scp-search-empty { padding: 18px; display: flex; align-items: center; gap: 10px; color: var(--fg-3); font-size: 12.5px; } .scp-search-row { display: grid; grid-template-columns: 28px 1fr auto; gap: 10px; align-items: center; padding: 9px 12px; width: 100%; text-align: left; background: transparent; border-bottom: 1px solid var(--border-soft); transition: background .12s; cursor: pointer; } .scp-search-row:last-of-type { border-bottom: 0; } .scp-search-row.focus, .scp-search-row:hover { background: var(--surface-2); } .scp-search-row .ico { width: 28px; height: 28px; border-radius: 7px; display: grid; place-items: center; background: oklch(0.30 0.06 160 / .45); color: oklch(0.92 0.14 160); } .scp-search-row .b { min-width: 0; display: flex; flex-direction: column; gap: 1px; } .scp-search-row .b .t { font-size: 13px; font-weight: 600; color: var(--fg); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .scp-search-row .b .s { font-size: 11.5px; color: var(--fg-3); } .scp-search-row .k { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: .08em; color: var(--fg-3); padding: 3px 8px; background: var(--surface-2); border-radius: 999px; border: 1px solid var(--border-soft); } .scp-search-foot { display: flex; justify-content: flex-end; gap: 14px; padding: 8px 12px; border-top: 1px solid var(--border-soft); font-size: 11px; color: var(--fg-3); background: oklch(0.18 0.012 250 / .5); } .scp-search-foot kbd { background: var(--surface-2); border: 1px solid var(--border-soft); padding: 1px 6px; border-radius: 4px; font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--fg-2); } /* Period popover */ .scp-period-wrap { position: relative; } .scp-period-pop { position: absolute; top: calc(100% + 8px); right: 0; min-width: 220px; background: var(--surface); border: 1px solid var(--border); border-radius: 12px; box-shadow: var(--shadow-pop); z-index: 30; overflow: hidden; animation: fadein .15s ease both; } .scp-period-pop-head { padding: 10px 14px; font-size: 11px; font-weight: 700; color: var(--fg-3); text-transform: uppercase; letter-spacing: .12em; border-bottom: 1px solid var(--border-soft); background: oklch(0.18 0.012 250 / .5); } .scp-period-row { display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 9px 14px; font-size: 13px; color: var(--fg-2); text-align: left; transition: background .12s, color .12s; } .scp-period-row:hover { background: var(--surface-2); color: var(--fg); } .scp-period-row.active { color: oklch(0.92 0.14 160); } .scp-period-row.active span:first-child { font-weight: 600; } /* Notify */ .scp-notify { position: relative; } .scp-notify-dot { position: absolute; top: -4px; right: -4px; background: oklch(0.78 0.16 25); color: white; font-size: 10px; width: 16px; height: 16px; display: grid; place-items: center; border-radius: 999px; font-weight: 700; border: 2px solid oklch(0.155 0.012 250); } .scp-notify-mask { position: fixed; inset: 0; z-index: 19; } .scp-notify-pop { position: absolute; right: 0; top: calc(100% + 8px); width: 380px; background: var(--surface); border: 1px solid var(--border); border-radius: 14px; z-index: 20; box-shadow: var(--shadow-pop); overflow: hidden; } .scp-notify-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; border-bottom: 1px solid var(--border-soft); } .scp-notify-head h4 { margin: 0; font-size: 14px; font-weight: 600; } .scp-notify-list { max-height: 360px; overflow: auto; } .scp-notify-item { display: grid; grid-template-columns: 32px 1fr auto; gap: 10px; padding: 12px 14px; border-bottom: 1px solid var(--border-soft); align-items: flex-start; } .scp-notify-item:last-child { border-bottom: 0; } .scp-notify-item.unread { background: oklch(0.20 0.020 250 / .5); } .scp-notify-item .ico { width: 32px; height: 32px; border-radius: 9px; display: grid; place-items: center; } .scp-notify-item .ico.tone-emerald { background: oklch(0.30 0.06 160 / .6); color: oklch(0.86 0.14 160); } .scp-notify-item .ico.tone-amber { background: oklch(0.30 0.06 78 / .6); color: oklch(0.88 0.13 78); } .scp-notify-item .ico.tone-sky { background: oklch(0.30 0.06 230 / .6); color: oklch(0.86 0.12 230); } .scp-notify-item .ico.tone-coral { background: oklch(0.30 0.06 25 / .6); color: oklch(0.86 0.16 25); } .scp-notify-item .body .t { font-size: 12.5px; font-weight: 600; color: var(--fg); line-height: 1.35; } .scp-notify-item .body .s { font-size: 11.5px; color: var(--fg-3); margin-top: 2px; } .scp-notify-item .at { font-size: 11px; color: var(--fg-4); } .scp-notify-foot { display: flex; justify-content: space-between; padding: 10px 14px; border-top: 1px solid var(--border-soft); } .scp-notify-foot button { font-size: 12px; color: var(--fg-3); font-weight: 500; } .scp-notify-foot button:hover { color: var(--fg); } `; document.head.appendChild(s); })(); Object.assign(window, { Sidebar, TopBar });