e-li-nps/internal/elinps/handlers.go

192 lines
5.6 KiB
Go

package elinps
import (
"encoding/json"
"errors"
"log/slog"
"net/http"
"strings"
"e-li.nps/internal/db"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
type Handlers struct {
store *Store
tpl *TemplateRenderer
}
func NewHandlers(pool *pgxpool.Pool) *Handlers {
return &Handlers{
store: NewStore(pool),
tpl: NewTemplateRenderer(mustParseTemplates()),
}
}
func (h *Handlers) PostPedido(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
var in PedidoInput
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "json_invalido"})
return
}
if err := ValidatePedidoInput(&in); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
return
}
// Garante a tabela do produto (e normaliza o identificador técnico).
table, err := h.store.EnsureTableForProduto(ctx, in.ProdutoNome)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "produto_invalido"})
return
}
// Mantemos a forma normalizada para o widget montar URLs com segurança.
// table = "nps_" + produto_normalizado
produtoNormalizado := strings.TrimPrefix(table, "nps_")
// Regras.
respRecente, err := h.store.HasRespostaValidaRecente(ctx, table, in.InquilinoCodigo, in.UsuarioCodigo)
if err != nil {
slog.Error("erro ao checar resposta recente", "err", err)
// Fail-closed.
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
return
}
if respRecente {
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "resposta_recente"})
return
}
pedidoAberto, err := h.store.HasPedidoEmAbertoRecente(ctx, table, in.InquilinoCodigo, in.UsuarioCodigo)
if err != nil {
slog.Error("erro ao checar pedido em aberto", "err", err)
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
return
}
if pedidoAberto {
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "pedido_em_aberto"})
return
}
id, err := h.store.CreatePedido(ctx, table, in, r)
if err != nil {
slog.Error("erro ao criar pedido", "err", err)
writeJSON(w, http.StatusOK, PedidoResponse{PodeAbrir: false, Motivo: "erro"})
return
}
writeJSON(w, http.StatusOK, map[string]any{"pode_abrir": true, "id": id, "produto": produtoNormalizado})
}
func (h *Handlers) PatchResposta(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
produtoParam := chi.URLParam(r, "produto")
id := chi.URLParam(r, "id")
// produtoParam já está no path; sanitizamos novamente por segurança.
prod, err := db.NormalizeProduto(produtoParam)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "produto_invalido"})
return
}
table := db.TableNameForProduto(prod)
if err := db.EnsureNPSTable(ctx, h.store.pool, table); err != nil {
slog.Error("erro ao garantir tabela", "err", err)
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": "db"})
return
}
var in PatchInput
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "json_invalido"})
return
}
if err := ValidatePatchInput(&in); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": err.Error()})
return
}
if in.Nota == nil && in.Justificativa == nil && !in.Finalizar {
writeJSON(w, http.StatusBadRequest, map[string]any{"error": "nada_para_atualizar"})
return
}
if err := h.store.PatchRegistro(ctx, table, id, in); err != nil {
slog.Error("erro ao atualizar registro", "err", err)
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": "db"})
return
}
// Se chamado via HTMX, respondemos com fragmento HTML atualizado.
if r.Header.Get("HX-Request") == "true" {
reg, err := h.store.GetRegistro(ctx, table, id)
if err != nil {
slog.Error("erro ao buscar registro", "err", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("db"))
return
}
data := FormPageData{Produto: prod, ID: id, Reg: reg}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
h.tpl.Render(w, "form_inner.html", data)
return
}
writeJSON(w, http.StatusOK, map[string]any{"ok": true})
}
func (h *Handlers) GetForm(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
produtoParam := chi.URLParam(r, "produto")
id := chi.URLParam(r, "id")
prod, err := db.NormalizeProduto(produtoParam)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("produto invalido"))
return
}
table := db.TableNameForProduto(prod)
if err := db.EnsureNPSTable(ctx, h.store.pool, table); err != nil {
slog.Error("erro ao garantir tabela", "err", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("db"))
return
}
reg, err := h.store.GetRegistro(ctx, table, id)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("nao encontrado"))
return
}
slog.Error("erro ao buscar registro", "err", err)
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("db"))
return
}
data := FormPageData{
Produto: prod,
ID: id,
Reg: reg,
}
// Sempre retornamos uma página HTML completa para o widget usar iframe.
// Porém o container interno também é compatível com HTMX (swap de si mesmo).
w.Header().Set("Content-Type", "text/html; charset=utf-8")
h.tpl.Render(w, "form_page.html", data)
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(v)
}