refatoração de tipagem go
This commit is contained in:
parent
6f78511946
commit
0c41ed4279
12 changed files with 175 additions and 117 deletions
|
|
@ -131,6 +131,12 @@ func main() {
|
||||||
http.ServeFile(w, r, "web/static/teste.html")
|
http.ServeFile(w, r, "web/static/teste.html")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Conveniência: favicon.
|
||||||
|
// O arquivo fica em web/static/favicon.ico.
|
||||||
|
r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.ServeFile(w, r, "web/static/favicon.ico")
|
||||||
|
})
|
||||||
|
|
||||||
// Rotas NPS.
|
// Rotas NPS.
|
||||||
h := elinps.NewHandlers(pool)
|
h := elinps.NewHandlers(pool)
|
||||||
r.Route("/api/e-li.nps", func(r chi.Router) {
|
r.Route("/api/e-li.nps", func(r chi.Router) {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall/js"
|
"syscall/js"
|
||||||
|
|
||||||
|
"e-li.nps/internal/contratos"
|
||||||
)
|
)
|
||||||
|
|
||||||
// IMPORTANTE (.agent): o backend Go continua sendo a autoridade das regras.
|
// IMPORTANTE (.agent): o backend Go continua sendo a autoridade das regras.
|
||||||
|
|
@ -14,17 +16,7 @@ import (
|
||||||
|
|
||||||
var dataISORe = regexp.MustCompile(`^([0-9]{4})-([0-9]{2})-([0-9]{2})$`)
|
var dataISORe = regexp.MustCompile(`^([0-9]{4})-([0-9]{2})-([0-9]{2})$`)
|
||||||
|
|
||||||
type cfgWidget struct {
|
type cfgWidget = contratos.ConfigWidget
|
||||||
ProdutoNome string
|
|
||||||
InquilinoCodigo string
|
|
||||||
InquilinoNome string
|
|
||||||
UsuarioCodigo string
|
|
||||||
UsuarioNome string
|
|
||||||
UsuarioTelefone string
|
|
||||||
UsuarioEmail string
|
|
||||||
CooldownHours float64
|
|
||||||
DataMinimaAbertura string
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
js.Global().Set("__eli_nps_wasm_preflight", js.FuncOf(preflight))
|
js.Global().Set("__eli_nps_wasm_preflight", js.FuncOf(preflight))
|
||||||
|
|
|
||||||
128
internal/contratos/tipos.go
Normal file
128
internal/contratos/tipos.go
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
package contratos
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// Tipos centralizados do projeto.
|
||||||
|
//
|
||||||
|
// Regra: este arquivo concentra as tipagens (structs) usadas como contratos de
|
||||||
|
// dados entre camadas (backend, painel e widget/WASM), para manter consistência
|
||||||
|
// e facilitar auditoria.
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
|
// API do widget
|
||||||
|
// ------------------------------
|
||||||
|
|
||||||
|
type PedidoInput struct {
|
||||||
|
ProdutoNome string `json:"produto_nome"`
|
||||||
|
InquilinoCodigo string `json:"inquilino_codigo"`
|
||||||
|
InquilinoNome string `json:"inquilino_nome"`
|
||||||
|
UsuarioCodigo string `json:"usuario_codigo"`
|
||||||
|
UsuarioNome string `json:"usuario_nome"`
|
||||||
|
UsuarioTelefone string `json:"usuario_telefone"`
|
||||||
|
UsuarioEmail string `json:"usuario_email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PedidoResponse struct {
|
||||||
|
PodeAbrir bool `json:"pode_abrir"`
|
||||||
|
Motivo string `json:"motivo,omitempty"`
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
// Produto normalizado retornado pelo backend para montar URL segura.
|
||||||
|
Produto string `json:"produto,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PatchInput struct {
|
||||||
|
Nota *int `json:"nota,omitempty"`
|
||||||
|
Justificativa *string `json:"justificativa,omitempty"`
|
||||||
|
Finalizar bool `json:"finalizar,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Registro struct {
|
||||||
|
// ProdutoNome é o nome original do produto como enviado pela integração/widget.
|
||||||
|
// Ele existe apenas para exibição ao usuário.
|
||||||
|
//
|
||||||
|
// Importante: a normalização (remoção de acentos/símbolos) é usada apenas
|
||||||
|
// para formar o nome da tabela no Postgres e o parâmetro {produto} da rota.
|
||||||
|
ProdutoNome string
|
||||||
|
ID string
|
||||||
|
Status string
|
||||||
|
Nota *int
|
||||||
|
Justificativa *string
|
||||||
|
PedidoCriadoEm time.Time
|
||||||
|
RespondidoEm *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormPageData é o payload para renderização do formulário no iframe.
|
||||||
|
type FormPageData struct {
|
||||||
|
Produto string
|
||||||
|
ID string
|
||||||
|
Reg Registro
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
|
// Painel
|
||||||
|
// ------------------------------
|
||||||
|
|
||||||
|
// NPSMensal representa o cálculo do NPS agregado por mês.
|
||||||
|
type NPSMensal struct {
|
||||||
|
Mes string
|
||||||
|
Detratores int
|
||||||
|
Neutros int
|
||||||
|
Promotores int
|
||||||
|
Total int
|
||||||
|
NPS int
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
type PainelDados struct {
|
||||||
|
Produto string
|
||||||
|
Produtos []string
|
||||||
|
Meses []NPSMensal
|
||||||
|
Respostas []RespostaPainel
|
||||||
|
Pagina int
|
||||||
|
SomenteBaixas bool
|
||||||
|
MsgErro string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListarRespostasFiltro struct {
|
||||||
|
SomenteNotasBaixas bool
|
||||||
|
Pagina int
|
||||||
|
PorPagina int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *ListarRespostasFiltro) Normalizar() {
|
||||||
|
if f.Pagina <= 0 {
|
||||||
|
f.Pagina = 1
|
||||||
|
}
|
||||||
|
if f.PorPagina <= 0 || f.PorPagina > 200 {
|
||||||
|
f.PorPagina = 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------------------
|
||||||
|
// Widget/WASM
|
||||||
|
// ------------------------------
|
||||||
|
|
||||||
|
// ConfigWidget representa as opções passadas para window.ELiNPS.init(...).
|
||||||
|
// No WASM lemos via syscall/js; aqui fica apenas a tipagem centralizada.
|
||||||
|
type ConfigWidget struct {
|
||||||
|
ProdutoNome string
|
||||||
|
InquilinoCodigo string
|
||||||
|
InquilinoNome string
|
||||||
|
UsuarioCodigo string
|
||||||
|
UsuarioNome string
|
||||||
|
UsuarioTelefone string
|
||||||
|
UsuarioEmail string
|
||||||
|
CooldownHours float64
|
||||||
|
DataMinimaAbertura string
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"e-li.nps/internal/contratos"
|
||||||
"e-li.nps/internal/db"
|
"e-li.nps/internal/db"
|
||||||
|
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
@ -29,7 +30,7 @@ func NewHandlers(pool *pgxpool.Pool) *Handlers {
|
||||||
func (h *Handlers) PostPedido(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) PostPedido(w http.ResponseWriter, r *http.Request) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
var in PedidoInput
|
var in contratos.PedidoInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "json_invalido"})
|
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "json_invalido"})
|
||||||
return
|
return
|
||||||
|
|
@ -55,29 +56,29 @@ func (h *Handlers) PostPedido(w http.ResponseWriter, r *http.Request) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("erro ao checar resposta recente", "err", err)
|
slog.Error("erro ao checar resposta recente", "err", err)
|
||||||
// Fail-closed.
|
// Fail-closed.
|
||||||
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
writeJSON(w, http.StatusOK, contratos.PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if respRecente {
|
if respRecente {
|
||||||
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "resposta_recente"})
|
writeJSON(w, http.StatusOK, contratos.PedidoResponse{PodeAbrir: false, Motivo: "resposta_recente"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
pedidoAberto, err := h.store.HasPedidoEmAbertoRecente(ctx, table, in.InquilinoCodigo, in.UsuarioCodigo)
|
pedidoAberto, err := h.store.HasPedidoEmAbertoRecente(ctx, table, in.InquilinoCodigo, in.UsuarioCodigo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("erro ao checar pedido em aberto", "err", err)
|
slog.Error("erro ao checar pedido em aberto", "err", err)
|
||||||
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
writeJSON(w, http.StatusOK, contratos.PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if pedidoAberto {
|
if pedidoAberto {
|
||||||
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "pedido_em_aberto"})
|
writeJSON(w, http.StatusOK, contratos.PedidoResponse{PodeAbrir: false, Motivo: "pedido_em_aberto"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := h.store.CreatePedido(ctx, table, in, r)
|
id, err := h.store.CreatePedido(ctx, table, in, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Error("erro ao criar pedido", "err", err)
|
slog.Error("erro ao criar pedido", "err", err)
|
||||||
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
writeJSON(w, http.StatusOK, contratos.PedidoResponse{PodeAbrir: false, Motivo: "erro"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,7 +103,7 @@ func (h *Handlers) PatchResposta(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var in PatchInput
|
var in contratos.PatchInput
|
||||||
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
|
||||||
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "json_invalido"})
|
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "json_invalido"})
|
||||||
return
|
return
|
||||||
|
|
@ -132,7 +133,7 @@ func (h *Handlers) PatchResposta(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Write([]byte("db"))
|
w.Write([]byte("db"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data := FormPageData{Produto: prod, ID: id, Reg: reg}
|
data := contratos.FormPageData{Produto: prod, ID: id, Reg: reg}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
h.tpl.Render(w, "form_inner.html", data)
|
h.tpl.Render(w, "form_inner.html", data)
|
||||||
return
|
return
|
||||||
|
|
@ -173,7 +174,7 @@ func (h *Handlers) GetForm(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := FormPageData{
|
data := contratos.FormPageData{
|
||||||
Produto: prod,
|
Produto: prod,
|
||||||
ID: id,
|
ID: id,
|
||||||
Reg: reg,
|
Reg: reg,
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
package elinps
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type PedidoInput struct {
|
|
||||||
ProdutoNome string `json:"produto_nome"`
|
|
||||||
InquilinoCodigo string `json:"inquilino_codigo"`
|
|
||||||
InquilinoNome string `json:"inquilino_nome"`
|
|
||||||
UsuarioCodigo string `json:"usuario_codigo"`
|
|
||||||
UsuarioNome string `json:"usuario_nome"`
|
|
||||||
UsuarioTelefone string `json:"usuario_telefone"`
|
|
||||||
UsuarioEmail string `json:"usuario_email"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PedidoResponse struct {
|
|
||||||
PodeAbrir bool `json:"pode_abrir"`
|
|
||||||
Motivo string `json:"motivo,omitempty"`
|
|
||||||
ID string `json:"id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PatchInput struct {
|
|
||||||
Nota *int `json:"nota,omitempty"`
|
|
||||||
Justificativa *string `json:"justificativa,omitempty"`
|
|
||||||
Finalizar bool `json:"finalizar,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Registro struct {
|
|
||||||
// ProdutoNome é o nome original do produto como enviado pela integração/widget.
|
|
||||||
// Ele existe apenas para exibição ao usuário.
|
|
||||||
//
|
|
||||||
// Importante: a normalização (remoção de acentos/símbolos) é usada apenas
|
|
||||||
// para formar o nome da tabela no Postgres e o parâmetro {produto} da rota.
|
|
||||||
ProdutoNome string
|
|
||||||
ID string
|
|
||||||
Status string
|
|
||||||
Nota *int
|
|
||||||
Justificativa *string
|
|
||||||
PedidoCriadoEm time.Time
|
|
||||||
RespondidoEm *time.Time
|
|
||||||
}
|
|
||||||
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"e-li.nps/internal/contratos"
|
||||||
"e-li.nps/internal/db"
|
"e-li.nps/internal/db"
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
)
|
)
|
||||||
|
|
@ -57,37 +58,11 @@ func (a AuthPainel) handlerLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/painel", http.StatusFound)
|
http.Redirect(w, r, "/painel", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NPSMensal representa o cálculo do NPS agregado por mês.
|
type NPSMensal = contratos.NPSMensal
|
||||||
type NPSMensal struct {
|
|
||||||
Mes string
|
|
||||||
Detratores int
|
|
||||||
Neutros int
|
|
||||||
Promotores int
|
|
||||||
Total int
|
|
||||||
NPS int
|
|
||||||
}
|
|
||||||
|
|
||||||
// RespostaPainel representa uma resposta para listagem no painel.
|
type RespostaPainel = contratos.RespostaPainel
|
||||||
type RespostaPainel struct {
|
|
||||||
ID string
|
|
||||||
RespondidoEm *time.Time
|
|
||||||
PedidoCriadoEm time.Time
|
|
||||||
UsuarioCodigo *string
|
|
||||||
UsuarioNome string
|
|
||||||
UsuarioEmail *string
|
|
||||||
Nota *int
|
|
||||||
Justificativa *string
|
|
||||||
}
|
|
||||||
|
|
||||||
type PainelDados struct {
|
type PainelDados = contratos.PainelDados
|
||||||
Produto string
|
|
||||||
Produtos []string
|
|
||||||
Meses []NPSMensal
|
|
||||||
Respostas []RespostaPainel
|
|
||||||
Pagina int
|
|
||||||
SomenteBaixas bool
|
|
||||||
MsgErro string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a AuthPainel) handlerPainel(w http.ResponseWriter, r *http.Request, store *Store) {
|
func (a AuthPainel) handlerPainel(w http.ResponseWriter, r *http.Request, store *Store) {
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
@ -145,7 +120,7 @@ func (a AuthPainel) handlerPainel(w http.ResponseWriter, r *http.Request, store
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Se a tabela ainda não tem coluna ip_real/ etc, EnsureNPSTable deveria ajustar.
|
// Se a tabela ainda não tem coluna ip_real/ etc, EnsureNPSTable deveria ajustar.
|
||||||
if err == pgx.ErrNoRows {
|
if err == pgx.ErrNoRows {
|
||||||
respostas = []RespostaPainel{}
|
respostas = []contratos.RespostaPainel{}
|
||||||
} else {
|
} else {
|
||||||
dados.MsgErro = "erro ao listar respostas"
|
dados.MsgErro = "erro ao listar respostas"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"e-li.nps/internal/contratos"
|
||||||
"e-li.nps/internal/db"
|
"e-li.nps/internal/db"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
@ -43,7 +44,7 @@ ORDER BY tablename`)
|
||||||
// - 1–6 detratores
|
// - 1–6 detratores
|
||||||
// - 7–8 neutros
|
// - 7–8 neutros
|
||||||
// - 9–10 promotores
|
// - 9–10 promotores
|
||||||
func (s *Store) NPSMesAMes(ctx context.Context, tabela string, meses int) ([]NPSMensal, error) {
|
func (s *Store) NPSMesAMes(ctx context.Context, tabela string, meses int) ([]contratos.NPSMensal, error) {
|
||||||
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
||||||
if !db.TableNameValido(tabela) {
|
if !db.TableNameValido(tabela) {
|
||||||
return nil, fmt.Errorf("tabela invalida")
|
return nil, fmt.Errorf("tabela invalida")
|
||||||
|
|
@ -75,9 +76,9 @@ ORDER BY mes ASC`, tabela)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
out := []NPSMensal{}
|
out := []contratos.NPSMensal{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var m NPSMensal
|
var m contratos.NPSMensal
|
||||||
if err := rows.Scan(&m.Mes, &m.Detratores, &m.Neutros, &m.Promotores, &m.Total); err != nil {
|
if err := rows.Scan(&m.Mes, &m.Detratores, &m.Neutros, &m.Promotores, &m.Total); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -97,17 +98,10 @@ type ListarRespostasFiltro struct {
|
||||||
PorPagina int
|
PorPagina int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *ListarRespostasFiltro) normalizar() {
|
func (f *ListarRespostasFiltro) normalizar() { (*contratos.ListarRespostasFiltro)(f).Normalizar() }
|
||||||
if f.Pagina <= 0 {
|
|
||||||
f.Pagina = 1
|
|
||||||
}
|
|
||||||
if f.PorPagina <= 0 || f.PorPagina > 200 {
|
|
||||||
f.PorPagina = 50
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListarRespostas retorna respostas respondidas, com paginação e filtro.
|
// ListarRespostas retorna respostas respondidas, com paginação e filtro.
|
||||||
func (s *Store) ListarRespostas(ctx context.Context, tabela string, filtro ListarRespostasFiltro) ([]RespostaPainel, error) {
|
func (s *Store) ListarRespostas(ctx context.Context, tabela string, filtro ListarRespostasFiltro) ([]contratos.RespostaPainel, error) {
|
||||||
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
||||||
if !db.TableNameValido(tabela) {
|
if !db.TableNameValido(tabela) {
|
||||||
return nil, fmt.Errorf("tabela invalida")
|
return nil, fmt.Errorf("tabela invalida")
|
||||||
|
|
@ -145,9 +139,9 @@ LIMIT $1 OFFSET $2`, tabela, cond)
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
|
|
||||||
respostas := []RespostaPainel{}
|
respostas := []contratos.RespostaPainel{}
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var r RespostaPainel
|
var r contratos.RespostaPainel
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
&r.ID,
|
&r.ID,
|
||||||
&r.RespondidoEm,
|
&r.RespondidoEm,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"e-li.nps/internal/contratos"
|
||||||
"e-li.nps/internal/db"
|
"e-li.nps/internal/db"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
@ -98,7 +99,7 @@ LIMIT 1`, table)
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CreatePedido(ctx context.Context, table string, in PedidoInput, r *http.Request) (string, error) {
|
func (s *Store) CreatePedido(ctx context.Context, table string, in contratos.PedidoInput, r *http.Request) (string, error) {
|
||||||
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
||||||
if !db.TableNameValido(table) {
|
if !db.TableNameValido(table) {
|
||||||
return "", fmt.Errorf("tabela invalida")
|
return "", fmt.Errorf("tabela invalida")
|
||||||
|
|
@ -122,24 +123,24 @@ RETURNING id`, table)
|
||||||
return id, err
|
return id, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) GetRegistro(ctx context.Context, table, id string) (Registro, error) {
|
func (s *Store) GetRegistro(ctx context.Context, table, id string) (contratos.Registro, error) {
|
||||||
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
||||||
if !db.TableNameValido(table) {
|
if !db.TableNameValido(table) {
|
||||||
return Registro{}, fmt.Errorf("tabela invalida")
|
return contratos.Registro{}, fmt.Errorf("tabela invalida")
|
||||||
}
|
}
|
||||||
q := fmt.Sprintf(`
|
q := fmt.Sprintf(`
|
||||||
SELECT id, produto_nome, status, nota, justificativa, pedido_criado_em, respondido_em
|
SELECT id, produto_nome, status, nota, justificativa, pedido_criado_em, respondido_em
|
||||||
FROM %s
|
FROM %s
|
||||||
WHERE id=$1`, table)
|
WHERE id=$1`, table)
|
||||||
|
|
||||||
var reg Registro
|
var reg contratos.Registro
|
||||||
err := s.pool.QueryRow(ctx, q, id).Scan(
|
err := s.pool.QueryRow(ctx, q, id).Scan(
|
||||||
®.ID, ®.ProdutoNome, ®.Status, ®.Nota, ®.Justificativa, ®.PedidoCriadoEm, ®.RespondidoEm,
|
®.ID, ®.ProdutoNome, ®.Status, ®.Nota, ®.Justificativa, ®.PedidoCriadoEm, ®.RespondidoEm,
|
||||||
)
|
)
|
||||||
return reg, err
|
return reg, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) PatchRegistro(ctx context.Context, table, id string, in PatchInput) error {
|
func (s *Store) PatchRegistro(ctx context.Context, table, id string, in contratos.PatchInput) error {
|
||||||
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
// Segurança: a tabela é um identificador interpolado. Validamos sempre.
|
||||||
if !db.TableNameValido(table) {
|
if !db.TableNameValido(table) {
|
||||||
return fmt.Errorf("tabela invalida")
|
return fmt.Errorf("tabela invalida")
|
||||||
|
|
@ -169,7 +170,7 @@ func (s *Store) TouchAtualizadoEm(ctx context.Context, table, id string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) CooldownSuggested(reg Registro) time.Duration {
|
func (s *Store) CooldownSuggested(reg contratos.Registro) time.Duration {
|
||||||
// Não é usado pelo servidor hoje; fica como helper se precisarmos.
|
// Não é usado pelo servidor hoje; fica como helper se precisarmos.
|
||||||
if reg.Status == "respondido" {
|
if reg.Status == "respondido" {
|
||||||
return 45 * 24 * time.Hour
|
return 45 * 24 * time.Hour
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"html/template"
|
"html/template"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"e-li.nps/internal/contratos"
|
||||||
)
|
)
|
||||||
|
|
||||||
type TemplateRenderer struct {
|
type TemplateRenderer struct {
|
||||||
|
|
@ -23,8 +25,5 @@ func (r *TemplateRenderer) Render(w http.ResponseWriter, name string, data any)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormPageData struct {
|
// Alias para manter as chamadas concisas dentro do pacote elinps.
|
||||||
Produto string
|
type FormPageData = contratos.FormPageData
|
||||||
ID string
|
|
||||||
Reg Registro
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"e-li.nps/internal/contratos"
|
||||||
)
|
)
|
||||||
|
|
||||||
var emailRe = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
|
var emailRe = regexp.MustCompile(`^[^\s@]+@[^\s@]+\.[^\s@]+$`)
|
||||||
|
|
@ -12,7 +14,7 @@ func normalizeEmail(s string) string {
|
||||||
return strings.ToLower(strings.TrimSpace(s))
|
return strings.ToLower(strings.TrimSpace(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidatePedidoInput(in *PedidoInput) error {
|
func ValidatePedidoInput(in *contratos.PedidoInput) error {
|
||||||
in.ProdutoNome = strings.TrimSpace(in.ProdutoNome)
|
in.ProdutoNome = strings.TrimSpace(in.ProdutoNome)
|
||||||
in.InquilinoCodigo = strings.TrimSpace(in.InquilinoCodigo)
|
in.InquilinoCodigo = strings.TrimSpace(in.InquilinoCodigo)
|
||||||
in.InquilinoNome = strings.TrimSpace(in.InquilinoNome)
|
in.InquilinoNome = strings.TrimSpace(in.InquilinoNome)
|
||||||
|
|
@ -49,7 +51,7 @@ func ValidatePedidoInput(in *PedidoInput) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidatePatchInput(in *PatchInput) error {
|
func ValidatePatchInput(in *contratos.PatchInput) error {
|
||||||
if in.Nota != nil {
|
if in.Nota != nil {
|
||||||
if *in.Nota < 1 || *in.Nota > 10 {
|
if *in.Nota < 1 || *in.Nota > 10 {
|
||||||
return errors.New("nota invalida")
|
return errors.New("nota invalida")
|
||||||
|
|
|
||||||
Binary file not shown.
BIN
web/static/favicon.ico
Normal file
BIN
web/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
Loading…
Add table
Add a link
Reference in a new issue