diff --git a/manager/dist/assets/test-interactive.js b/manager/dist/assets/test-interactive.js new file mode 100644 index 0000000000..5f2adee99b --- /dev/null +++ b/manager/dist/assets/test-interactive.js @@ -0,0 +1,448 @@ +/* eslint-disable */ +/** + * Painel de testes para envio de mensagens interativas (Botões, Lista, Carrossel). + * Injeta um botão "Testar Interativo" em cada card de instância no manager. + * Fallback: se nenhum card for detectado, mostra um botão flutuante (FAB). + */ +(function () { + 'use strict'; + + if (window.__evoTestInteractive) return; + window.__evoTestInteractive = true; + + const STYLE = ` + .evo-test-btn { + display: inline-flex; align-items: center; gap: 6px; + padding: 6px 10px; margin: 4px; + font-size: 12px; font-weight: 600; line-height: 1; + background: #6e44ff; color: #fff; border: none; border-radius: 6px; + cursor: pointer; box-shadow: 0 1px 2px rgba(0,0,0,.15); + transition: background .15s; + } + .evo-test-btn:hover { background: #5a36d6; } + .evo-test-fab { + position: fixed; right: 24px; bottom: 24px; z-index: 999998; + padding: 12px 16px; font-size: 14px; font-weight: 700; + background: #6e44ff; color: #fff; border: none; border-radius: 999px; + cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,.25); + } + .evo-test-overlay { + position: fixed; inset: 0; background: rgba(0,0,0,.55); + z-index: 999999; display: flex; align-items: center; justify-content: center; + font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + } + .evo-test-modal { + width: min(720px, 92vw); max-height: 92vh; overflow: auto; + background: #fff; color: #1a1a1a; border-radius: 12px; + padding: 20px; box-shadow: 0 12px 40px rgba(0,0,0,.4); + } + .evo-test-modal h2 { margin: 0 0 4px; font-size: 18px; } + .evo-test-modal .evo-sub { color: #666; font-size: 12px; margin-bottom: 14px; } + .evo-test-tabs { display: flex; gap: 4px; border-bottom: 1px solid #e5e5e5; margin-bottom: 14px; } + .evo-test-tab { + padding: 8px 14px; border: none; background: transparent; cursor: pointer; + font-size: 13px; font-weight: 600; color: #666; border-bottom: 2px solid transparent; + } + .evo-test-tab.active { color: #6e44ff; border-color: #6e44ff; } + .evo-test-form label { display: block; font-size: 12px; font-weight: 600; margin: 10px 0 4px; color: #444; } + .evo-test-form input, .evo-test-form textarea { + width: 100%; padding: 8px 10px; border: 1px solid #d0d0d0; border-radius: 6px; + font-size: 13px; font-family: inherit; box-sizing: border-box; + } + .evo-test-form textarea { font-family: ui-monospace, "SF Mono", Consolas, monospace; min-height: 180px; resize: vertical; } + .evo-test-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; } + .evo-test-actions button { + padding: 8px 14px; font-size: 13px; font-weight: 600; border-radius: 6px; cursor: pointer; border: none; + } + .evo-test-cancel { background: #eee; color: #333; } + .evo-test-send { background: #6e44ff; color: #fff; } + .evo-test-send:disabled { opacity: .6; cursor: not-allowed; } + .evo-test-toast { + position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); + padding: 12px 18px; border-radius: 8px; font-size: 13px; color: #fff; + z-index: 9999999; box-shadow: 0 4px 12px rgba(0,0,0,.3); + } + .evo-test-toast.ok { background: #16a34a; } + .evo-test-toast.err { background: #dc2626; } + .evo-test-fab-menu { + position: fixed; right: 24px; bottom: 80px; z-index: 999998; + background: #fff; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.2); + max-height: 320px; overflow: auto; min-width: 220px; + } + .evo-test-fab-menu button { + display: block; width: 100%; padding: 10px 14px; text-align: left; + border: none; background: transparent; font-size: 13px; cursor: pointer; border-bottom: 1px solid #f0f0f0; + } + .evo-test-fab-menu button:hover { background: #f5f3ff; } + `; + + const styleEl = document.createElement('style'); + styleEl.textContent = STYLE; + document.head.appendChild(styleEl); + + // Os tipos de botão NÃO PODEM SER MISTURADOS no mesmo envio. + // Regras da API: + // - reply: 1 a 3 botões, sem misturar com CTA ou PIX + // - CTA (url/call/copy): 1 a 2 botões, sem misturar com reply ou PIX + // - pix: exatamente 1 botão isolado (payment_info) + // Por isso há abas separadas. + const TEMPLATES = { + reply: { + number: '', + title: 'Resposta Rápida', + description: 'Escolha uma das opções abaixo:', + footer: 'Evolution API', + buttons: [ + { type: 'reply', displayText: '✅ Confirmar', id: 'opt_confirm' }, + { type: 'reply', displayText: '❌ Cancelar', id: 'opt_cancel' }, + { type: 'reply', displayText: '🤔 Talvez', id: 'opt_maybe' }, + ], + }, + cta: { + number: '', + title: 'Botões CTA', + description: 'Botões de URL e copia-código (cta_url + cta_copy):', + footer: 'Máx. 2 botões CTA por mensagem', + buttons: [ + { type: 'url', displayText: '🌐 Abrir site', url: 'https://example.com' }, + { type: 'copy', displayText: '📋 Copiar PIX', copyCode: '00020126580014BR.GOV.BCB.PIX0136abc12345-6789-0000-aaaa-bbbbccccdddd5204000053039865802BR5913FULANO DE TAL6009SAO PAULO62070503***6304ABCD' }, + ], + }, + pix: { + number: '', + title: 'Pagamento via PIX', + description: 'Toque para pagar via PIX (payment_info)', + footer: 'WhatsApp Pay', + buttons: [ + { + type: 'pix', + currency: 'BRL', + name: 'Empresa Exemplo', + keyType: 'random', + key: 'abc12345-6789-0000-aaaa-bbbbccccdddd', + }, + ], + }, + list: { + number: '', + title: 'Cardápio de Teste', + description: 'Escolha um item abaixo', + footerText: 'Validade hoje', + buttonText: 'Ver opções', + sections: [ + { + title: 'Bebidas', + rows: [ + { title: 'Coca-Cola', description: 'Lata 350ml', rowId: 'coca' }, + { title: 'Suco de Laranja', description: '300ml natural', rowId: 'suco' }, + ], + }, + { + title: 'Lanches', + rows: [ + { title: 'X-Burger', description: 'Pão, carne 150g, queijo', rowId: 'xburger' }, + ], + }, + ], + }, + carousel: { + number: '', + body: 'Catálogo da semana', + cards: [ + { + body: 'Produto A', + footer: 'R$ 99,90', + imageUrl: 'https://picsum.photos/seed/a/600/400', + buttons: [{ type: 'url', displayText: 'Comprar', url: 'https://exemplo.com/a' }], + }, + { + body: 'Produto B', + footer: 'R$ 149,90', + imageUrl: 'https://picsum.photos/seed/b/600/400', + buttons: [{ type: 'url', displayText: 'Comprar', url: 'https://exemplo.com/b' }], + }, + { + body: 'Produto C', + footer: 'R$ 199,90', + imageUrl: 'https://picsum.photos/seed/c/600/400', + buttons: [{ type: 'reply', displayText: 'Quero!', id: 'prod_c' }], + }, + ], + }, + }; + + const ENDPOINT = { + reply: 'sendButtons', + cta: 'sendButtons', + pix: 'sendButtons', + list: 'sendList', + carousel: 'sendCarousel', + }; + const TAB_LABEL = { + reply: 'Reply', + cta: 'CTA', + pix: 'PIX', + list: 'Lista', + carousel: 'Carrossel', + }; + + function getApiKey() { + return localStorage.getItem('accessToken') || localStorage.getItem('token') || ''; + } + + function getApiUrl() { + const u = localStorage.getItem('apiUrl'); + return u && u !== 'undefined' && u !== 'null' ? u.replace(/\/+$/, '') : window.location.origin; + } + + async function fetchInstances() { + const apikey = getApiKey(); + if (!apikey) return []; + try { + const res = await fetch(getApiUrl() + '/instance/fetchInstances', { headers: { apikey } }); + if (!res.ok) return []; + const data = await res.json(); + const list = Array.isArray(data) ? data : (data?.instances || []); + return list + .map((it) => { + const name = it?.name || it?.instanceName || it?.instance?.instanceName; + const token = it?.token || it?.hash || it?.instance?.hash || it?.apikey; + return name ? { name, token } : null; + }) + .filter(Boolean); + } catch (e) { + console.error('[evo-test] fetchInstances error', e); + return []; + } + } + + function showToast(msg, type) { + const el = document.createElement('div'); + el.className = 'evo-test-toast ' + (type || 'ok'); + el.textContent = msg; + document.body.appendChild(el); + setTimeout(() => el.remove(), 4000); + } + + function openModal(instance) { + const overlay = document.createElement('div'); + overlay.className = 'evo-test-overlay'; + + const modal = document.createElement('div'); + modal.className = 'evo-test-modal'; + + let activeTab = 'reply'; + + const render = () => { + const tpl = TEMPLATES[activeTab]; + if (!tpl.number) tpl.number = ''; + + modal.innerHTML = ` +
POST /message/${ENDPOINT[activeTab]}/${instance.name}