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("
Data Nota Usuário Inquilino Email Telefone Comentário Ações ")
+ b.WriteString("Data Nota Usuário Comentário ")
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("" + template.HTMLEscapeString(data) + " " + template.HTMLEscapeString(nota) + " " + usuario + " " + inquilino + " " + emailHTML + " " + telefoneHTML + " " + coment + " " + acoes + " ")
+ b.WriteString("" + template.HTMLEscapeString(data) + " " + template.HTMLEscapeString(nota) + " " + usuario + " " + coment + " ")
}
b.WriteString("
")
@@ -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("