189 lines
6.5 KiB
JavaScript
189 lines
6.5 KiB
JavaScript
(function(){
|
|
// Widget NPS (arquivo único).
|
|
//
|
|
// Regras do projeto (.agent):
|
|
// - sem dependências externas
|
|
// - fail-closed
|
|
// - contratos públicos estáveis
|
|
//
|
|
// Evolução: regras de negócio do cliente foram movidas para WASM (Go)
|
|
// sempre que possível. O backend continua sendo a autoridade.
|
|
|
|
const DEFAULTS = {
|
|
apiBase: '',
|
|
cooldownHours: 24,
|
|
// Data mínima para permitir abertura do modal.
|
|
// Formato ISO (data): YYYY-MM-DD (ex.: "2026-01-01").
|
|
// Se vazio, não aplica bloqueio por data.
|
|
data_minima_abertura: '',
|
|
};
|
|
|
|
function cooldownKey(produto, inquilino, usuarioCodigo){
|
|
// Prefixo de storage atualizado para o novo nome do projeto.
|
|
return `eli-nps:cooldown:${produto}:${inquilino}:${usuarioCodigo}`;
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// WASM (Go)
|
|
// ------------------------------------------------------------------
|
|
|
|
async function carregarWasm(apiBase){
|
|
// fail-closed: se o WASM não carregar, o widget não abre.
|
|
if(window.__eli_nps_wasm_ready) return true;
|
|
if(window.__eli_nps_wasm_loading) return window.__eli_nps_wasm_loading;
|
|
|
|
window.__eli_nps_wasm_loading = (async function(){
|
|
try{
|
|
// wasm_exec.js expõe global `Go`.
|
|
if(!window.Go){
|
|
await carregarScript(`${apiBase}/static/wasm_exec.js`);
|
|
}
|
|
|
|
const go = new Go();
|
|
const res = await fetch(`${apiBase}/static/e-li.nps.wasm`, {cache: 'no-cache'});
|
|
if(!res.ok) return false;
|
|
const bytes = await res.arrayBuffer();
|
|
const {instance} = await WebAssembly.instantiate(bytes, go.importObject);
|
|
go.run(instance);
|
|
return !!window.__eli_nps_wasm_ready;
|
|
}catch(e){
|
|
return false;
|
|
}
|
|
})();
|
|
|
|
return window.__eli_nps_wasm_loading;
|
|
}
|
|
|
|
function carregarScript(src){
|
|
return new Promise(function(resolve, reject){
|
|
try{
|
|
const s = document.createElement('script');
|
|
s.src = src;
|
|
s.async = true;
|
|
s.onload = function(){ resolve(); };
|
|
s.onerror = function(){ reject(new Error('script_fail')); };
|
|
document.head.appendChild(s);
|
|
}catch(e){
|
|
reject(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
function createModal(){
|
|
const host = document.createElement('div');
|
|
host.id = 'eli-nps-host';
|
|
const shadow = host.attachShadow ? host.attachShadow({mode:'open'}) : host;
|
|
|
|
const style = document.createElement('style');
|
|
style.textContent = `
|
|
.eli-nps-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:flex;align-items:center;justify-content:center;z-index:2147483647;}
|
|
/*
|
|
Responsividade do modal do widget:
|
|
- Em telas pequenas, usa quase a tela toda.
|
|
- Em telas maiores, mantém tamanho máximo confortável.
|
|
*/
|
|
.eli-nps-panel{width:min(560px, calc(100vw - 24px));height:min(540px, calc(100vh - 24px));background:#fff;border-radius:14px;overflow:hidden;box-shadow:0 12px 44px rgba(0,0,0,.35);display:flex;flex-direction:column;}
|
|
.eli-nps-header{flex:0 0 auto;display:flex;justify-content:flex-end;align-items:center;padding:10px;border-bottom:1px solid #eee;}
|
|
.eli-nps-close{border:1px solid #ddd;background:#fff;border-radius:10px;padding:8px 12px;cursor:pointer;font:600 13px system-ui;}
|
|
iframe{width:100%;flex:1 1 auto;border:0;}
|
|
|
|
@media (max-width: 480px){
|
|
.eli-nps-panel{width:calc(100vw - 16px);height:calc(100vh - 16px);border-radius:12px;}
|
|
.eli-nps-header{padding:8px;}
|
|
.eli-nps-close{padding:10px 12px;font-size:14px;}
|
|
}
|
|
`;
|
|
|
|
const backdrop = document.createElement('div');
|
|
backdrop.className = 'eli-nps-backdrop';
|
|
const panel = document.createElement('div');
|
|
panel.className = 'eli-nps-panel';
|
|
|
|
const header = document.createElement('div');
|
|
header.className = 'eli-nps-header';
|
|
|
|
const close = document.createElement('button');
|
|
close.className = 'eli-nps-close';
|
|
close.textContent = 'Fechar';
|
|
|
|
header.appendChild(close);
|
|
panel.appendChild(header);
|
|
backdrop.appendChild(panel);
|
|
shadow.appendChild(style);
|
|
shadow.appendChild(backdrop);
|
|
|
|
function destroy(){
|
|
try{ host.remove(); }catch(e){}
|
|
window.removeEventListener('message', onMsg);
|
|
}
|
|
|
|
function onMsg(ev){
|
|
if(ev && ev.data && ev.data.type === 'eli-nps:done'){
|
|
destroy();
|
|
}
|
|
}
|
|
|
|
close.addEventListener('click', destroy);
|
|
// Importante: não fechamos o modal ao clicar fora (backdrop).
|
|
// Em mobile é comum tocar fora sem querer e perder o formulário.
|
|
window.addEventListener('message', onMsg);
|
|
|
|
document.body.appendChild(host);
|
|
return {shadow, panel, destroy};
|
|
}
|
|
|
|
async function postJSON(url, body){
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {'Content-Type':'application/json'},
|
|
body: JSON.stringify(body),
|
|
});
|
|
return res;
|
|
}
|
|
|
|
// API pública do widget.
|
|
// Nome novo do projeto: e-li.nps
|
|
window.ELiNPS = {
|
|
init: async function(opts){
|
|
const cfg = Object.assign({}, DEFAULTS, opts || {});
|
|
|
|
// Carrega WASM (Go). Sem WASM, não abrimos o widget (fail-closed).
|
|
const okWasm = await carregarWasm(cfg.apiBase);
|
|
if(!okWasm) return;
|
|
|
|
// Pré-validação e preparação do payload no WASM.
|
|
const pre = window.__eli_nps_wasm_preflight(cfg);
|
|
if(!pre || !pre.ok) return;
|
|
|
|
// Cooldown visual no browser (WASM faz storage best-effort).
|
|
// A chave do cooldown é best-effort e não participa de regra de segurança.
|
|
if(window.__eli_nps_wasm_cooldown_ativo(pre.chave_cooldown)) return;
|
|
|
|
let data;
|
|
try{
|
|
const res = await postJSON(`${cfg.apiBase}/api/e-li.nps/pedido`, pre.payload);
|
|
if(!res.ok) return; // fail-closed
|
|
data = await res.json();
|
|
}catch(e){
|
|
return; // fail-closed
|
|
}
|
|
|
|
const dec = window.__eli_nps_wasm_decidir(cfg, data);
|
|
if(!dec || !dec.abrir){
|
|
// cooldown para evitar flicker se o backend seguir rejeitando.
|
|
if(dec && dec.aplicar_cooldown){
|
|
window.__eli_nps_wasm_set_cooldown(pre.chave_cooldown, dec.cooldown_ate_ms);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const modal = createModal();
|
|
const iframe = document.createElement('iframe');
|
|
iframe.src = `${cfg.apiBase}/e-li.nps/${dec.produto_rota}/${dec.id}/form`;
|
|
modal.panel.appendChild(iframe);
|
|
|
|
// Visual cooldown so it doesn't keep popping (even if user closes).
|
|
window.__eli_nps_wasm_set_cooldown(pre.chave_cooldown, dec.cooldown_ate_ms);
|
|
}
|
|
};
|
|
})();
|