primeira versão do e-li-nps construido com IA
This commit is contained in:
commit
06950d6e2c
34 changed files with 2524 additions and 0 deletions
203
web/static/e-li.nps.js
Normal file
203
web/static/e-li.nps.js
Normal file
|
|
@ -0,0 +1,203 @@
|
|||
(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);
|
||||
}
|
||||
};
|
||||
})();
|
||||
87
web/static/teste.html
Normal file
87
web/static/teste.html
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<!doctype html>
|
||||
<html lang="pt-br">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>e-li.nps • Teste</title>
|
||||
<style>
|
||||
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;padding:24px;}
|
||||
.card{max-width:760px;margin:0 auto;border:1px solid #e5e5e5;border-radius:12px;padding:16px;}
|
||||
.row{display:flex;gap:12px;flex-wrap:wrap;}
|
||||
label{display:block;font-size:12px;color:#444;margin-bottom:6px;}
|
||||
input{width:280px;max-width:100%;padding:10px;border:1px solid #ddd;border-radius:10px;}
|
||||
button{padding:10px 14px;border-radius:10px;border:1px solid #111;background:#111;color:#fff;cursor:pointer;}
|
||||
code{background:#f6f6f6;padding:2px 6px;border-radius:6px;}
|
||||
.muted{color:#555;font-size:13px;}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>e-li.nps • Página de teste</h1>
|
||||
<p class="muted">
|
||||
Esta página carrega <code>/static/e-li.nps.js</code> e dispara <code>window.ELiNPS.init()</code>.
|
||||
Se a API permitir, abrirá o modal (iframe) com o formulário HTMX.
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<div>
|
||||
<label>produto_nome</label>
|
||||
<input id="produto" value="e-licencie.ind" />
|
||||
</div>
|
||||
<div>
|
||||
<label>inquilino_codigo</label>
|
||||
<input id="inquilino_codigo" value="acme" />
|
||||
</div>
|
||||
<div>
|
||||
<label>inquilino_nome</label>
|
||||
<input id="inquilino_nome" value="ACME LTDA" />
|
||||
</div>
|
||||
<div>
|
||||
<label>usuario_codigo</label>
|
||||
<input id="usuario_codigo" value="u-123" />
|
||||
</div>
|
||||
<div>
|
||||
<label>usuario_nome</label>
|
||||
<input id="usuario_nome" value="Maria" />
|
||||
</div>
|
||||
<div>
|
||||
<label>usuario_telefone</label>
|
||||
<input id="usuario_telefone" value="+55 11 99999-9999" />
|
||||
</div>
|
||||
<div>
|
||||
<label>usuario_email (opcional)</label>
|
||||
<input id="usuario_email" value="maria@acme.com" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p style="margin-top:16px;">
|
||||
<button id="btn">Abrir NPS</button>
|
||||
</p>
|
||||
|
||||
<p class="muted">
|
||||
Dica: se você testar repetidamente, pode cair nas regras (45 dias / 10 dias).
|
||||
Para forçar reaparecer, use outro e-mail ou limpe a tabela do produto no Postgres.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<script src="/static/e-li.nps.js"></script>
|
||||
<script>
|
||||
function read(id){ return document.getElementById(id).value; }
|
||||
document.getElementById('btn').addEventListener('click', function(){
|
||||
window.ELiNPS.init({
|
||||
apiBase: window.location.origin,
|
||||
// Bloqueia abertura antes de uma data (YYYY-MM-DD).
|
||||
// Ex.: "2026-01-01".
|
||||
// data_minima_abertura: '2026-01-01',
|
||||
produto_nome: read('produto'),
|
||||
inquilino_codigo: read('inquilino_codigo'),
|
||||
inquilino_nome: read('inquilino_nome'),
|
||||
usuario_codigo: read('usuario_codigo'),
|
||||
usuario_nome: read('usuario_nome'),
|
||||
usuario_telefone: read('usuario_telefone'),
|
||||
usuario_email: read('usuario_email')
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
1
web/static/vendor/htmx.min.js
vendored
Normal file
1
web/static/vendor/htmx.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
12
web/static/vendor/json-enc.js
vendored
Normal file
12
web/static/vendor/json-enc.js
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
htmx.defineExtension('json-enc', {
|
||||
onEvent: function (name, evt) {
|
||||
if (name === "htmx:configRequest") {
|
||||
evt.detail.headers['Content-Type'] = "application/json";
|
||||
}
|
||||
},
|
||||
|
||||
encodeParameters : function(xhr, parameters, elt) {
|
||||
xhr.overrideMimeType('text/json');
|
||||
return (JSON.stringify(parameters));
|
||||
}
|
||||
});
|
||||
7
web/templates/edit_block.html
Normal file
7
web/templates/edit_block.html
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{{define "edit_block.html"}}
|
||||
<p class="eli-nps-sub">Nota atual: {{if .Reg.Nota}}{{.Reg.Nota}}{{else}}(sem nota){{end}}</p>
|
||||
{{template "nota_block.html" .}}
|
||||
{{if .Reg.Nota}}
|
||||
{{template "justificativa_block.html" .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
18
web/templates/form_inner.html
Normal file
18
web/templates/form_inner.html
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{{define "form_inner.html"}}
|
||||
{{/* This block can be swapped by HTMX (target #eli-nps-modal-body) */}}
|
||||
{{if eq .Reg.Status "respondido"}}
|
||||
<div class="eli-nps-ok">
|
||||
<p class="eli-nps-title">Obrigado!</p>
|
||||
<p class="eli-nps-sub">Sua resposta foi registrada. Você pode editar se quiser.</p>
|
||||
{{template "edit_block.html" .}}
|
||||
</div>
|
||||
{{else}}
|
||||
<p class="eli-nps-title">De 1 a 10, quanto você recomenda {{if .Reg.ProdutoNome}}{{.Reg.ProdutoNome}}{{else}}{{produtoLabel .Produto}}{{end}} para um amigo?</p>
|
||||
{{template "nota_block.html" .}}
|
||||
{{if .Reg.Nota}}
|
||||
{{template "justificativa_block.html" .}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<p class="eli-nps-foot">e-li.nps • produto: {{if .Reg.ProdutoNome}}{{.Reg.ProdutoNome}}{{else}}{{.Produto}}{{end}} • id: {{.ID}}</p>
|
||||
{{end}}
|
||||
117
web/templates/form_page.html
Normal file
117
web/templates/form_page.html
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
{{define "form_page.html"}}
|
||||
<!doctype html>
|
||||
<html lang="pt-br">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>e-li.nps</title>
|
||||
{{/*
|
||||
Importante: evitamos SRI aqui porque o hash pode mudar entre CDNs/builds.
|
||||
Servimos arquivos locais para garantir estabilidade do widget.
|
||||
*/}}
|
||||
<script src="/static/vendor/htmx.min.js"></script>
|
||||
<script src="/static/vendor/json-enc.js"></script>
|
||||
<style>
|
||||
.eli-nps-wrap{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;color:#111;margin:0;padding:0;}
|
||||
.eli-nps-card{max-width:520px;margin:0 auto;padding:16px;}
|
||||
.eli-nps-title{font-size:18px;font-weight:700;margin:0 0 8px;}
|
||||
.eli-nps-sub{margin:0 0 12px;color:#444;font-size:14px;}
|
||||
.eli-nps-scale{display:grid;grid-template-columns:repeat(10,1fr);gap:8px;margin:12px 0;}
|
||||
/*
|
||||
Botões de nota (1–10) com escala vermelho → verde.
|
||||
Importante: a cor base é determinada pelo valor; quando selecionado,
|
||||
o botão ganha destaque (borda/sombra) mas mantém a cor.
|
||||
*/
|
||||
.eli-nps-btn{
|
||||
border:1px solid #ddd;
|
||||
background:#fff;
|
||||
border-radius:8px;
|
||||
padding:10px 0;
|
||||
cursor:pointer;
|
||||
font-weight:700;
|
||||
color:#111;
|
||||
transition:
|
||||
transform 80ms ease,
|
||||
box-shadow 120ms ease,
|
||||
border-color 120ms ease,
|
||||
background-color 120ms ease,
|
||||
color 120ms ease;
|
||||
}
|
||||
|
||||
/*
|
||||
Base mais “clara” (pastel) e destaque mais “forte” no hover/seleção.
|
||||
Isso evita começar chamativo demais, mas deixa bem evidente ao interagir.
|
||||
*/
|
||||
.eli-nps-btn:hover,
|
||||
.eli-nps-btn-selected{transform:translateY(-1px);box-shadow:0 6px 14px rgba(0,0,0,.14);border-color:#111;}
|
||||
.eli-nps-btn:active{transform:translateY(0) scale(0.98);}
|
||||
|
||||
/*
|
||||
Paleta por grupo:
|
||||
- 1 a 6: tons de vermelho
|
||||
- 7 e 8: tons de amarelo
|
||||
- 9 e 10: tons de verde
|
||||
*/
|
||||
|
||||
/* 1–6 (vermelho) */
|
||||
.eli-nps-btn-1{background:#ffebee;border-color:#ffcdd2;}
|
||||
.eli-nps-btn-1:hover,.eli-nps-btn-1.eli-nps-btn-selected{background:#ef5350;color:#fff;border-color:#ef5350;}
|
||||
|
||||
.eli-nps-btn-2{background:#ffebee;border-color:#ffcdd2;}
|
||||
.eli-nps-btn-2:hover,.eli-nps-btn-2.eli-nps-btn-selected{background:#e53935;color:#fff;border-color:#e53935;}
|
||||
|
||||
.eli-nps-btn-3{background:#ffebee;border-color:#ffcdd2;}
|
||||
.eli-nps-btn-3:hover,.eli-nps-btn-3.eli-nps-btn-selected{background:#d32f2f;color:#fff;border-color:#d32f2f;}
|
||||
|
||||
.eli-nps-btn-4{background:#ffebee;border-color:#ffcdd2;}
|
||||
.eli-nps-btn-4:hover,.eli-nps-btn-4.eli-nps-btn-selected{background:#c62828;color:#fff;border-color:#c62828;}
|
||||
|
||||
.eli-nps-btn-5{background:#ffebee;border-color:#ffcdd2;}
|
||||
.eli-nps-btn-5:hover,.eli-nps-btn-5.eli-nps-btn-selected{background:#b71c1c;color:#fff;border-color:#b71c1c;}
|
||||
|
||||
.eli-nps-btn-6{background:#ffebee;border-color:#ffcdd2;}
|
||||
.eli-nps-btn-6:hover,.eli-nps-btn-6.eli-nps-btn-selected{background:#8e0000;color:#fff;border-color:#8e0000;}
|
||||
|
||||
/* 7–8 (amarelo) */
|
||||
.eli-nps-btn-7{background:#fffde7;border-color:#fff9c4;}
|
||||
.eli-nps-btn-7:hover,.eli-nps-btn-7.eli-nps-btn-selected{background:#fdd835;color:#111;border-color:#fdd835;}
|
||||
|
||||
.eli-nps-btn-8{background:#fff8e1;border-color:#ffecb3;}
|
||||
.eli-nps-btn-8:hover,.eli-nps-btn-8.eli-nps-btn-selected{background:#ffb300;color:#111;border-color:#ffb300;}
|
||||
|
||||
/* 9–10 (verde) */
|
||||
.eli-nps-btn-9{background:#e8f5e9;border-color:#c8e6c9;}
|
||||
.eli-nps-btn-9:hover,.eli-nps-btn-9.eli-nps-btn-selected{background:#2e7d32;color:#fff;border-color:#2e7d32;}
|
||||
|
||||
.eli-nps-btn-10{background:#e8f5e9;border-color:#c8e6c9;}
|
||||
.eli-nps-btn-10:hover,.eli-nps-btn-10.eli-nps-btn-selected{background:#1b5e20;color:#fff;border-color:#1b5e20;}
|
||||
.eli-nps-textarea{width:100%;min-height:100px;border:1px solid #ddd;border-radius:10px;padding:10px;font-size:14px;}
|
||||
.eli-nps-actions{display:flex;gap:10px;margin-top:12px;}
|
||||
.eli-nps-primary{background:#111;color:#fff;border:1px solid #111;border-radius:10px;padding:10px 14px;cursor:pointer;}
|
||||
.eli-nps-secondary{background:#fff;color:#111;border:1px solid #ddd;border-radius:10px;padding:10px 14px;cursor:pointer;}
|
||||
.eli-nps-foot{margin-top:10px;color:#666;font-size:12px;}
|
||||
.eli-nps-ok{padding:16px;border:1px solid #e5e5e5;border-radius:12px;background:#fafafa;}
|
||||
|
||||
/*
|
||||
Ajustes para dispositivos móveis.
|
||||
Objetivo: manter leitura confortável e botões clicáveis sem ficar apertado.
|
||||
*/
|
||||
@media (max-width: 480px){
|
||||
.eli-nps-card{max-width:none;padding:12px;}
|
||||
.eli-nps-title{font-size:16px;line-height:1.25;}
|
||||
.eli-nps-sub{font-size:13px;}
|
||||
.eli-nps-scale{grid-template-columns:repeat(5,1fr);gap:10px;}
|
||||
.eli-nps-btn{padding:12px 0;border-radius:10px;}
|
||||
.eli-nps-actions{flex-direction:column;}
|
||||
.eli-nps-primary,.eli-nps-secondary{width:100%;}
|
||||
.eli-nps-textarea{min-height:120px;}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="eli-nps-wrap">
|
||||
<div class="eli-nps-card" id="eli-nps-modal-body">
|
||||
{{template "form_inner.html" .}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
2
web/templates/funcs.html
Normal file
2
web/templates/funcs.html
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{{define "funcs"}}{{end}}
|
||||
|
||||
59
web/templates/justificativa_block.html
Normal file
59
web/templates/justificativa_block.html
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
{{define "justificativa_block.html"}}
|
||||
<p class="eli-nps-sub" style="margin-top:16px;">Quer nos contar o motivo?</p>
|
||||
|
||||
<form
|
||||
hx-patch="/api/e-li.nps/{{.Produto}}/{{.ID}}"
|
||||
hx-target="#eli-nps-modal-body"
|
||||
hx-swap="innerHTML"
|
||||
hx-ext="json-enc"
|
||||
>
|
||||
<textarea
|
||||
class="eli-nps-textarea"
|
||||
name="justificativa"
|
||||
id="eli-nps-justificativa"
|
||||
>{{if .Reg.Justificativa}}{{.Reg.Justificativa}}{{end}}</textarea>
|
||||
|
||||
<div class="eli-nps-actions">
|
||||
<button class="eli-nps-primary" type="button" onclick="window.__eliNpsSubmitJust('{{.Produto}}','{{.ID}}')">Enviar</button>
|
||||
<button class="eli-nps-secondary" type="button" onclick="window.__eliNpsFinalizar('{{.Produto}}','{{.ID}}')">Finalizar</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Funções auxiliares usadas pelo iframe.
|
||||
// Elas notificarão o widget pai (janela que contém o iframe) para fechar o modal.
|
||||
window.__eliNpsSubmitJust = async function(produto, id){
|
||||
const v = document.getElementById('eli-nps-justificativa')?.value || '';
|
||||
|
||||
// Regra do produto: ao clicar em "Enviar" após escolher a nota,
|
||||
// consideramos a resposta como finalizada e fechamos o modal.
|
||||
const res = await fetch(`/api/e-li.nps/${produto}/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {'Content-Type':'application/json','HX-Request':'true'},
|
||||
body: JSON.stringify({justificativa: v, finalizar:true})
|
||||
});
|
||||
if(!res.ok){ return; }
|
||||
|
||||
// Close parent widget modal (iframe).
|
||||
try{ parent.postMessage({type:'eli-nps:done', id:id, produto:produto}, '*'); }catch(e){}
|
||||
}
|
||||
|
||||
window.__eliNpsFinalizar = async function(produto, id){
|
||||
const v = document.getElementById('eli-nps-justificativa')?.value || '';
|
||||
|
||||
const res = await fetch(`/api/e-li.nps/${produto}/${id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {'Content-Type':'application/json','HX-Request':'true'},
|
||||
body: JSON.stringify({justificativa: v, finalizar:true})
|
||||
});
|
||||
if(!res.ok){ return; }
|
||||
|
||||
const html = await res.text();
|
||||
document.getElementById('eli-nps-modal-body').innerHTML = html;
|
||||
|
||||
// notify parent widget to close
|
||||
try{ parent.postMessage({type:'eli-nps:done', id:id, produto:produto}, '*'); }catch(e){}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
14
web/templates/nota_block.html
Normal file
14
web/templates/nota_block.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{{define "nota_block.html"}}
|
||||
<div class="eli-nps-scale">
|
||||
{{range $i := seq 1 10}}
|
||||
<button
|
||||
class="eli-nps-btn eli-nps-btn-{{$i}} {{if noteEq $.Reg.Nota $i}}eli-nps-btn-selected{{end}}"
|
||||
hx-patch="/api/e-li.nps/{{$.Produto}}/{{$.ID}}"
|
||||
hx-target="#eli-nps-modal-body"
|
||||
hx-swap="innerHTML"
|
||||
hx-ext="json-enc"
|
||||
hx-vals='{"nota":{{$i}}}'
|
||||
>{{$i}}</button>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
Loading…
Add table
Add a link
Reference in a new issue