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) }