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:
- Listar alunos elegíveis e inscrevê-los em olimpíadas em lote.
- Visualizar quantitativos por série/turma.
- Consultar dados institucionais da escola.
- 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-tripuseInscricoesOlimpiada(olimpiadaId?)— list/mutations CRUDuseSeriesEscolares()— 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
| Action | Escopo | Roles permitidos |
|---|---|---|
list | READ | coordenador, escola, diretor |
historico | READ | coordenador, escola, diretor |
stats | READ | coordenador, escola, diretor |
batch_init | READ | coordenador, escola, diretor |
create_batch | WRITE | coordenador |
update_status | WRITE | coordenador |
cancel | WRITE | coordenador |
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) → confirmada ↔ cancelada.
'inscrita'foi removido do enum frontend, doSTATUS_CONFIGe do dropdown de histórico.- O backend não emite mais inscrições como
'pendente': novosINSERTs usam'confirmada'(cliente já valida elegibilidade). - Registros legados com
'pendente'continuam sendo lidos e podem ser convertidos viaupdate_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().
// 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 seescola.ano_letivo_atualausente.
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:
{
"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 Postgres | Cenário | Resposta |
|---|---|---|
23505 | UNIQUE 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:
- Geração de XLSX (
handleGerarArquivos) — monta planilhas por nível/segmento. - Contagens agregadas (
contarAlunosPorNivel,contarAlunosPorSegmento,contagemPorSerieTurma) exibidas na aba "Quantidade de Alunos". - "Selecionar todos elegíveis" e badges de "X elegíveis" — precisam de visão global.
- 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 (
useDebouncedValueemsrc/hooks/useDebouncedValue.ts): o input continua controlado/responsivo, mas ofilter+sortsó recomputa após pausa de digitação. Reset de paginação atrelado ao termo debounced. - Memoização de
filteredAlunos,sortedAlunos,alunosPaginados,seriesUnicasecontagemPorSerieTurma. - 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_serie—GROUP BY serie_idpara 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.