const USER_ROLES = [ { key: 'Administrador', tone: 'emerald', desc: 'Acesso total · gerencia usuários, configurações, IA, plano' }, { key: 'Comprador', tone: 'sky', desc: 'Cotações, fornecedores e pedidos · sem configurações' }, { key: 'Estoquista', tone: 'amber', desc: 'Estoque, recebimentos e leitura de pedidos' }, { key: 'Visualizador', tone: 'violet', desc: 'Apenas leitura · sem permissão para ações' }, ]; function ScreenSettings() { const { COMPANY } = window.SCP; const [tab, setTab] = useState('empresa'); const [apiKey, setApiKey] = useState('sk-proj-••••••••••••••••••••••••••••••••'); const [showKey, setShowKey] = useState(false); const [notif, setNotif] = useState({ cotacoes: true, pedidos: true, estoque: true, recebimentos: true, ia: true, whatsFornec: true, whatsVoce: false, diario: true, semanal: true, }); const [users, setUsers] = useState([]); const [usersLoading, setUsersLoading] = useState(true); const [usersErr, setUsersErr] = useState(null); const [userModal, setUserModal] = useState(null); // null | { mode: 'new' } | { mode: 'edit', user } const [highlightUserId, setHighlightUserId] = useState(null); // Carrega profiles na primeira vez que a aba Usuários é aberta useEffect(() => { if (tab !== 'usuarios') return; if (users.length > 0 && !usersErr) return; loadUsers(); }, [tab]); const loadUsers = async () => { setUsersLoading(true); setUsersErr(null); try { const data = await window.scpDb.profiles.list(); // Adapta DB → UI shape (role enum → label) const roleLabel = { admin: 'Administrador', comprador: 'Comprador', estoquista: 'Estoquista', visualizador: 'Visualizador' }; setUsers(data.map(u => ({ id: u.id, name: u.name, email: u.email, role: roleLabel[u.role] || 'Visualizador', role_db: u.role, active: u.active, last: u.last_seen_at ? new Date(u.last_seen_at).toLocaleString('pt-BR') : 'nunca', }))); } catch (e) { setUsersErr(e.message || 'Falha ao carregar usuários'); } finally { setUsersLoading(false); } }; const roleTone = (role) => (USER_ROLES.find(r => r.key === role)?.tone) || 'fg'; const roleToDb = { 'Administrador': 'admin', 'Comprador': 'comprador', 'Estoquista': 'estoquista', 'Visualizador': 'visualizador' }; const handleSaveUser = async (data) => { try { if (userModal?.mode === 'edit') { await window.scpDb.profiles.update(userModal.user.id, { name: data.name, role: roleToDb[data.role], }); window.scpToast('Usuário atualizado', { kind: 'emerald', sub: data.name }); setHighlightUserId(userModal.user.id); } else { // Cria novo via signUp + atualiza profile const result = await window.scpDb.profiles.invite({ name: data.name, email: data.email, password: data.password, role: roleToDb[data.role], }); const msg = result.needsConfirmation ? `Convite enviado · ${data.email}` : `Usuário criado · ${data.email}`; const sub = result.needsConfirmation ? 'Pediu pra confirmar e-mail. Verifique a caixa de entrada do convidado.' : `Senha temporária: ${data.password}`; window.scpToast(msg, { kind: 'emerald', sub }); setHighlightUserId(result.user.id); } await loadUsers(); setUserModal(null); setTimeout(() => setHighlightUserId(null), 2400); } catch (e) { window.scpToast('Erro ao salvar', { kind: 'coral', sub: e.message }); } }; const handleDeleteUser = async (u) => { if (u.role === 'Administrador' && users.filter(x => x.role === 'Administrador' && x.active).length === 1) { window.scpToast('Não é possível desativar o único Administrador', { kind: 'coral' }); return; } if (!confirm(`Desativar "${u.name}"? Ele não poderá mais acessar o sistema.`)) return; try { await window.scpDb.profiles.deactivate(u.id); await loadUsers(); window.scpToast('Usuário desativado', { kind: 'amber', sub: u.name }); } catch (e) { window.scpToast('Erro ao desativar', { kind: 'coral', sub: e.message }); } }; const TABS = [ { k: 'empresa', l: 'Empresa', i: }, { k: 'usuarios', l: 'Usuários', i: }, { k: 'ia', l: 'Inteligência IA', i: }, { k: 'notificacoes', l: 'Notificações', i: }, { k: 'seguranca', l: 'Segurança', i: }, ]; return (
{/* sidebar tabs */}
{TABS.map(t => ( ))}
{tab === 'empresa' && (

Dados da empresa

Informações exibidas nos pedidos de compra e comunicações com fornecedores.

Logo da empresa PNG ou SVG · exibida nos pedidos de compra
Salvar alterações
)} {tab === 'usuarios' && (

Usuários e permissões

Gerencie os perfis que podem acessar o sistema. O fornecedor não precisa de cadastro.

{usersErr && ( Falha ao carregar usuários:{' '} {usersErr}
Se a função get_user_emails ainda não foi criada, rode supabase/function-user-emails.sql.
)} {usersLoading && ( )} {!usersLoading && users.length === 0 && ( )} {!usersLoading && users.map(u => { const tone = roleTone(u.role); return ( ); })}
UsuárioPerfilÚltimo acessoStatus
Carregando usuários…
Nenhum usuário cadastrado.
{u.name}
{u.email}
{u.role} {u.last} {u.active ? 'Ativo' : 'Inativo'}
} size="sm" kind="ghost" title="Editar" onClick={() => setUserModal({ mode: 'edit', user: u })}/> } size="sm" kind="ghost" title="Remover" onClick={() => handleDeleteUser(u)}/>
} onClick={() => setUserModal({ mode: 'new' })}>Convidar usuário {userModal && ( u.email)} onClose={() => setUserModal(null)} onSave={handleSaveUser} /> )}
)} {tab === 'ia' && (

Integração com IA · OpenAI

Configure sua chave de API da OpenAI para habilitar a análise inteligente de cotações (Módulo 5).

API conectada · gpt-4o · 847 análises realizadas

Critérios da análise

{[ { l: 'Peso: Preço', v: 60 }, { l: 'Peso: Confiabilidade', v: 25 }, { l: 'Peso: Prazo', v: 15 }, ].map(w => (
{w.l} {w.v}%
))}
Salvar configurações de IA
)} {tab === 'notificacoes' && (

Notificações automáticas

Configure quando e como o sistema deve alertar você e seus fornecedores.

{[ { g: 'Operação', k: 'cotacoes', t: 'Cotações e lances', s: 'Novas respostas, vencimento de prazo, lembretes', icon: }, { g: 'Operação', k: 'pedidos', t: 'Pedidos de compra', s: 'Confirmação, trânsito, entrega e cancelamento', icon: }, { g: 'Operação', k: 'estoque', t: 'Alertas de estoque', s: 'Itens abaixo do mínimo e sugestão de cotação', icon: }, { g: 'Operação', k: 'recebimentos', t: 'Recebimentos com divergência', s: 'Conferência pendente, item faltando, validade', icon: }, { g: 'Inteligência', k: 'ia', t: 'Recomendações da IA', s: 'Fechar cotação, abrir nova, otimizações', icon: }, { g: 'WhatsApp', k: 'whatsFornec', t: 'WhatsApp para fornecedores', s: 'Aviso de cotação e lembrete de prazo', icon: }, { g: 'WhatsApp', k: 'whatsVoce', t: 'WhatsApp para você', s: 'Eventos críticos no seu WhatsApp pessoal', icon: }, { g: 'Resumos', k: 'diario', t: 'Resumo diário por e-mail', s: 'Tudo que aconteceu no sistema · 18h', icon: }, { g: 'Resumos', k: 'semanal', t: 'Resumo semanal por e-mail', s: 'Métricas da semana toda segunda às 09h', icon: }, ].reduce((acc, n) => { const last = acc[acc.length - 1]; if (!last || last.g !== n.g) acc.push({ g: n.g, items: [n] }); else last.items.push(n); return acc; }, []).map(group => (
{group.g} {group.items.map(n => (
{n.icon}
{n.t} {n.s}
setNotif(p => ({...p, [n.k]: v}))}/>
))}
))}
{ const ativos = Object.values(notif).filter(Boolean).length; window.scpToast('Preferências salvas', { kind: 'emerald', sub: `${ativos} canais de notificação ativos` }); }}>Salvar preferências
)} {tab === 'seguranca' && (

Segurança e acesso

Autenticação, logs de acesso e configurações de segurança da conta.

Últimos acessos

{[ { ip: '187.45.12.88', loc: 'São José do Rio Preto · SP', when: 'agora', browser: 'Chrome 124' }, { ip: '187.45.12.88', loc: 'São José do Rio Preto · SP', when: 'ontem 18:42', browser: 'Safari iOS' }, { ip: '177.32.66.11', loc: 'São Paulo · SP', when: '11/05 09:12', browser: 'Chrome 124' }, ].map((a, i) => (
{a.ip} {a.loc} {a.when} · {a.browser}
))}
Salvar
)}
); } (function() { if (document.getElementById('scp-settings-css')) return; const s = document.createElement('style'); s.id = 'scp-settings-css'; s.textContent = ` .scp-settings-layout { display: grid; grid-template-columns: 220px 1fr; gap: 16px; } .scp-settings-nav { display: flex; flex-direction: column; gap: 2px; } .scp-snav { display: flex; align-items: center; gap: 10px; padding: 9px 12px; border-radius: 10px; color: var(--fg-3); font-size: 13px; font-weight: 500; transition: background .12s, color .12s; text-align: left; } .scp-snav .ico { display: inline-flex; } .scp-snav:hover { background: var(--surface-2); color: var(--fg); } .scp-snav.active { background: oklch(0.22 0.05 160 / .55); color: oklch(0.92 0.14 160); box-shadow: inset 0 0 0 1px oklch(0.30 0.06 160 / .5); } .scp-settings-body > * { display: flex; flex-direction: column; gap: 14px; } .scp-sfm { display: flex; flex-direction: column; gap: 18px; } .scp-sfm h3 { margin: 0; font-size: 22px; font-weight: 600; letter-spacing: -0.02em; } .scp-sfm > p { margin: -6px 0; color: var(--fg-3); font-size: 13.5px; } .scp-sfm h4 { margin: 0; font-size: 13px; font-weight: 600; color: var(--fg); } .scp-sfield-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; } .scp-sfield-grid .scp-input { width: 100%; } .scp-sfield-upload { margin-top: 4px; } .scp-logo-box { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px; width: 180px; height: 100px; border-radius: 12px; border: 2px dashed var(--border-strong); color: var(--fg-4); font-size: 12px; cursor: pointer; transition: border-color .15s, color .15s; } .scp-logo-box:hover { border-color: oklch(0.55 0.10 160); color: var(--fg-2); } .scp-logo-box small { font-size: 10.5px; color: var(--fg-4); } .scp-ia-setup { display: flex; flex-direction: column; gap: 12px; padding: 16px; background: var(--surface-2); border-radius: 12px; border: 1px solid var(--border-soft); } .scp-ia-status { display: flex; align-items: center; gap: 8px; padding: 8px 12px; border-radius: 8px; font-size: 12.5px; font-weight: 600; } .scp-ia-status.connected { background: oklch(0.26 0.06 160 / .5); color: oklch(0.92 0.13 160); } .scp-ia-prefs { padding: 16px; background: var(--surface-2); border-radius: 12px; border: 1px solid var(--border-soft); display: flex; flex-direction: column; gap: 12px; } .scp-ia-weights { display: flex; flex-direction: column; gap: 8px; } .weight-row { display: grid; grid-template-columns: 160px 1fr 40px; gap: 12px; align-items: center; font-size: 12.5px; color: var(--fg-2); } .weight-row input[type=range] { accent-color: oklch(0.74 0.16 160); } .weight-row .num { font-family: 'JetBrains Mono', monospace; font-weight: 600; color: var(--fg); } .scp-notif-grid { display: flex; flex-direction: column; gap: 14px; } .scp-notif-group { display: flex; flex-direction: column; gap: 4px; } .scp-notif-group-title { font-size: 10.5px; font-weight: 700; color: var(--fg-3); text-transform: uppercase; letter-spacing: .14em; padding: 0 4px 4px; } .scp-notif-row { display: grid; grid-template-columns: 28px 1fr auto; align-items: center; padding: 14px 16px; border-radius: 10px; background: var(--surface-2); border: 1px solid var(--border-soft); gap: 14px; } .scp-notif-row .t { display: block; font-size: 13px; font-weight: 600; color: var(--fg); } .scp-notif-row .s { display: block; font-size: 11.5px; color: var(--fg-3); margin-top: 2px; } .scp-notif-icon { width: 28px; height: 28px; border-radius: 8px; display: grid; place-items: center; background: oklch(0.30 0.04 160 / .35); color: oklch(0.86 0.14 160); } .scp-altercoes { display: flex; flex-direction: column; gap: 8px; } .alt-row { display: flex; justify-content: space-between; align-items: center; padding: 12px 14px; background: var(--surface-2); border-radius: 10px; border: 1px solid var(--border-soft); gap: 16px; } .alt-row .t { display: block; font-size: 13px; font-weight: 600; color: var(--fg); } .alt-row .s { display: block; font-size: 11.5px; color: var(--fg-3); margin-top: 2px; } .scp-suporte { padding: 16px; background: var(--surface-2); border-radius: 12px; border: 1px solid var(--border-soft); } `; document.head.appendChild(s); })(); function generateTempPassword() { const chars = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'; let pw = ''; for (let i = 0; i < 12; i++) pw += chars[Math.floor(Math.random() * chars.length)]; return pw; } function UserModal({ mode, initial, existingEmails, onClose, onSave }) { const [name, setName] = useState(initial?.name || ''); const [email, setEmail] = useState(initial?.email || ''); const [role, setRole] = useState(initial?.role || 'Comprador'); const [password, setPassword] = useState(generateTempPassword()); const [saving, setSaving] = useState(false); const isEdit = mode === 'edit'; const emailValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()); const emailNorm = email.trim().toLowerCase(); const dup = !isEdit && existingEmails.includes(emailNorm); const pwValid = isEdit || password.length >= 8; const canSubmit = !saving && name.trim().length >= 2 && emailValid && !dup && pwValid; const submit = async () => { if (!canSubmit) return; setSaving(true); try { await onSave({ name: name.trim(), email: emailNorm, role, password }); } finally { setSaving(false); } }; return ReactDOM.createPortal((
e.stopPropagation()}>

{isEdit ? 'Editar usuário' : 'Convidar usuário'}

} size="sm" onClick={onClose}/>
Nome completo * setName(e.target.value)} autoFocus />
E-mail * setEmail(e.target.value)} type="email" disabled={isEdit} /> {email && !emailValid && E-mail inválido} {dup && Este e-mail já está cadastrado}
{!isEdit && (
Senha temporária *
setPassword(e.target.value)} type="text" /> setPassword(generateTempPassword())} type="button">Gerar nova
O usuário pode trocar depois. Mínimo 8 caracteres.
)}
Perfil de acesso
{USER_ROLES.map(r => ( ))}
{isEdit ? 'As alterações entram em vigor no próximo login' : 'O usuário receberá um convite por e-mail'}
Cancelar }> {isEdit ? 'Salvar alterações' : 'Enviar convite'}
), document.body); } (function() { if (document.getElementById('scp-users-css')) return; const s = document.createElement('style'); s.id = 'scp-users-css'; s.textContent = ` .scp-users-table { background: var(--surface-2); border-radius: 12px; border: 1px solid var(--border-soft); overflow: hidden; } .scp-users-table tbody tr.just-added td { background: oklch(0.26 0.06 160 / .35); animation: justAddedPulseUser 2.4s ease-out; } @keyframes justAddedPulseUser { 0% { background: oklch(0.34 0.10 160 / .55); box-shadow: inset 3px 0 0 oklch(0.78 0.16 160); } 100% { background: oklch(0.235 0.014 250); box-shadow: inset 3px 0 0 transparent; } } .scp-role-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } .scp-role-card { display: flex; flex-direction: column; gap: 6px; padding: 12px 14px; border-radius: 12px; background: var(--surface-2); border: 1px solid var(--border-soft); text-align: left; transition: background .12s, border-color .12s, transform .08s; cursor: pointer; } .scp-role-card:hover { background: var(--surface-3); border-color: var(--border); } .scp-role-card .head { display: flex; align-items: center; gap: 8px; } .scp-role-card .head strong { font-size: 13.5px; font-weight: 600; color: var(--fg); flex: 1; } .scp-role-card .head svg { color: oklch(0.86 0.14 160); } .role-dot { width: 8px; height: 8px; border-radius: 999px; } .role-dot.tone-emerald { background: oklch(0.78 0.16 160); box-shadow: 0 0 0 3px oklch(0.78 0.16 160 / .2); } .role-dot.tone-sky { background: oklch(0.76 0.12 230); box-shadow: 0 0 0 3px oklch(0.76 0.12 230 / .2); } .role-dot.tone-amber { background: oklch(0.84 0.14 78); box-shadow: 0 0 0 3px oklch(0.84 0.14 78 / .2); } .role-dot.tone-violet { background: oklch(0.74 0.14 285); box-shadow: 0 0 0 3px oklch(0.74 0.14 285 / .2); } .scp-role-card .desc { font-size: 11.5px; color: var(--fg-3); line-height: 1.4; } .scp-role-card.active.tone-emerald { background: oklch(0.26 0.06 160 / .55); border-color: oklch(0.50 0.10 160 / .65); } .scp-role-card.active.tone-sky { background: oklch(0.26 0.06 230 / .45); border-color: oklch(0.50 0.10 230 / .65); } .scp-role-card.active.tone-amber { background: oklch(0.26 0.06 78 / .45); border-color: oklch(0.50 0.10 78 / .65); } .scp-role-card.active.tone-violet { background: oklch(0.26 0.06 285 / .45); border-color: oklch(0.50 0.10 285 / .65); } .scp-role-card.active .desc { color: var(--fg-2); } `; document.head.appendChild(s); })(); window.ScreenSettings = ScreenSettings;