Gestão de Resultados — Coordenador
Última atualização: 2026-04-12
1. Visão Geral
Sistema de inserção, gestão e premiação de resultados de olimpíadas. Permite inserção manual e importação em lote (background), definição de nota de corte, faixas de premiação automática/manual, e sincronização com o Mural Olímpico via snapshots materializados. Acessível a coordenador, escola, administrador e especialista.
2. Arquitetura
Frontend Backend Banco
──────── ─────── ─────
InserirDialog (wizard) ─┐ gestao-resultados (orquestrador) resultados_aluno
ResultadosTabela ───────┤ ├── _shared/resultados-queries.ts inscricoes_olimpiada
NotaCorteDialog ────────┤── useGestao ├── _shared/resultados-mutations.ts configuracoes_fase_nivel
PremiacaoDialog ────────┤ Resultados ├── _shared/resultados-config.ts mural_liberacoes
ImportacaoResultados ───┘ ├── _shared/resultados-import.ts mural_dados_publicados
└── _shared/snapshot-publicados.ts importacao_resultados_sessoes3. Schema (Tabelas)
| Tabela | Propósito | RLS |
|---|---|---|
resultados_aluno | Pontuações brutas por fase (inscricao_id, pontuacao, situacao) | coordenador (SELECT, INSERT, UPDATE, DELETE) |
inscricoes_olimpiada | Vínculo aluno↔olimpíada, auto-criado na importação se inexistente | coordenador (SELECT, INSERT) |
configuracoes_fase_nivel | Nota de corte e faixas de premiação (ouro/prata/bronze/menção) por fase+nível | coordenador (SELECT, INSERT, UPDATE, DELETE) |
mural_liberacoes | Flags de publicação (liberar_notas, liberar_resultados) | coordenador (SELECT, INSERT, UPDATE) |
mural_dados_publicados | Snapshots materializados para o mural | service_role (write) |
importacao_resultados_sessoes | Sessões de importação background (status, progresso) | service_role (write), coordenador (SELECT) |
4. Edge Function: gestao-resultados
Orquestrador modular — roteia para handlers em _shared/resultados-*.ts.
Auth: requireRole(["coordenador", "escola", "administrador", "especialista"])
Feature Flag: requireFeatureFlag("resultados")
CORS: getCorsHeaders(req) em todas as respostas incluindo catch.
Queries (resultados-queries.ts)
| Action | Descrição |
|---|---|
batch_init | Consolida olimpíadas + stats + stats_agregados em 1 chamada |
list | Lista olimpíadas com estatísticas de resultados |
list_by_olimpiada | Resultados por olimpíada/fase (paginado, com filtros) |
stats | Estatísticas gerais (total, medalhas, classificados) |
stats_agregados | Estatísticas por série, turma, ranking de competidores |
preview_nota_corte | Preview de classificados/não-classificados para uma nota de corte |
Mutations (resultados-mutations.ts)
| Action | Descrição | Logging |
|---|---|---|
upsert_batch | Inserção em lote (matrícula → inscrição, auto-cria se inexistente) | resultados.upsert_batch |
upsert_single | Inserção/edição individual | resultados.upsert_single |
delete | Exclui resultado (mantém inscrição) | resultados.delete |
delete_with_inscription | Exclui resultado + inscrição | resultados.delete_with_inscription |
delete_fase_results | Limpa fase inteira (cascata completa) | resultados.delete_fase_results |
Config (resultados-config.ts)
| Action | Descrição | Logging |
|---|---|---|
set_nota_corte | Define nota de corte → classifica/desclassifica alunos | resultados.set_nota_corte |
set_premiacao | Define faixas de premiação automática (ouro/prata/bronze/menção) | resultados.set_premiacao |
set_premiacoes_manual | Define premiações manualmente por aluno | resultados.set_premiacoes_manual |
recompute | Recomputa snapshots do mural para fase/nível | resultados.recompute |
Importação Background (resultados-import.ts)
| Action | Descrição |
|---|---|
start_import_session | Cria sessão + EdgeRuntime.waitUntil() para processamento background |
get_import_session_status | Polling de status da sessão (progresso, erros) |
cancel_import_session | Cancela sessão pendente/processando |
Processamento: batches de 50 registros, recomputeSnapshotsIfPublished() ao final.
5. Validação Zero-Trust (Backend)
- Pontuação máxima por nível: Validada no backend via
fases_olimpiada.pontuacao_maxima_por_nivel. Excedentes rejeitados. - Matrícula: Lookup por escola_id + matrícula. Inexistente → erro.
- Série/nível: Elegibilidade verificada — alunos de série incompatível rejeitados como "inaptos" (retornados em
matriculasInaptas). - Inscricão auto-criada: Se aluno existe mas não tem inscrição, a inscrição é criada automaticamente durante
upsert_batch.
6. Hook: useGestaoResultados.ts
Query Keys
| Key | Formato |
|---|---|
all | ['gestao-resultados'] |
olimpiadas | ['gestao-resultados', 'olimpiadas'] |
porOlimpiada | ['gestao-resultados', 'por-olimpiada', olimpiadaId, faseId] |
porOlimpiadaPaginado | ['gestao-resultados', 'por-olimpiada-pag', ...filtros] |
stats | ['gestao-resultados', 'stats', anoEdicao] |
statsAgregados | ['gestao-resultados', 'stats-agregados', anoEdicao] |
batchInit | ['gestao-resultados', 'batch-init', anoEdicao] |
Exports
| Export | Tipo | staleTime |
|---|---|---|
useResultadosBatchInit | Query | 5min |
useResultadosOlimpiadas | Query | 5min |
useResultadosPorOlimpiada | Query | 5min |
useResultadosPorOlimpiadaPaginado | Query | 5min |
useResultadosStats | Query | 5min |
useResultadosStatsAgregados | Query | 5min |
useResultadosMutations | Mutations | — |
Invalidação
Todas as mutations chamam invalidateAll():
await Promise.all([
queryClient.invalidateQueries({ queryKey: resultadosKeys.all }),
queryClient.invalidateQueries({ queryKey: ['mural-liberacao-stats'], refetchType: 'all' }),
queryClient.invalidateQueries({ queryKey: ['mural-liberacoes'], refetchType: 'all' }),
]);Cross-feature: invalida mural-liberacao-stats e mural-liberacoes para sincronia com o Mural.refetchType: 'all' garante refetch mesmo de queries inativas.
Mutations Expostas (boolean wrappers)
| Função | Mutation | Backend Action |
|---|---|---|
inserirResultadosLote | inserirLoteMut | upsert_batch |
inserirResultado | inserirSingleMut | upsert_single |
definirNotaCorte | notaCorteMut | set_nota_corte |
definirPremiacao | premiacaoMut | set_premiacao |
definirPremiacoesManual | premiacaoManualMut | set_premiacoes_manual |
excluirResultado | excluirMut | delete |
excluirResultadoEInscricao | excluirComInscricaoMut | delete_with_inscription |
excluirResultadosFase | excluirFaseResultadosMut | delete_fase_results |
7. Hook de Importação: useImportacaoResultados.ts
Gerencia sessões de importação background com polling:
startImportSession()→ cria sessão e inicia pollingpollSessionStatus()→ verifica progresso a cada 2scancelImportSession()→ cancela sessão ativasessionIdpersistido emlocalStorage(sobrevive navegação)
8. Fluxo "Limpar Fase" — Efeitos Cascata
A action delete_fase_results executa:
- Deleta
resultados_alunoda fase (+filtro por nível se especificado) - Deleta
inscricoes_olimpiadacorrespondentes - Deleta
configuracoes_fase_nivel(nota de corte + faixas) - Recomputa snapshots (vazios → deletados)
- Auto-despublica: seta flags
falseemmural_liberacoes - Varredura de fantasmas: liberações
truesem dados reais → despublica
9. Padrões Aplicados
| Padrão | Referência |
|---|---|
| Importação background (>50 registros) | → Ref: docs/development/IMPORT_SYSTEM_STANDARD.md |
| Shared Data Layer (chunks + derivação em memória) | → Ref: _shared/resultados-queries.ts |
| Snapshots materializados | → Ref: _shared/snapshot-publicados.ts |
| Cross-feature cache invalidation | → Ref: docs/development/REACT_QUERY_CACHE.md |
| Zero-trust validation | → Ref: docs/development/CODING_STANDARDS.md |
| Renderização Atômica | → Ref: docs/development/ATOMIC_RENDERING.md |
| Error handling (getUserFriendlyError) | → Ref: docs/development/CODING_STANDARDS.md |
10. Testes
| Arquivo | Tipo | Cobertura |
|---|---|---|
gestao-resultados/index.test.ts | Contract (Deno) | CORS, auth, anti-leak |
mural-olimpico/__tests__/fase-content.test.tsx | Component | Renderização condicional de resultados |
11. Navegação
- sections-registry.ts:
id: 'resultados',permissionKey: 'resultados' - Feature flag:
resultados(deve estar ativa emfeature_flags)
12. Scoping por nivel_id (Fases por Nível)
Adicionado em: 2026-04-12
Para olimpíadas com modo_config_fases = 'por_nivel', todas as queries que buscam "fase anterior" ou "última fase" devem escopar por nivel_id. Sem esse scoping, queries com .single() falham com PGRST116 (múltiplas linhas) porque níveis diferentes compartilham a mesma ordem.
Padrão Obrigatório
let q = supabase.from("fases_olimpiada")
.select("*")
.eq("olimpiada_id", olimpiadaId)
.eq("ordem", targetOrdem);
if (faseAtual.nivel_id) {
q = q.eq("nivel_id", faseAtual.nivel_id);
} else {
q = q.is("nivel_id", null);
}
const { data } = await q.maybeSingle(); // NUNCA .single()Módulos Afetados
| Módulo | Função | Scoping |
|---|---|---|
resultados-queries.ts | Busca fase anterior para preview | ✅ Escopado |
resultados-mutations.ts | handleUpsertBatch, handleUpsertSingle — fase anterior | ✅ Escopado |
resultados-compute.ts | ehUltimaFase — detecção de última fase | ✅ Escopado |
resultados-import.ts | Importação background — última fase | ✅ Escopado |
→ Ref: docs/audits/AUDIT_FASES_POR_NIVEL_2026-04-12.md
13. Referências
docs/features/MURAL.md— Liberação de resultados e snapshotsdocs/development/IMPORT_SYSTEM_STANDARD.md— Padrão de importaçãodocs/security/AUTHENTICATION.md— Auth e rolesdocs/audits/RESULTADOS_REFACTOR_AUDIT.md— Auditoria de refactordocs/audits/AUDIT_FASES_POR_NIVEL_2026-04-12.md— Auditoria fases por nível