From e8ca410b941b4c6f3920d168f5a8ea7edd9b6578 Mon Sep 17 00:00:00 2001 From: Luiz Silva Date: Mon, 5 Jan 2026 08:57:14 -0300 Subject: [PATCH] adicionado 0 na escal de nota --- README.md | 2 ++ internal/db/schema.go | 55 +++++++++++++++++++++++++++++-- internal/elinps/painel_queries.go | 10 +++--- internal/elinps/validate.go | 3 +- migrations/001_init.sql | 2 +- web/templates/form_inner.html | 2 +- web/templates/form_page.html | 11 ++++--- web/templates/nota_block.html | 2 +- 8 files changed, 72 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index e54a623..79f5dfa 100644 --- a/README.md +++ b/README.md @@ -380,6 +380,8 @@ Abre o formulário (HTML) para responder/editar. ### `PATCH /api/e-li.nps/{produto}/{id}` +Regra de negócio: a nota (NPS) vai de **0 até 10**. + ```bash curl -sS -X PATCH http://localhost:8080/api/e-li.nps/elicencie_gov/ \ -H 'Content-Type: application/json' \ diff --git a/internal/db/schema.go b/internal/db/schema.go index 4a96470..eeda0ea 100644 --- a/internal/db/schema.go +++ b/internal/db/schema.go @@ -120,7 +120,8 @@ CREATE TABLE IF NOT EXISTS %s ( pedido_criado_em timestamptz NOT NULL DEFAULT now(), respondido_em timestamptz NULL, atualizado_em timestamptz NOT NULL DEFAULT now(), - nota int NULL CHECK (nota BETWEEN 1 AND 10), + -- Escala NPS do projeto: 0–10. + nota int NULL CHECK (nota BETWEEN 0 AND 10), justificativa text NULL, valida bool NOT NULL DEFAULT true, origem text NOT NULL DEFAULT 'widget_iframe', @@ -135,6 +136,43 @@ ALTER TABLE %s ADD COLUMN IF NOT EXISTS usuario_codigo text; ALTER TABLE %s ADD COLUMN IF NOT EXISTS produto_nome text NOT NULL DEFAULT ''; ALTER TABLE %s ADD COLUMN IF NOT EXISTS ip_real text; +-- Migração defensiva (em runtime) da constraint de nota. +-- +-- Motivação: +-- - Em versões anteriores a escala era 1–10. +-- - As tabelas por produto são criadas automaticamente; portanto, podem existir +-- tabelas antigas com CHECK antigo em nota. +-- +-- Estratégia: +-- - Remover qualquer CHECK existente que mencione a coluna nota. +-- - Recriar uma constraint nomeada ck_nota_0_10 com a regra atual (0–10). +-- +-- Segurança: tableName é validado por TableNameValido (regex) antes de ser +-- interpolado e usado como regclass/identificador. +DO $$ +DECLARE c record; +BEGIN + FOR c IN + SELECT conname + FROM pg_constraint + WHERE conrelid = '%s'::regclass + AND contype='c' + AND pg_get_constraintdef(oid) ILIKE '%%nota%%' + LOOP + EXECUTE format('ALTER TABLE %%I DROP CONSTRAINT %%I', '%s', c.conname); + END LOOP; + + IF NOT EXISTS ( + SELECT 1 + FROM pg_constraint + WHERE conrelid = '%s'::regclass + AND contype='c' + AND conname='ck_nota_0_10' + ) THEN + EXECUTE format('ALTER TABLE %%I ADD CONSTRAINT ck_nota_0_10 CHECK (nota BETWEEN 0 AND 10)', '%s'); + END IF; +END$$; + -- NOTE: controle de exibição é por (produto + inquilino_codigo + usuario_codigo) -- então os índices são baseados em usuario_codigo. @@ -145,7 +183,20 @@ CREATE INDEX IF NOT EXISTS idx_nps_resp_recente_%s CREATE INDEX IF NOT EXISTS idx_nps_pedido_aberto_%s ON %s (inquilino_codigo, usuario_codigo, pedido_criado_em DESC) WHERE status='pedido'; -`, tableName, tableName, tableName, tableName, tableName, tableName, tableName, tableName) +`, + tableName, // CREATE TABLE + tableName, // ALTER TABLE add usuario_codigo + tableName, // ALTER TABLE add produto_nome + tableName, // ALTER TABLE add ip_real + tableName, // DO block: conrelid (1) + tableName, // DO block: DROP CONSTRAINT (identificador) + tableName, // DO block: conrelid (2) + tableName, // DO block: ADD CONSTRAINT (identificador) + tableName, // idx_nps_resp_recente_%s + tableName, // ON %s + tableName, // idx_nps_pedido_aberto_%s + tableName, // ON %s + ) _, err := pool.Exec(ctx, q) return err diff --git a/internal/elinps/painel_queries.go b/internal/elinps/painel_queries.go index 22ac1ee..e8f6f85 100644 --- a/internal/elinps/painel_queries.go +++ b/internal/elinps/painel_queries.go @@ -40,8 +40,8 @@ ORDER BY tablename`) // NPSMesAMes calcula o NPS por mês para um produto (tabela `nps_{produto}`). // -// Regra NPS (1–10): -// - 1–6 detratores +// Regra NPS (0–10): +// - 0–6 detratores // - 7–8 neutros // - 9–10 promotores func (s *Store) NPSMesAMes(ctx context.Context, tabela string, meses int) ([]contratos.NPSMensal, error) { @@ -62,7 +62,7 @@ WITH base AS ( ) SELECT to_char(mes, 'YYYY-MM') AS mes, - SUM(CASE WHEN nota BETWEEN 1 AND 6 THEN 1 ELSE 0 END)::int AS detratores, + SUM(CASE WHEN nota BETWEEN 0 AND 6 THEN 1 ELSE 0 END)::int AS detratores, SUM(CASE WHEN nota BETWEEN 7 AND 8 THEN 1 ELSE 0 END)::int AS neutros, SUM(CASE WHEN nota BETWEEN 9 AND 10 THEN 1 ELSE 0 END)::int AS promotores, COUNT(*)::int AS total @@ -116,7 +116,7 @@ func (s *Store) ListarRespostas(ctx context.Context, tabela string, filtro Lista cond := "status='respondido' AND valida=true" if filtro.SomenteNotasBaixas { - cond += " AND nota BETWEEN 1 AND 6" + cond += " AND nota BETWEEN 0 AND 6" } // Importante (segurança): apesar do cond ser construído em string, ele NÃO usa @@ -180,7 +180,7 @@ func (s *Store) ExportarRespostas(ctx context.Context, tabela string, filtro Exp cond := "status='respondido' AND valida=true" if filtro.SomenteNotasBaixas { - cond += " AND nota BETWEEN 1 AND 6" + cond += " AND nota BETWEEN 0 AND 6" } // Sem LIMIT/OFFSET (export completo). diff --git a/internal/elinps/validate.go b/internal/elinps/validate.go index 75bf0ae..e1d9d0c 100644 --- a/internal/elinps/validate.go +++ b/internal/elinps/validate.go @@ -53,7 +53,8 @@ func ValidatePedidoInput(in *contratos.PedidoInput) error { func ValidatePatchInput(in *contratos.PatchInput) error { if in.Nota != nil { - if *in.Nota < 1 || *in.Nota > 10 { + // Regra do produto: escala NPS é 0–10. + if *in.Nota < 0 || *in.Nota > 10 { return errors.New("nota invalida") } } diff --git a/migrations/001_init.sql b/migrations/001_init.sql index bc51904..fd8b796 100644 --- a/migrations/001_init.sql +++ b/migrations/001_init.sql @@ -14,7 +14,7 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto; -- pedido_criado_em timestamptz NOT NULL DEFAULT now(), -- respondido_em timestamptz NULL, -- atualizado_em timestamptz NOT NULL DEFAULT now(), --- nota int NULL CHECK (nota BETWEEN 1 AND 10), +-- nota int NULL CHECK (nota BETWEEN 0 AND 10), -- justificativa text NULL, -- valida bool NOT NULL DEFAULT true, -- origem text NOT NULL DEFAULT 'widget_iframe', diff --git a/web/templates/form_inner.html b/web/templates/form_inner.html index b0567f1..2772de8 100644 --- a/web/templates/form_inner.html +++ b/web/templates/form_inner.html @@ -7,7 +7,7 @@ {{template "edit_block.html" .}} {{else}} -

De 1 a 10, quanto você recomenda {{if .Reg.ProdutoNome}}{{.Reg.ProdutoNome}}{{else}}{{produtoLabel .Produto}}{{end}} para um amigo?

+

De 0 a 10, quanto você recomenda {{if .Reg.ProdutoNome}}{{.Reg.ProdutoNome}}{{else}}{{produtoLabel .Produto}}{{end}} para um amigo?

{{template "nota_block.html" .}} {{if .Reg.Nota}} {{template "justificativa_block.html" .}} diff --git a/web/templates/form_page.html b/web/templates/form_page.html index c2cd634..cd3f162 100644 --- a/web/templates/form_page.html +++ b/web/templates/form_page.html @@ -16,9 +16,9 @@ .eli-nps-card{max-width:520px;margin:0 auto;padding:16px;} .eli-nps-title{font-size:18px;font-weight:700;margin:0 0 8px;} .eli-nps-sub{margin:0 0 12px;color:#444;font-size:14px;} - .eli-nps-scale{display:grid;grid-template-columns:repeat(10,1fr);gap:8px;margin:12px 0;} + .eli-nps-scale{display:grid;grid-template-columns:repeat(11,1fr);gap:8px;margin:12px 0;} /* - Botões de nota (1–10) com escala vermelho → verde. + Botões de nota (0–10) com escala vermelho → verde. Importante: a cor base é determinada pelo valor; quando selecionado, o botão ganha destaque (borda/sombra) mas mantém a cor. */ @@ -48,12 +48,15 @@ /* Paleta por grupo: - - 1 a 6: tons de vermelho + - 0 a 6: tons de vermelho - 7 e 8: tons de amarelo - 9 e 10: tons de verde */ - /* 1–6 (vermelho) */ + /* 0–6 (vermelho) */ + .eli-nps-btn-0{background:#ffebee;border-color:#ffcdd2;} + .eli-nps-btn-0:hover,.eli-nps-btn-0.eli-nps-btn-selected{background:#f44336;color:#fff;border-color:#f44336;} + .eli-nps-btn-1{background:#ffebee;border-color:#ffcdd2;} .eli-nps-btn-1:hover,.eli-nps-btn-1.eli-nps-btn-selected{background:#ef5350;color:#fff;border-color:#ef5350;} diff --git a/web/templates/nota_block.html b/web/templates/nota_block.html index fadd8a5..8b2f93c 100644 --- a/web/templates/nota_block.html +++ b/web/templates/nota_block.html @@ -1,6 +1,6 @@ {{define "nota_block.html"}}
- {{range $i := seq 1 10}} + {{range $i := seq 0 10}}