Skip to content

Inscrições — Visão geral (SSOT)

Single Source of Truth da feature de inscrições em olimpíadas. Atualizado: Fase 2 do plano de hardening (2026-04-18).

Visão geral

A tela Inscrições (/inscricoes, papel coordenador/escola/diretor) permite:

  1. Listar alunos elegíveis e inscrevê-los em olimpíadas em lote.
  2. Visualizar quantitativos por série/turma.
  3. Consultar dados institucionais da escola.
  4. Consultar histórico de inscrições e atualizar/cancelar status.

Arquitetura

Frontend

  • Entrada: src/components/inscricoes/index.tsx (composição/JSX)
  • Estado/handlers: src/components/inscricoes/use-inscricoes-page-state.ts
  • Hooks de dados:
    • useInscricoesBatchInit() — consolida ano_letivo + escola + olimpíadas + count em 1 round-trip
    • useInscricoesOlimpiada(olimpiadaId?) — list/mutations CRUD
    • useSeriesEscolares() — séries (cache 30min)
    • useEscolaDados(), useAnoLetivo() — dados auxiliares
  • Skeletons: src/components/inscricoes/skeletons.tsx

Backend

  • Edge Function: supabase/functions/inscricoes-olimpiada/index.ts
  • Schemas Zod: supabase/functions/inscricoes-olimpiada/schemas.ts
  • Helper compartilhado: supabase/functions/_shared/ano-letivo.ts

Actions e roles

ActionEscopoRoles permitidos
listREADcoordenador, escola, diretor
historicoREADcoordenador, escola, diretor
statsREADcoordenador, escola, diretor
batch_initREADcoordenador, escola, diretor
create_batchWRITEcoordenador
update_statusWRITEcoordenador
cancelWRITEcoordenador

WRITE não autorizado → 403 com mensagem: "Apenas coordenadores podem criar, atualizar ou cancelar inscrições."

Status semantics (decisão Fase 2 §6.4)

Ciclo real: pendente (legado, default DB) → confirmadacancelada.

  • 'inscrita' foi removido do enum frontend, do STATUS_CONFIG e do dropdown de histórico.
  • O backend não emite mais inscrições como 'pendente': novos INSERTs usam 'confirmada' (cliente já valida elegibilidade).
  • Registros legados com 'pendente' continuam sendo lidos e podem ser convertidos via update_status.

SSOT do ano_edicao

Toda operação de WRITE (create_batch) e de leitura agregada (stats) usa escolas.ano_letivo_atual como SSOT — não new Date().getFullYear().

ts
// supabase/functions/_shared/ano-letivo.ts
export async function getAnoLetivoEscola(supabase, escolaId): Promise<number>
  • Cache por request (WeakMap por client).
  • Fallback: new Date().getFullYear() apenas se escola.ano_letivo_atual ausente.

batch_init — consolidação

Substitui 4 requests independentes (escola-dados/get, escola-dados/get_ano_letivo, coordenador-olimpiadas/list_para_inscricao, contagem de alunos) por 1 chamada:

json
{
  "ano_letivo": { "atual": 2025, "sistema": 2026, "precisa_atualizar": true },
  "escola": { "id", "nome", "codigo_inep", "tipo", "endereco": {...} },
  "olimpiadas": [ { "id", "olimpiada": { "niveis": [...] } } ],
  "alunos_ativos_count": 312
}

Padrão alinhado com mural-escola/batch_init. Referência: mem://architecture/batch-init-request-consolidation.

Validação Zod

Todo payload é validado em schemas.ts antes do dispatch:

  • UUIDs com z.string().uuid()
  • Status com z.enum(['pendente', 'confirmada', 'cancelada'])
  • alunosIds: array de 1 a 2000 UUIDs
  • Erro → 400 { success: false, message, fieldErrors }

Erros mapeados

Código PostgresCenárioResposta
23505UNIQUE violation em inscricoes_olimpiada (mesmo aluno, mesma olimpíada, mesmo ano)409 + "Um ou mais alunos já possuem inscrição (mesmo cancelada). Reative ou ajuste o status existente."

Cache strategy (queryKeys)

Todas as keys incluem escola_id para isolamento cross-tenant (ref: mem://architecture/query-key-context-isolation):

  • ['inscricoes-batch-init', escolaId] — staleTime 5min
  • ['inscricoes-olimpiada', escolaId, ...] — list/historico/stats
  • ['inscricoes-existentes', escolaId, olimpiadaId] — gating por olimpíada
  • ['ano-letivo', escolaId] — staleTime 10min
  • ['gestao-alunos', 'series'] — staleTime 30min

Status code semantics

  • 401 — Token ausente, inválido, expirado, revogado ou de portal (AuthError.status=401)
  • 403 — Token válido mas papel insuficiente (AuthError.status=403)
  • 400 — Validação Zod falhou ou action desconhecida
  • 404 — Inscrição inexistente
  • 409 — UNIQUE violation
  • 500 — Erro interno

Sem heurística por substring de mensagem. Ref: mem://architecture/auth-and-idor-status-code-semantics.

Performance — listagem de alunos

Carregamento atual (intencional)

A query gestao-alunos/list da aba "Lista de Alunos" usa limit: 99999, retornando todos os alunos ativos da escola em uma única request. Isso é intencional porque o frontend depende do dataset completo para:

  1. Geração de XLSX (handleGerarArquivos) — monta planilhas por nível/segmento.
  2. Contagens agregadas (contarAlunosPorNivel, contarAlunosPorSegmento, contagemPorSerieTurma) exibidas na aba "Quantidade de Alunos".
  3. "Selecionar todos elegíveis" e badges de "X elegíveis" — precisam de visão global.
  4. Filtro local por série — UX instantânea, sem round-trip.

Para datasets típicos (até ~1.500 alunos por escola) a request é leve (<300ms) e o DOM permanece pequeno graças à paginação visual de 10 itens.

Otimizações aplicadas

  • Debounce 250ms no filtro de busca (useDebouncedValue em src/hooks/useDebouncedValue.ts): o input continua controlado/responsivo, mas o filter+sort só recomputa após pausa de digitação. Reset de paginação atrelado ao termo debounced.
  • Memoização de filteredAlunos, sortedAlunos, alunosPaginados, seriesUnicas e contagemPorSerieTurma.
  • Paginação visual de 10 itens/página mantém o DOM enxuto.

Roadmap futuro (escolas >3.000 alunos)

Migrar para paginação real server-side exige novos endpoints leves (fora do escopo atual):

  • gestao-alunos/contagem_por_serieGROUP BY serie_id para a aba "Quantidade".
  • gestao-alunos/list_ids_por_olimpiada — IDs elegíveis para "Selecionar todos".
  • XLSX passa a buscar dataset completo on-demand ao clicar em "Gerar arquivos".

Sem esses endpoints, paginar a list quebraria contagens/seleção/XLSX. Ver docs/audits/INSCRICOES_AUDIT.md para o gap formal.