203 lines
7 KiB
JavaScript
203 lines
7 KiB
JavaScript
(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);
|
|
}
|
|
};
|
|
})();
|