(function(){ 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 parseDataMinima(s){ // Aceita somente ISO (data) YYYY-MM-DD. // Retorna um Date no início do dia (00:00) no horário local. const v = String(s || '').trim(); if(!v) return null; const m = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.exec(v); if(!m) return null; const ano = Number(m[1]); const mes = Number(m[2]); const dia = Number(m[3]); if(!ano || mes < 1 || mes > 12 || dia < 1 || dia > 31) return null; return new Date(ano, mes-1, dia, 0, 0, 0, 0); } function antesDaDataMinima(cfg){ const d = parseDataMinima(cfg.data_minima_abertura); if(!d) return false; return new Date() < d; } function normalizeEmail(email){ return String(email || '').trim().toLowerCase(); } function cooldownKey(produto, inquilino, usuarioCodigo){ // Prefixo de storage atualizado para o novo nome do projeto. return `eli-nps:cooldown:${produto}:${inquilino}:${usuarioCodigo}`; } function nowMs(){ return Date.now(); } function withinCooldown(key){ try{ const v = localStorage.getItem(key); if(!v) return false; const obj = JSON.parse(v); return obj && obj.until && nowMs() < obj.until; }catch(e){ return false; } } function setCooldown(key, hours){ try{ const until = nowMs() + hours*3600*1000; localStorage.setItem(key, JSON.stringify({until})); }catch(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 || {}); // Bloqueio por data mínima (feature flag simples). // Ex.: não abrir modal antes de 2026-01-01. if(antesDaDataMinima(cfg)){ return; } // produto_nome pode ser qualquer string (ex.: "e-licencie", "Cachaça & Churras"). // Regra do projeto: o tratamento/normalização de caracteres deve ser feito // apenas no backend, exclusivamente para nome de tabela/rotas. const produtoNome = String(cfg.produto_nome || '').trim(); const inquilino = String(cfg.inquilino_codigo || '').trim(); const usuarioCodigo = String(cfg.usuario_codigo || '').trim(); const email = normalizeEmail(cfg.usuario_email); // controle de exibição: produto + inquilino_codigo + usuario_codigo if(!produtoNome || !inquilino || !usuarioCodigo){ return; // missing required context } // A chave do cooldown é “best-effort” e não participa de nenhuma regra // de segurança. Mantemos o produto como foi informado. const chaveCooldown = cooldownKey(produtoNome, inquilino, usuarioCodigo); if(withinCooldown(chaveCooldown)) return; // Enviamos exatamente o produto_nome informado. const payload = { produto_nome: produtoNome, inquilino_codigo: inquilino, inquilino_nome: String(cfg.inquilino_nome || '').trim(), usuario_codigo: usuarioCodigo, usuario_nome: String(cfg.usuario_nome || '').trim(), usuario_telefone: String(cfg.usuario_telefone || '').trim(), usuario_email: email, }; let data; try{ const res = await postJSON(`${cfg.apiBase}/api/e-li.nps/pedido`, payload); if(!res.ok) return; // fail-closed data = await res.json(); }catch(e){ return; // fail-closed } if(!data || !data.pode_abrir || !data.id){ // small cooldown to avoid flicker if backend keeps rejecting setCooldown(chaveCooldown, cfg.cooldownHours); return; } // Backend can return normalized product; use it for building iframe URL. const produtoRota = data.produto; if(!produtoRota){ // fail-closed (não dá pra montar URL segura) setCooldown(chaveCooldown, cfg.cooldownHours); return; } const modal = createModal(); const iframe = document.createElement('iframe'); iframe.src = `${cfg.apiBase}/e-li.nps/${produtoRota}/${data.id}/form`; modal.panel.appendChild(iframe); // Visual cooldown so it doesn't keep popping (even if user closes). setCooldown(chaveCooldown, cfg.cooldownHours); } }; })();