Neuroredes
Módulos do Produto

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.

Capítulo 01

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, chama getSurveysRpc e renderiza SurveysClient.
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.

Capítulo 02

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

TabelaPapel
mydb.surveys Cabeçalho da pesquisa: title, description, question_1question_5 (wide table — perguntas como colunas), start_date, end_date, status, company_id.
mydb.survey_attributes PK survey_id. attribute_1attribute_7 definem os nomes dos atributos da pesquisa.
mydb.survey_respondents Pessoas que respondem: name, email, phone, attribute_17 (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_15 (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.
Nota

Perguntas e atributos são modelados como colunas fixas (question_1question_5, attribute_1attribute_7), não como tabelas filhas. Por isso o teto rígido de cinco perguntas e sete atributos. Mudar esses limites exige migration.

Capítulo 03

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.

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.

Capítulo 04

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 pesquisasTodasDa própria empresaApenas a sua
CriarSim, qualquer empresaSó a sua empresaNão
EditarSimSó da sua empresaNão
VisualizarSimSó da sua empresaRedirecionado para /surveys
EncerrarSimSó da sua empresaNão
ExcluirSimSó da sua empresaNão
Enviar magic linksSimSimNão
Análise (Excel, grafo, dashboard)SimSimSim
Destaque

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.

Capítulo 05

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:

  1. INSERT INTO mydb.surveys com status = 1 (Aguardando).
  2. INSERT INTO mydb.survey_attributes ligando os nomes dos atributos ao survey_id.
  3. INSERT INTO mydb.survey_respondents em 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çãoModoPermite editar
end_date já passoureadonlyNada
status = 1 (Aguardando)fullTudo: dados, perguntas, atributos, respondentes
status = 2 (Em Andamento)limitedApenas fim (data) e email/telefone dos respondentes
status ∈ {0, 3}readonlyNada

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:

edit-survey response typescript
{ 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_tokens com revoked_at = NOW().
  • Reseta flags dos respondentes: email_sent = 0, whatsapp_sent = 0, finished_filling = NULL.
Atenção

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.

  1. Lê os ids dos respondentes da pesquisa.
  2. DELETE FROM mydb.survey_responses por respondent_id.
  3. DELETE FROM mydb.survey_respondent_tokens por respondent_id.
  4. DELETE FROM mydb.survey_respondents por survey_id.
  5. DELETE FROM mydb.survey_attributes por survey_id.
  6. Caches (survey_processing_results, survey_cache) e logs (survey_processing_log).
  7. 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.

Capítulo 06

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)
titulotitle
descricaodescription
empresa_idcompany_id
data_iniciostart_date
data_fimend_date
status_codigostatus
status_textocomputado via getStatusText()
data_criacaocreated_at
data_atualizacaoupdated_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.

Capítulo 07

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.