diff --git a/internal/contratos/tipos.go b/internal/contratos/tipos.go
index 3eba408..89bd980 100644
--- a/internal/contratos/tipos.go
+++ b/internal/contratos/tipos.go
@@ -74,14 +74,17 @@ type NPSMensal struct {
// RespostaPainel representa uma resposta para listagem no painel.
type RespostaPainel struct {
- ID string
- RespondidoEm *time.Time
- PedidoCriadoEm time.Time
- UsuarioCodigo *string
- UsuarioNome string
- UsuarioEmail *string
- Nota *int
- Justificativa *string
+ ID string
+ RespondidoEm *time.Time
+ PedidoCriadoEm time.Time
+ InquilinoCodigo string
+ InquilinoNome string
+ UsuarioCodigo *string
+ UsuarioNome string
+ UsuarioEmail *string
+ UsuarioTelefone *string
+ Nota *int
+ Justificativa *string
}
type PainelDados struct {
diff --git a/internal/elinps/painel.go b/internal/elinps/painel.go
index cdbeff1..5f997d4 100644
--- a/internal/elinps/painel.go
+++ b/internal/elinps/painel.go
@@ -6,6 +6,7 @@ import (
"html/template"
"net/http"
"net/url"
+ "regexp"
"strings"
"time"
@@ -133,6 +134,62 @@ 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
@@ -148,6 +205,9 @@ 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("")
@@ -186,11 +246,14 @@ 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 | Comentário |
")
+ b.WriteString("| Data | Nota | Usuário | Inquilino | Email | Telefone | Comentário | Ações |
")
for _, r := range d.Respostas {
data := "-"
if r.RespondidoEm != nil {
- data = r.RespondidoEm.Format("2006-01-02 15:04")
+ data = formatarDataPainel(*r.RespondidoEm)
+ } else {
+ // fallback: apesar do painel listar "respondido", mantemos robustez.
+ data = formatarDataPainel(r.PedidoCriadoEm)
}
nota := "-"
if r.Nota != nil {
@@ -200,11 +263,43 @@ 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 + " | " + coment + " |
")
+ b.WriteString("| " + template.HTMLEscapeString(data) + " | " + template.HTMLEscapeString(nota) + " | " + usuario + " | " + inquilino + " | " + emailHTML + " | " + telefoneHTML + " | " + coment + " | " + acoes + " |
")
}
b.WriteString("
")
diff --git a/internal/elinps/painel_queries.go b/internal/elinps/painel_queries.go
index 8b03642..2eb3c39 100644
--- a/internal/elinps/painel_queries.go
+++ b/internal/elinps/painel_queries.go
@@ -123,9 +123,12 @@ SELECT
id,
respondido_em,
pedido_criado_em,
+ inquilino_codigo,
+ inquilino_nome,
usuario_codigo,
usuario_nome,
usuario_email,
+ usuario_telefone,
nota,
justificativa
FROM %s
@@ -146,9 +149,12 @@ 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 dd5ff33..6df7008 100755
Binary files a/server and b/server differ