diff --git a/cmd/widgetwasm/main.go b/cmd/widgetwasm/main.go index 222aa6f..931c4d1 100644 --- a/cmd/widgetwasm/main.go +++ b/cmd/widgetwasm/main.go @@ -3,7 +3,6 @@ package main import ( - "net/url" "regexp" "strings" "syscall/js" @@ -24,128 +23,12 @@ func main() { js.Global().Set("__eli_nps_wasm_decidir", js.FuncOf(decidir)) js.Global().Set("__eli_nps_wasm_cooldown_ativo", js.FuncOf(cooldownAtivo)) js.Global().Set("__eli_nps_wasm_set_cooldown", js.FuncOf(setCooldown)) - js.Global().Set("__eli_nps_wasm_painel_init", js.FuncOf(painelInit)) js.Global().Set("__eli_nps_wasm_ready", true) // Mantém o módulo vivo. select {} } -func painelInit(this js.Value, args []js.Value) any { - // Painel: persistência de filtros no localStorage. - // Regras (.agent): lógica no WASM (Go). Aqui é best-effort e fail-open. - // - // - Salva: produto selecionado + checkbox "baixas". - // - Restaura: se a URL não tiver parâmetros e houver valor salvo, - // redireciona para /painel?produto=...&baixas=1. - // - // Importante: não roda fora do /painel. - loc := js.Global().Get("location") - if loc.IsUndefined() || loc.IsNull() { - return nil - } - path := loc.Get("pathname").String() - if path == "" || !strings.HasPrefix(path, "/painel") { - return nil - } - - storage := js.Global().Get("localStorage") - if storage.IsUndefined() || storage.IsNull() { - return nil - } - - const keyProd = "eli_nps_painel_produto" - const keyBaixas = "eli_nps_painel_baixas" - - // Aguarda o DOM estar pronto para conseguirmos acessar o form/option list. - doc := js.Global().Get("document") - if doc.IsUndefined() || doc.IsNull() { - return nil - } - - handler := js.FuncOf(func(this js.Value, args []js.Value) any { - // Best-effort: evita exceptions se APIs não existirem. - defer func() { _ = recover() }() - - form := doc.Call("querySelector", `form[action="/painel"]`) - if form.IsUndefined() || form.IsNull() { - return nil - } - - sel := form.Call("querySelector", `select[name="produto"]`) - chk := form.Call("querySelector", `input[name="baixas"]`) - - // Helper: verifica se option existe. - optionExists := func(selectEl js.Value, value string) bool { - if selectEl.IsUndefined() || selectEl.IsNull() { - return false - } - opts := selectEl.Get("options") - ln := opts.Get("length").Int() - for i := 0; i < ln; i++ { - opt := opts.Index(i) - if opt.Get("value").String() == value { - return true - } - } - return false - } - - persist := func() { - if !sel.IsUndefined() && !sel.IsNull() { - v := strings.TrimSpace(sel.Get("value").String()) - if v != "" { - storage.Call("setItem", keyProd, v) - } - } - baixas := "0" - if !chk.IsUndefined() && !chk.IsNull() && chk.Get("checked").Truthy() { - baixas = "1" - } - storage.Call("setItem", keyBaixas, baixas) - } - - // Restaura/redirect apenas se não há query string. - s := loc.Get("search").String() - if strings.TrimSpace(s) == "" { - storedProd := strings.TrimSpace(storage.Call("getItem", keyProd).String()) - storedBaixas := strings.TrimSpace(storage.Call("getItem", keyBaixas).String()) - if storedProd != "" && optionExists(sel, storedProd) { - q := "?produto=" + url.QueryEscape(storedProd) - if storedBaixas == "1" { - q += "&baixas=1" - } - loc.Call("replace", path+q) - return nil - } - // Se só baixas estiver setado, aplica também. - if storedBaixas == "1" { - loc.Call("replace", path+"?baixas=1") - return nil - } - } - - // Liga listeners para persistência. - onSubmit := js.FuncOf(func(this js.Value, args []js.Value) any { persist(); return nil }) - onChange := js.FuncOf(func(this js.Value, args []js.Value) any { persist(); return nil }) - form.Call("addEventListener", "submit", onSubmit) - form.Call("addEventListener", "change", onChange) - - return nil - }) - - // DOMContentLoaded - readyState := doc.Get("readyState").String() - if readyState != "loading" { - // Se o WASM foi carregado depois do DOM pronto (comum no painel), - // rodamos imediatamente. - handler.Invoke() - } else { - doc.Call("addEventListener", "DOMContentLoaded", handler) - } - return nil -} - func cooldownAtivo(this js.Value, args []js.Value) any { if len(args) < 1 { return false diff --git a/internal/contratos/tipos.go b/internal/contratos/tipos.go index 89bd980..3eba408 100644 --- a/internal/contratos/tipos.go +++ b/internal/contratos/tipos.go @@ -74,17 +74,14 @@ type NPSMensal struct { // RespostaPainel representa uma resposta para listagem no painel. type RespostaPainel struct { - ID string - RespondidoEm *time.Time - PedidoCriadoEm time.Time - InquilinoCodigo string - InquilinoNome string - UsuarioCodigo *string - UsuarioNome string - UsuarioEmail *string - UsuarioTelefone *string - Nota *int - Justificativa *string + ID string + RespondidoEm *time.Time + PedidoCriadoEm time.Time + UsuarioCodigo *string + UsuarioNome string + UsuarioEmail *string + Nota *int + Justificativa *string } type PainelDados struct { diff --git a/internal/elinps/painel.go b/internal/elinps/painel.go index 690617b..cdbeff1 100644 --- a/internal/elinps/painel.go +++ b/internal/elinps/painel.go @@ -6,7 +6,6 @@ import ( "html/template" "net/http" "net/url" - "regexp" "strings" "time" @@ -134,62 +133,6 @@ func (a AuthPainel) handlerPainel(w http.ResponseWriter, r *http.Request, store func (a AuthPainel) renderPainelHTML(w http.ResponseWriter, d PainelDados) { w.Header().Set("Content-Type", "text/html; charset=utf-8") - formatarDataPainel := func(t time.Time) string { - // O banco armazena timestamptz (UTC normalizado). O requisito do painel é - // exibir no fuso de Brasília. - loc, err := time.LoadLocation("America/Sao_Paulo") - if err != nil { - // Fallback best-effort (sem regras de DST antigas, mas suficiente para - // ambientes sem tzdata). - loc = time.FixedZone("America/Sao_Paulo", -3*60*60) - } - return t.In(loc).Format("02/01/2006 15:04") - } - - soDigitos := func(s string) string { - // Normalização best-effort para criar links tel/WhatsApp. - // Mantemos apenas dígitos e descartamos qualquer outro caractere. - re := regexp.MustCompile(`\D`) - return re.ReplaceAllString(s, "") - } - - linkEmail := func(email string) string { - email = strings.TrimSpace(email) - if email == "" { - return "-" - } - // mailto aceita percent-encoding; usar PathEscape evita injeção no href. - href := "mailto:" + url.PathEscape(email) - txt := template.HTMLEscapeString(email) - return "" + txt + "" - } - - linkTelefone := func(telOriginal string) (hrefTel string, hrefWA string, htmlTel string) { - telOriginal = strings.TrimSpace(telOriginal) - if telOriginal == "" { - return "", "", "-" - } - dig := soDigitos(telOriginal) - if dig == "" { - // Se não há dígitos, exibimos apenas como texto. - return "", "", template.HTMLEscapeString(telOriginal) - } - - // tel: — preferimos com '+' quando possível. - hrefTel = "tel:+" + dig - // WhatsApp wa.me exige número em formato internacional com dígitos. - waNum := dig - // Heurística: se parece número BR (10/11 dígitos) e não tem DDI, prefixa 55. - if (len(waNum) == 10 || len(waNum) == 11) && !strings.HasPrefix(waNum, "55") { - waNum = "55" + waNum - } - hrefWA = "https://wa.me/" + waNum - - // Exibição do número: mantém o original (mais amigável), mas escapado. - htmlTel = "" + template.HTMLEscapeString(telOriginal) + "" - return hrefTel, hrefWA, htmlTel - } - // HTML propositalmente simples (sem template engine) para manter isolado. // Se quiser evoluir, dá pra migrar para templates. var b strings.Builder @@ -205,9 +148,6 @@ table{width:100%;border-collapse:collapse;font-size:13px;} th,td{padding:8px;border-bottom:1px solid #eee;text-align:left;vertical-align:top;} .muted{color:#666;font-size:12px;} .badge{display:inline-block;padding:2px 8px;border-radius:999px;background:#f2f2f2;border:1px solid #e5e5e5;font-size:12px;} -.iconbtn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:10px;border:1px solid #e5e5e5;background:#fff;margin-right:6px;text-decoration:none;} -.iconbtn:hover{border-color:#bbb;background:#fafafa;} -.icon{width:16px;height:16px;display:block;} `) b.WriteString("
") @@ -246,14 +186,11 @@ th,td{padding:8px;border-bottom:1px solid #eee;text-align:left;vertical-align:to // Respostas b.WriteString("

Respostas

") - b.WriteString("") + b.WriteString("
DataNotaUsuárioInquilinoEmailTelefoneComentárioAções
") for _, r := range d.Respostas { data := "-" if r.RespondidoEm != nil { - data = formatarDataPainel(*r.RespondidoEm) - } else { - // fallback: apesar do painel listar "respondido", mantemos robustez. - data = formatarDataPainel(r.PedidoCriadoEm) + data = r.RespondidoEm.Format("2006-01-02 15:04") } nota := "-" if r.Nota != nil { @@ -263,43 +200,11 @@ th,td{padding:8px;border-bottom:1px solid #eee;text-align:left;vertical-align:to if r.UsuarioCodigo != nil { usuario += " (" + template.HTMLEscapeString(*r.UsuarioCodigo) + ")" } - inquilino := template.HTMLEscapeString(r.InquilinoNome) + " (" + template.HTMLEscapeString(r.InquilinoCodigo) + ")" - emailHTML := "-" - emailHref := "" - if r.UsuarioEmail != nil && strings.TrimSpace(*r.UsuarioEmail) != "" { - emailHTML = linkEmail(*r.UsuarioEmail) - emailHref = "mailto:" + url.PathEscape(strings.TrimSpace(*r.UsuarioEmail)) - } - telHref := "" - waHref := "" - telefoneHTML := "-" - if r.UsuarioTelefone != nil && strings.TrimSpace(*r.UsuarioTelefone) != "" { - telHref, waHref, telefoneHTML = linkTelefone(*r.UsuarioTelefone) - } - - // Ícones (inline SVG) — simples e sem dependências. - iconMail := `` - iconPhone := `` - iconWA := `` - - acoes := "" - if emailHref != "" { - acoes += "" + iconMail + "" - } - if waHref != "" { - acoes += "" + iconWA + "" - } - if telHref != "" { - acoes += "" + iconPhone + "" - } - if acoes == "" { - acoes = "-" - } coment := "" if r.Justificativa != nil { coment = template.HTMLEscapeString(*r.Justificativa) } - b.WriteString("") + b.WriteString("") } b.WriteString("
DataNotaUsuárioComentário
" + template.HTMLEscapeString(data) + "" + template.HTMLEscapeString(nota) + "" + usuario + "" + inquilino + "" + emailHTML + "" + telefoneHTML + "" + coment + "" + acoes + "
" + template.HTMLEscapeString(data) + "" + template.HTMLEscapeString(nota) + "" + usuario + "" + coment + "
") @@ -321,10 +226,6 @@ th,td{padding:8px;border-bottom:1px solid #eee;text-align:left;vertical-align:to b.WriteString("
") - // JS do painel: apenas bootstrap para executar a lógica no WASM. - // Regras (.agent): sem dependências externas. A lógica fica no WASM. - b.WriteString(``) - b.WriteString("") w.Write([]byte(b.String())) } diff --git a/internal/elinps/painel_queries.go b/internal/elinps/painel_queries.go index 2eb3c39..8b03642 100644 --- a/internal/elinps/painel_queries.go +++ b/internal/elinps/painel_queries.go @@ -123,12 +123,9 @@ SELECT id, respondido_em, pedido_criado_em, - inquilino_codigo, - inquilino_nome, usuario_codigo, usuario_nome, usuario_email, - usuario_telefone, nota, justificativa FROM %s @@ -149,12 +146,9 @@ LIMIT $1 OFFSET $2`, tabela, cond) &r.ID, &r.RespondidoEm, &r.PedidoCriadoEm, - &r.InquilinoCodigo, - &r.InquilinoNome, &r.UsuarioCodigo, &r.UsuarioNome, &r.UsuarioEmail, - &r.UsuarioTelefone, &r.Nota, &r.Justificativa, ); err != nil { diff --git a/server b/server index 6df7008..dd5ff33 100755 Binary files a/server and b/server differ diff --git a/web/static/e-li.nps.wasm b/web/static/e-li.nps.wasm index b544181..d533202 100755 Binary files a/web/static/e-li.nps.wasm and b/web/static/e-li.nps.wasm differ diff --git a/web/static/painel.js b/web/static/painel.js deleted file mode 100644 index 4726e5a..0000000 --- a/web/static/painel.js +++ /dev/null @@ -1,49 +0,0 @@ -(function(){ - // Bootstrap mínimo do painel. - // Regra (.agent): a lógica fica no WASM; aqui apenas garantimos que o WASM - // esteja carregado/executando. - // - // Este arquivo existe para evitar JS inline no HTML do painel. - - async function carregarWasmPainel(){ - try{ - 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{ - if(!window.Go){ - // wasm_exec.js deve ter sido carregado pelo HTML. - return false; - } - const go = new Go(); - const res = await fetch('/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; - }catch(e){ - return false; - } - } - - document.addEventListener('DOMContentLoaded', function(){ - carregarWasmPainel().then(function(ok){ - if(!ok) return; - // Executa o init do painel no WASM (best-effort). - try{ - if(typeof window.__eli_nps_wasm_painel_init === 'function'){ - window.__eli_nps_wasm_painel_init(); - } - }catch(e){} - }); - }); -})(); -