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("") + b.WriteString("
DataNotaUsuárioComentário
") 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("") + b.WriteString("") } b.WriteString("
DataNotaUsuárioInquilinoEmailTelefoneComentárioAções
" + template.HTMLEscapeString(data) + "" + template.HTMLEscapeString(nota) + "" + usuario + "" + coment + "
" + template.HTMLEscapeString(data) + "" + template.HTMLEscapeString(nota) + "" + usuario + "" + inquilino + "" + emailHTML + "" + telefoneHTML + "" + coment + "" + acoes + "
") 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