/* Shell da Máquina Comercial Pessoal: sessão, abas, WhatsApp e configurações. */ (async function init() { let usuarioAtual; try { ({ user: usuarioAtual } = await API.get('/api/auth/me')); } catch { window.location.href = '/login.html'; return; } const isAdmin = usuarioAtual.role === 'admin'; let manualLeadContext = null; let whatsappPollTimer = null; let whatsappGeneration = 0; let whatsappPollBusyGeneration = null; let lastQrExpiry = null; const pollingStates = new Set(['STARTING', 'QR_REQUIRED', 'AUTHENTICATING', 'RECONNECTING']); const stateLabels = { STOPPED: 'Parado', STARTING: 'Iniciando', QR_REQUIRED: 'QR necessário', AUTHENTICATING: 'Autenticando', READY: 'Conectado', RECONNECTING: 'Reconectando', DISCONNECTED: 'Desconectado', ERROR: 'Erro' }; el('usuario-logado').textContent = `${usuarioAtual.username} (${isAdmin ? 'admin' : 'comercial'})`; el('btn-usuarios').classList.toggle('hidden', !isAdmin); for (const control of document.querySelectorAll('.admin-only')) control.classList.toggle('hidden', !isAdmin); el('btn-sair').addEventListener('click', async () => { const button = el('btn-sair'); await comCarregando(button, async () => { await API.post('/api/auth/logout'); window.location.href = '/login.html'; }); }); async function carregarListaUsuarios() { const { users } = await API.get('/api/users'); el('lista-usuarios').replaceChildren(...users.map((user) => h('div', { class: 'row space-between' }, h('span', {}, user.username), h('span', { class: 'muted' }, user.role === 'admin' ? 'Administrador' : 'Comercial')))); } el('btn-usuarios').addEventListener('click', async () => { const button = el('btn-usuarios'); el('modal-usuarios').showModal(); await comCarregando(button, async () => { try { await carregarListaUsuarios(); } catch (error) { toast(`Erro: ${error.message}`); } }); }); el('btn-fechar-usuarios').addEventListener('click', () => el('modal-usuarios').close()); el('form-novo-usuario').addEventListener('submit', async (event) => { event.preventDefault(); const form = el('form-novo-usuario'); const button = form.querySelector('button[type=submit]'); await comCarregando(button, async () => { try { await API.post('/api/users', { username: el('novo-usuario-username').value, password: el('novo-usuario-password').value, role: el('novo-usuario-role').value }); el('novo-usuario-username').value = ''; el('novo-usuario-password').value = ''; await carregarListaUsuarios(); toast('Usuário criado'); } catch (error) { toast(`Erro: ${error.message}`); } }); }); for (const tab of document.querySelectorAll('.tab')) { tab.addEventListener('click', () => { for (const item of document.querySelectorAll('.tab')) { item.classList.toggle('active', item === tab); item.setAttribute('aria-selected', item === tab ? 'true' : 'false'); } for (const panel of document.querySelectorAll('.tab-panel')) { panel.classList.toggle('active', panel.id === `tab-${tab.dataset.tab}`); } if (tab.dataset.tab === 'tratamento') Tratamento.ativar(); if (tab.dataset.tab === 'campanhas') Campanhas.ativar(); }); } function stopWhatsAppPolling() { clearTimeout(whatsappPollTimer); whatsappPollTimer = null; } function ownsWhatsAppGeneration(generation) { return generation === whatsappGeneration && el('modal-settings').open; } function nextWhatsAppGeneration() { stopWhatsAppPolling(); whatsappGeneration += 1; return whatsappGeneration; } function scheduleWhatsAppPolling(state, generation) { stopWhatsAppPolling(); if (!ownsWhatsAppGeneration(generation)) return; const delay = pollingStates.has(state) ? 1000 : 5000; whatsappPollTimer = setTimeout(() => refreshWhatsAppStatus(true, generation), delay); } function renderWhatsAppStatus(status, generation) { if (!ownsWhatsAppGeneration(generation)) return false; const state = status?.state || 'STOPPED'; const stateEl = el('whatsapp-state'); stateEl.textContent = stateLabels[state] || state; stateEl.className = `chip connection-state state-${state.toLowerCase()}`; const connectedNumber = status?.number || status?.numero || status?.phone || status?.connectedNumber; el('whatsapp-number').textContent = connectedNumber ? `Número conectado: ${connectedNumber}` : ''; el('whatsapp-number').classList.toggle('hidden', !connectedNumber); el('whatsapp-error').textContent = status?.error || status?.erro || ''; el('whatsapp-error').classList.toggle('hidden', !(status?.error || status?.erro)); if (state !== 'QR_REQUIRED') hideQr(); return true; } function hideQr() { el('whatsapp-qr').removeAttribute('src'); el('whatsapp-qr').classList.add('hidden'); el('whatsapp-qr-placeholder').classList.remove('hidden'); el('whatsapp-qr-expiry').textContent = ''; lastQrExpiry = null; } async function loadQr(generation) { if (!isAdmin || !ownsWhatsAppGeneration(generation)) return; try { const { qr, expiresAt } = await API.get('/api/whatsapp/qr'); if (!ownsWhatsAppGeneration(generation)) return; if (expiresAt !== lastQrExpiry || !el('whatsapp-qr').getAttribute('src')) { el('whatsapp-qr').src = qr; lastQrExpiry = expiresAt; } el('whatsapp-qr').classList.remove('hidden'); el('whatsapp-qr-placeholder').classList.add('hidden'); el('whatsapp-qr-expiry').textContent = expiresAt ? `Expira em ${dataBr(expiresAt)}` : ''; } catch (error) { if (!ownsWhatsAppGeneration(generation)) return; el('whatsapp-qr-expiry').textContent = error.message; } } async function refreshWhatsAppStatus(fromPoll = false, generation = whatsappGeneration) { if (!ownsWhatsAppGeneration(generation) || whatsappPollBusyGeneration === generation) return; whatsappPollBusyGeneration = generation; try { const status = await API.get('/api/whatsapp/status'); if (!renderWhatsAppStatus(status, generation)) return; if (status.state === 'QR_REQUIRED') await loadQr(generation); scheduleWhatsAppPolling(status.state || 'STOPPED', generation); } catch (error) { if (!renderWhatsAppStatus({ state: 'ERROR', error: error.message }, generation)) return; if (!fromPoll) toast(`WhatsApp: ${error.message}`); scheduleWhatsAppPolling('ERROR', generation); } finally { if (whatsappPollBusyGeneration === generation) whatsappPollBusyGeneration = null; } } async function whatsappAction(path) { const generation = nextWhatsAppGeneration(); try { const result = await API.post(path); if (!ownsWhatsAppGeneration(generation)) return; const status = result.status || await API.get('/api/whatsapp/status'); if (!renderWhatsAppStatus(status, generation)) return; if (status.state === 'QR_REQUIRED') await loadQr(generation); scheduleWhatsAppPolling(status.state || 'STOPPED', generation); } catch (error) { if (!ownsWhatsAppGeneration(generation)) return; toast(`WhatsApp: ${error.message}`); await refreshWhatsAppStatus(false, generation); } } el('btn-whatsapp-connect').addEventListener('click', () => comCarregando(el('btn-whatsapp-connect'), () => whatsappAction('/api/whatsapp/connect'))); el('btn-whatsapp-refresh-qr').addEventListener('click', () => comCarregando(el('btn-whatsapp-refresh-qr'), () => whatsappAction('/api/whatsapp/refresh-qr'))); el('btn-whatsapp-disconnect').addEventListener('click', () => comCarregando(el('btn-whatsapp-disconnect'), () => whatsappAction('/api/whatsapp/disconnect'))); el('btn-whatsapp-logout').addEventListener('click', async () => { if (!window.confirm('Sair do WhatsApp e apagar a sessão vinculada neste computador?')) return; await comCarregando(el('btn-whatsapp-logout'), () => whatsappAction('/api/whatsapp/logout')); }); function renderLeadContext() { if (!manualLeadContext) { el('manual-lead-context').textContent = 'Envio avulso, sem lead selecionado.'; return; } const lead = manualLeadContext; el('manual-lead-context').textContent = [ `Lead: ${lead.empresa || lead.legal_name || 'Empresa não informada'}`, lead.cidade ? `${lead.cidade}${lead.uf ? `/${lead.uf}` : ''}` : '', lead.segmento_label || lead.segmento || '', lead.cnpj || '' ].filter(Boolean).join(' · '); } window.MaquinaSelecionarLeadManual = (lead) => { manualLeadContext = lead || null; renderLeadContext(); }; window.MaquinaLimparLeadManual = () => { manualLeadContext = null; renderLeadContext(); }; el('btn-limpar-lead-manual').addEventListener('click', window.MaquinaLimparLeadManual); async function carregarOutreach() { const [{ settings }, { templates }, status] = await Promise.all([ API.get('/api/settings'), API.get('/api/outreach/templates'), API.get('/api/outreach/status') ]); el('outreach-ativo').checked = settings['outreach.ativo'] === 'true'; el('outreach-limite').value = settings['outreach.mensagensPorDia'] || 10; el('outreach-status').textContent = `Consumidos do limite hoje: ${status.enviadosHoje}/${status.mensagensPorDia}`; if (!templates.length) { el('outreach-templates').replaceChildren(h('p', { class: 'muted' }, 'Nenhum template disponível. Eles aparecerão após configurar seus serviços.')); } else { el('outreach-templates').replaceChildren(...templates.map((template) => h('div', { class: 'item outreach-template', 'data-produto': template.produto }, h('div', { class: 'row space-between' }, h('strong', {}, template.produto), h('label', { class: 'radio' }, h('input', { type: 'checkbox', class: 'outreach-template-ativo', checked: template.ativo ? '' : null }), ' Ativo')), h('textarea', { class: 'outreach-template-mensagem', rows: 3 }, template.mensagem || '')))); } const sendStatusLabels = { pendente: 'Preparando', aceito: 'Aguardando confirmação', enviado: 'Enviado ao servidor', entregue: 'Entregue', lido: 'Lido', desconhecido: 'Resultado desconhecido', erro: 'Falhou' }; el('outreach-ultimos').replaceChildren(...status.ultimos.map((send) => h('div', { class: 'item' }, h('div', {}, `${sendStatusLabels[send.status] || send.status} · ${send.numero || send.empresa || 'número não informado'}`), h('div', { class: 'sub' }, `${dataBr(send.sent_at)}${send.erro ? ` · ${send.erro}` : ''}`)))); if (!status.ultimos.length) el('outreach-ultimos').replaceChildren(h('p', { class: 'muted' }, 'Nenhum envio ainda.')); } async function carregarOptOuts() { const { optOuts } = await API.get('/api/outreach/opt-outs'); const list = el('optout-list'); if (!optOuts.length) { list.replaceChildren(h('p', { class: 'muted' }, 'Nenhum número bloqueado.')); return; } list.replaceChildren(...optOuts.map((item) => { let rowButton; rowButton = h('button', { class: 'btn-danger btn-sm', type: 'button', onclick: () => comCarregando(rowButton, () => removerOptOut(item.numero)) }, 'Remover'); return h('div', { class: 'item opt-out-item' }, h('div', {}, h('strong', {}, item.numero), h('div', { class: 'sub' }, `${item.motivo || 'Sem motivo'} · ${dataBr(item.created_at)}`)), rowButton); })); } async function removerOptOut(numero) { try { await API.delete(`/api/outreach/opt-outs/${encodeURIComponent(numero)}`); el('optout-status').textContent = 'Número removido da lista.'; await carregarOptOuts(); } catch (error) { el('optout-status').textContent = `Erro: ${error.message}`; } } el('btn-optout-add').addEventListener('click', async () => { const button = el('btn-optout-add'); await comCarregando(button, async () => { try { await API.post('/api/outreach/opt-outs', { numero: el('optout-numero').value.trim(), motivo: el('optout-motivo').value.trim() }); el('optout-numero').value = ''; el('optout-motivo').value = ''; el('optout-status').textContent = 'Número adicionado à lista.'; await carregarOptOuts(); } catch (error) { el('optout-status').textContent = `Erro: ${error.message}`; } }); }); el('btn-settings').addEventListener('click', async () => { const button = el('btn-settings'); el('modal-settings').showModal(); const generation = nextWhatsAppGeneration(); await comCarregando(button, async () => { try { await Promise.all([carregarOutreach(), carregarOptOuts(), refreshWhatsAppStatus(false, generation)]); } catch (error) { toast(`Erro ao carregar configurações: ${error.message}`); } }); }); el('btn-enviar-manual').addEventListener('click', async () => { const button = el('btn-enviar-manual'); if (button.disabled) return; button.disabled = true; const statusEl = el('manual-status'); statusEl.classList.remove('status-ok', 'status-error'); statusEl.textContent = 'Enviando...'; try { const result = await API.post('/api/outreach/enviar-manual', { numero: el('manual-numero').value.trim(), mensagem: el('manual-mensagem').value, leadId: manualLeadContext?.id || null, lead: manualLeadContext }); const uncertain = result.status === 'resultado_desconhecido' || result.status === 'persistencia_incerta'; const warning = Boolean(result.aviso); statusEl.textContent = uncertain || warning ? result.aviso : result.ok ? `Aceito pelo WhatsApp; aguardando confirmação${result.messageId ? ` · ID ${result.messageId}` : ''}` : result.erro; statusEl.classList.toggle('status-ok', Boolean(result.ok) && !warning); statusEl.classList.toggle('status-error', !result.ok || warning); await carregarOutreach(); } catch (error) { statusEl.textContent = `Erro: ${error.message}`; statusEl.classList.add('status-error'); } finally { button.disabled = false; } }); el('btn-toggle-outreach-log').addEventListener('click', () => { const hidden = el('outreach-ultimos').classList.toggle('hidden'); el('btn-toggle-outreach-log').textContent = hidden ? 'Ver log de envios' : 'Ocultar log de envios'; }); el('btn-salvar-outreach').addEventListener('click', async () => { const button = el('btn-salvar-outreach'); await comCarregando(button, async () => { try { for (const item of el('outreach-templates').querySelectorAll('.outreach-template')) { await API.put(`/api/outreach/templates/${encodeURIComponent(item.dataset.produto)}`, { mensagem: item.querySelector('.outreach-template-mensagem').value, ativo: item.querySelector('.outreach-template-ativo').checked }); } await API.put('/api/settings', { 'outreach.ativo': el('outreach-ativo').checked ? 'true' : 'false', 'outreach.mensagensPorDia': el('outreach-limite').value }); await carregarOutreach(); toast('Templates salvos'); } catch (error) { toast(`Erro: ${error.message}`); } }); }); el('btn-alterar-senha').addEventListener('click', async () => { const button = el('btn-alterar-senha'); const status = el('senha-status'); const novaSenha = el('senha-nova').value; if (novaSenha !== el('senha-confirmacao').value) { status.textContent = 'A confirmação não corresponde à nova senha.'; return; } await comCarregando(button, async () => { try { const result = await API.put('/api/auth/password', { senhaAtual: el('senha-atual').value, novaSenha }); el('senha-atual').value = ''; el('senha-nova').value = ''; el('senha-confirmacao').value = ''; status.textContent = 'Senha alterada. Entre novamente.'; if (result.relogin) window.location.href = '/login.html'; } catch (error) { status.textContent = `Erro: ${error.message}`; } }); }); el('btn-salvar-settings').addEventListener('click', async (event) => { event.preventDefault(); const button = el('btn-salvar-settings'); await comCarregando(button, async () => { try { await API.put('/api/settings', { 'outreach.ativo': el('outreach-ativo').checked ? 'true' : 'false', 'outreach.mensagensPorDia': el('outreach-limite').value }); stopWhatsAppPolling(); el('modal-settings').close(); toast('Configurações salvas'); } catch (error) { toast(`Erro: ${error.message}`); } }); }); el('modal-settings').addEventListener('close', nextWhatsAppGeneration); el('btn-limpar-base').addEventListener('click', () => { el('reset-base-senha').value = ''; el('reset-base-erro').classList.add('hidden'); stopWhatsAppPolling(); el('modal-settings').close(); el('modal-reset-base').showModal(); }); el('btn-cancelar-reset-base').addEventListener('click', () => el('modal-reset-base').close()); el('btn-confirmar-reset-base').addEventListener('click', async () => { const button = el('btn-confirmar-reset-base'); const erroEl = el('reset-base-erro'); const senha = el('reset-base-senha').value; if (!senha) { erroEl.textContent = 'Informe a senha de segurança'; erroEl.classList.remove('hidden'); return; } await comCarregando(button, async () => { try { await API.post('/api/admin/reset', { senha }); el('modal-reset-base').close(); toast('Base de leads limpa com sucesso. Recarregando...'); setTimeout(() => window.location.reload(), 1200); } catch (error) { erroEl.textContent = `Erro: ${error.message}`; erroEl.classList.remove('hidden'); } }); }); for (const dialog of document.querySelectorAll('dialog')) { dialog.addEventListener('click', (event) => { if (event.target === dialog) dialog.close(); }); } renderLeadContext(); Tratamento.bindEvents(); Campanhas.bindEvents(); try { await Captura.init(); } catch (error) { toast(`Erro ao carregar: ${error.message}`, 8000); } })();