Pesquisas
Núcleo do produto: criação, edição, encerramento e exclusão de pesquisas. É também onde as regras por papel mais aparecem — ADMIN, SURVEYOR e VIEWER veem botões e dados diferentes a partir do mesmo card.
Onde mora o módulo
Uma pesquisa Neuroredes é um conjunto de até cinco perguntas que cada respondente avalia para todos os outros respondentes da mesma pesquisa (modelo “todos avaliam todos”). Os respondentes podem ter até sete atributos categorizadores (departamento, cargo, etc.) usados depois pelo grafo e pelo dashboard para segmentar a análise.
Páginas Next.js
- app/(main)/surveys/page.tsx
- Lista de pesquisas. Server component lê o cookie
user_profile, chamagetSurveysRpce renderizaSurveysClient. - app/(main)/surveys/create/page.tsx
- Criação via stepper de cinco etapas. Bloqueado para VIEWER.
- app/(main)/surveys/[id]/edit/page.tsx
- Edição. O modo (full, limited, readonly) é resolvido por
computeEditMode(survey). - app/(main)/surveys/[id]/view/page.tsx
- Leitura. Redireciona VIEWER para
/surveys.
Componentes principais
Em components/Surveys/: SurveysClient (lista + filtros),
SurveysCard (ações por papel), CreateSurveyClient +
CreateSurveySteps/* (stepper), EditSurveyClient,
ViewSurveyClient. As páginas de envio (SendSurveyClient,
MessageListClient, etc.) pertencem ao módulo
Envio (Magic Links).
Edge Functions
create-survey, edit-survey, delete-survey,
close-survey, survey-handler. A listagem usa o
padrão RPC direto em rpc/surveys/get-surveys.ts.
Modelo de dados
Todas as tabelas vivem no schema mydb. Os nomes de coluna no
banco são em inglês; o mapeamento para português acontece
no RPC de listagem (ver Capítulo 06).
| Tabela | Papel |
|---|---|
mydb.surveys |
Cabeçalho da pesquisa: title, description, question_1…question_5 (wide table — perguntas como colunas), start_date, end_date, status, company_id. |
mydb.survey_attributes |
PK survey_id. attribute_1…attribute_7 definem os nomes dos atributos da pesquisa. |
mydb.survey_respondents |
Pessoas que respondem: name, email, phone, attribute_1…7 (valores), email_sent, whatsapp_sent, finished_filling, active. |
mydb.survey_responses |
Respostas. PK composta (respondent_id, evaluatee_id): cada par avaliador/avaliado tem uma linha com question_1…5 (notas). |
mydb.survey_respondent_tokens |
Magic links: token_hash, expires_at, used_at, revoked_at. Detalhado em Envio. |
mydb.survey_processing_results |
Cache atual de Neurocalc e do grafo, com processing_status e cache_version. |
mydb.survey_cache |
Cache legado. Coexiste com survey_processing_results — ver Pegadinhas. |
Perguntas e atributos são modelados como colunas fixas
(question_1…question_5, attribute_1…attribute_7),
não como tabelas filhas. Por isso o teto rígido de cinco perguntas e
sete atributos. Mudar esses limites exige migration.
Status e ciclo de vida
O campo status em mydb.surveys é um smallint
com valores 0 a 3. O texto exibido na UI (status_texto) é
computado em tempo de leitura em
rpc/_utils/get-status-text.ts, combinando status com
end_date.
| Código | Texto exibido | Quando |
|---|---|---|
| 0 | Rascunho | Estado legado. create-survey não produz mais este valor. |
| 1 | Aguardando | Default ao criar. Permite edição completa enquanto end_date não vencer. |
| 2 | Em Andamento | Pesquisa já com envios realizados. Edição passa a ser limitada. |
| 3 | Arquivada | Encerramento manual via close-survey, ou automático após end_date. |
| 1 ou 2 | Concluída | Pseudo-status: aparece sempre que end_date <= NOW(), independente do status persistido. Existe só na UI. |
Auto-close de pesquisas expiradas
Uma cron roda public.close_expired_surveys() a cada
cinco minutos via pg_cron. Migração:
20260407120000_auto_close_expired_surveys.sql.
UPDATE public.surveys
SET status = 3, updated_at = NOW()
WHERE status <> 3
AND end_date IS NOT NULL
AND end_date <= NOW();
Não há efeito colateral além da mudança de status: os dados
permanecem intactos e a pesquisa continua acessível para leitura,
relatórios e dashboard.
Permissões por papel
O SurveysCard esconde botões por papel; o backend faz o
enforcement independentemente. Cada Edge Function recebe
profile.role e profile.company_id no payload e
valida antes de qualquer escrita.
| Ação | ADMIN | SURVEYOR | VIEWER |
|---|---|---|---|
| Listar pesquisas | Todas | Da própria empresa | Apenas a sua |
| Criar | Sim, qualquer empresa | Só a sua empresa | Não |
| Editar | Sim | Só da sua empresa | Não |
| Visualizar | Sim | Só da sua empresa | Redirecionado para /surveys |
| Encerrar | Sim | Só da sua empresa | Não |
| Excluir | Sim | Só da sua empresa | Não |
| Enviar magic links | Sim | Sim | Não |
| Análise (Excel, grafo, dashboard) | Sim | Sim | Sim |
ADMIN tem profile.company_id = null. Ações que precisam
de um company_id (excluir, encerrar) usam o
survey.empresa_id do recurso-alvo. Isso está resolvido em
SurveysCard.tsx e replicado em qualquer outro lugar que
dispare ações de pesquisa para ADMIN.
Fluxos
Listar
app/(main)/surveys/page.tsx chama
getSurveysRpc(profile). O RPC despacha para uma de três
funções no Postgres conforme o papel:
- ADMIN →
get_all_surveys() - SURVEYOR →
get_surveys_by_company(profile.company_id) - VIEWER →
get_survey_by_id(profile.survey_id)
O resultado é mapeado para o tipo Survey (campos em pt-br) e
cada linha recebe status_texto via
getStatusText(status_codigo, start_date, end_date).
Criar
Stepper de cinco etapas: dados principais, perguntas, atributos,
respondentes, revisão. Ao confirmar, o frontend chama a Edge Function
create-survey, que executa três INSERTs em
sequência com rollback manual em caso de falha:
INSERT INTO mydb.surveyscomstatus = 1(Aguardando).INSERT INTO mydb.survey_attributesligando os nomes dos atributos aosurvey_id.INSERT INTO mydb.survey_respondentsem lote — telefones são limpos para conter só dígitos (máx. 14).
Se qualquer um dos passos falhar, a Edge Function deleta os artefatos já criados antes de retornar erro. Não há transação Postgres real — é compensação aplicativa.
Editar
O modo de edição é decidido por computeEditMode(survey) no
cliente:
| Condição | Modo | Permite editar |
|---|---|---|
end_date já passou | readonly | Nada |
status = 1 (Aguardando) | full | Tudo: dados, perguntas, atributos, respondentes |
status = 2 (Em Andamento) | limited | Apenas fim (data) e email/telefone dos respondentes |
status ∈ {0, 3} | readonly | Nada |
No modo full, se já existirem respondentes com email_sent = 1
(magic links disparados), a Edge Function edit-survey não
completa imediatamente. Ela retorna HTTP 200 com:
{ requiresConfirmation: true, reason: 'magic_links_sent' }
O cliente abre um diálogo de confirmação. Se o usuário confirma, o
cliente reenvia o mesmo payload com confirmDestructive: true,
e a Edge Function executa o cleanup antes da edição:
- Apaga linhas relevantes em
mydb.survey_responses. - Marca tokens em
mydb.survey_respondent_tokenscomrevoked_at = NOW(). - Reseta flags dos respondentes:
email_sent = 0,whatsapp_sent = 0,finished_filling = NULL.
Editar uma pesquisa Aguardando com magic links já enviados destrói as respostas já coletadas e exige novo envio. A confirmação é obrigatória, mas o efeito é irreversível depois de aplicado.
No modo limited, o payload aceita apenas fim e
respondentContacts (lista de { id, email, telefone }
para os respondentes alterados). Nenhuma resposta é descartada.
Visualizar
Página somente-leitura. Redireciona VIEWER para /surveys;
para SURVEYOR, valida se a pesquisa pertence à própria empresa antes de
renderizar.
Encerrar
Botão “Encerrar” no card abre um ConfirmDialog e chama
closeSurveyClient → Edge Function close-survey.
A função executa UPDATE mydb.surveys SET status = 3
para o survey_id, depois de validar papel e ownership.
Encerrar uma pesquisa já com status = 3 retorna erro.
Excluir
Cascata manual em sete passos, na ordem abaixo. A ordem importa: respostas e tokens dependem de respondentes; respondentes e atributos dependem da pesquisa.
- Lê os
ids dos respondentes da pesquisa. DELETE FROM mydb.survey_responsesporrespondent_id.DELETE FROM mydb.survey_respondent_tokensporrespondent_id.DELETE FROM mydb.survey_respondentsporsurvey_id.DELETE FROM mydb.survey_attributesporsurvey_id.- Caches (
survey_processing_results,survey_cache) e logs (survey_processing_log). DELETE FROM mydb.surveys.
survey_messages, survey_message_respondents e
survey_viewers são removidos via ON DELETE CASCADE
nas foreign keys, sem precisar de DELETE explícito.
Mapeamento pt-br ↔ en-us
Os tipos TypeScript e a UI usam nomes em português. O banco está em
inglês. A camada de tradução é o mapeador
mapSurveyRowToSurvey em
rpc/surveys/get-surveys.ts:
| Frontend (pt-br) | Banco (en) |
|---|---|
titulo | title |
descricao | description |
empresa_id | company_id |
data_inicio | start_date |
data_fim | end_date |
status_codigo | status |
status_texto | computado via getStatusText() |
data_criacao | created_at |
data_atualizacao | updated_at |
Edge Functions de escrita (create-survey,
edit-survey, etc.) recebem o payload em pt-br e fazem o
mapeamento inverso ao montar os INSERT/UPDATE.
Não há view ou alias no banco — o mapeamento é puro código de
aplicação.
Pegadinhas
Cache: legado e novo coexistem
Existem duas tabelas de cache para o mesmo dado:
mydb.survey_cache (legado, neurocalc_result e
graph_result simples) e
mydb.survey_processing_results (atual, com
processing_status e cache_version). A migração
está em curso. Ao tocar em código de cache, confirme em
survey-handler/index.ts qual fonte está sendo lida e
escrita; pode haver fallback entre as duas.
Status “Concluída” não existe no banco
Pesquisas com end_date no passado mas status ≠ 3
aparecem na UI como Concluída. Isso é puro cálculo do RPC. O
auto-close via pg_cron normaliza o estado em até cinco
minutos, mas no intervalo entre vencimento e cron a UI já mostra
“Concluída”.
Datas armazenadas naive, interpretadas como BRT
start_date e end_date não carregam timezone
explícito. O cálculo de “já venceu?” em
get-status-text.ts assume offset de -3h (BRT). Mudar a
interpretação exige ajustar essa função e revisar o cron.
Sem schemas Zod centralizados para survey
Diferente de outros domínios, lib/zod/ não tem schema
próprio para pesquisa. A validação está distribuída entre os
formulários do stepper (EditSurveyClient.validate(),
equivalentes em CreateSurveyClient) e as Edge Functions.
Acrescentar campo novo exige tocar nos dois lados.
Tokens revogados precisam ser checados na validação
Quando uma edição destrutiva revoga tokens (revoked_at = NOW()),
a validação de magic link em
Resposta Pública precisa rejeitar
tokens com revoked_at IS NOT NULL. Caso contrário, o link
antigo continua respondendo.
Sem RLS em tabelas-chave
survey_respondent_tokens, survey_messages e
survey_message_respondents estão com Row Level Security
desabilitado. As Edge Functions usam SERVICE_ROLE_KEY e,
desde 2026-06-10, autorizam o chamador pelo JWT
(verify_jwt: true + _shared/auth.ts, ver
backend.html Cap. 03);
ainda assim, qualquer acesso direto às tabelas via anon key
ignoraria essas validações — o vetor aberto é a falta de RLS, não a edge.