Skip to content

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

text
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_sessoes

3. Schema (Tabelas)

TabelaPropósitoRLS
resultados_alunoPontuações brutas por fase (inscricao_id, pontuacao, situacao)coordenador (SELECT, INSERT, UPDATE, DELETE)
inscricoes_olimpiadaVínculo aluno↔olimpíada, auto-criado na importação se inexistentecoordenador (SELECT, INSERT)
configuracoes_fase_nivelNota de corte e faixas de premiação (ouro/prata/bronze/menção) por fase+nívelcoordenador (SELECT, INSERT, UPDATE, DELETE)
mural_liberacoesFlags de publicação (liberar_notas, liberar_resultados)coordenador (SELECT, INSERT, UPDATE)
mural_dados_publicadosSnapshots materializados para o muralservice_role (write)
importacao_resultados_sessoesSessõ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)

ActionDescrição
batch_initConsolida olimpíadas + stats + stats_agregados em 1 chamada
listLista olimpíadas com estatísticas de resultados
list_by_olimpiadaResultados por olimpíada/fase (paginado, com filtros)
statsEstatísticas gerais (total, medalhas, classificados)
stats_agregadosEstatísticas por série, turma, ranking de competidores
preview_nota_cortePreview de classificados/não-classificados para uma nota de corte

Mutations (resultados-mutations.ts)

ActionDescriçãoLogging
upsert_batchInserção em lote (matrícula → inscrição, auto-cria se inexistente)resultados.upsert_batch
upsert_singleInserção/edição individualresultados.upsert_single
deleteExclui resultado (mantém inscrição)resultados.delete
delete_with_inscriptionExclui resultado + inscriçãoresultados.delete_with_inscription
delete_fase_resultsLimpa fase inteira (cascata completa)resultados.delete_fase_results

Config (resultados-config.ts)

ActionDescriçãoLogging
set_nota_corteDefine nota de corte → classifica/desclassifica alunosresultados.set_nota_corte
set_premiacaoDefine faixas de premiação automática global (ouro/prata/bronze/menção)resultados.set_premiacao
set_premiacao_batchDefine faixas em lote — modo global OU por_nivel (exclusivo) → ver ADR-014resultado.set_premiacao_batch
get_configs_faseLê faixas + modo detectado (global/por_nivel/vazio) + níveis disponíveis
set_premiacoes_manualDefine premiações manualmente por alunoresultados.set_premiacoes_manual
recomputeRecomputa snapshots do mural para fase/nívelresultados.recompute

Importação Background (resultados-import.ts)

ActionDescrição
start_import_sessionCria sessão + EdgeRuntime.waitUntil() para processamento background
get_import_session_statusPolling de status da sessão (progresso, erros)
cancel_import_sessionCancela 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

KeyFormato
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

ExportTipostaleTime
useResultadosBatchInitQuery5min
useResultadosOlimpiadasQuery5min
useResultadosPorOlimpiadaQuery5min
useResultadosPorOlimpiadaPaginadoQuery5min
useResultadosStatsQuery5min
useResultadosStatsAgregadosQuery5min
useResultadosMutationsMutations

Invalidação

Todas as mutations chamam invalidateAll():

typescript
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çãoMutationBackend Action
inserirResultadosLoteinserirLoteMutupsert_batch
inserirResultadoinserirSingleMutupsert_single
definirNotaCortenotaCorteMutset_nota_corte
definirPremiacaopremiacaoMutset_premiacao (modo global, legado)
definirPremiacaoBatchpremiacaoBatchMutset_premiacao_batch (global ou por_nivel)
definirPremiacoesManualpremiacaoManualMutset_premiacoes_manual
excluirResultadoexcluirMutdelete
excluirResultadoEInscricaoexcluirComInscricaoMutdelete_with_inscription
excluirResultadosFaseexcluirFaseResultadosMutdelete_fase_results

Query adicional: useConfigsFase(faseId) — hidrata o PremiacaoFaixasModal com modo detectado + faixas atuais + níveis disponíveis. Key isolada por escola_id.


7. Hook de Importação: useImportacaoResultados.ts

Gerencia sessões de importação background com polling:

  • startImportSession() → cria sessão e inicia polling
  • pollSessionStatus() → verifica progresso a cada 2s
  • cancelImportSession() → cancela sessão ativa
  • sessionId persistido em localStorage (sobrevive navegação)

8. Fluxo "Limpar Fase" — Efeitos Cascata

A action delete_fase_results executa:

  1. Deleta resultados_aluno da fase (+filtro por nível se especificado)
  2. Deleta inscricoes_olimpiada correspondentes
  3. Deleta configuracoes_fase_nivel (nota de corte + faixas)
  4. Recomputa snapshots (vazios → deletados)
  5. Auto-despublica: seta flags false em mural_liberacoes
  6. Varredura de fantasmas: liberações true sem dados reais → despublica

9. Padrões Aplicados

PadrãoReferê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

ArquivoTipoCobertura
gestao-resultados/index.test.tsContract (Deno)CORS, auth, anti-leak
_shared/__tests__/resultados-config-batch.test.tsUnit (Deno)handleSetPremiacaoBatch: validação, IDOR, diff por hash, exclusividade, idempotência
_shared/__tests__/resultados-compute-por-nivel.test.tsUnit (Deno)computeSituacaoFromConfig + getCfgForNivel (precedência nível>global>null, situação manual preservada na última fase, ranking por nível, empates)
src/hooks/__tests__/useGestaoResultados.premiacao-batch.test.tsRTLdefinirPremiacaoBatch, useConfigsFase: fetch gating, payload, invalidação dupla
src/components/coordenador/resultados/__tests__/premiacao-faixas-modal.test.tsxRTLToggle Global↔Por Nível, tabs por nível (sem UUID), validação local, submit
e2e/resultados-premiacao-por-nivel.spec.tsE2E (Playwright)Fluxo coordenador: modal → modos → faixas por nível → situações distintas na tabela
mural-olimpico/__tests__/fase-content.test.tsxComponentRenderização condicional de resultados

11. Navegação

  • sections-registry.ts: id: 'resultados', permissionKey: 'resultados'
  • Feature flag: resultados (deve estar ativa em feature_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

typescript
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óduloFunçãoScoping
resultados-queries.tsBusca fase anterior para preview✅ Escopado
resultados-mutations.tshandleUpsertBatch, handleUpsertSingle — fase anterior✅ Escopado
resultados-compute.tsehUltimaFase — detecção de última fase✅ Escopado
resultados-import.tsImportação background — última fase✅ Escopado

→ Ref: docs/audits/AUDIT_FASES_POR_NIVEL_2026-04-12.md


14. Premiação por Nível (ADR-014)

Adicionado em: 2026-06-02

Olimpíadas multi-nível (OMU/OBI/…) permitem faixas distintas por nível em uma única operação batch.

Modos exclusivos

ModoEstado em configuracoes_fase_nivel
global1 linha com nivel_id IS NULL
por_nivelN linhas, uma por nível, nivel_id IS NOT NULL
vaziosem linhas para a fase

Não há modo misto. handleSetPremiacaoBatch apaga o lado oposto antes de upsertar.

Decisão de qual modo aplicar (compute)

text
recompute → getCfgForNivel(nivelId do aluno)
  ├── existe cfg do nivel?      → usa nivel
  ├── senão, existe cfg global? → usa global (back-compat)
  └── senão                     → situação = "aguardando"

Fluxo coordenador

  1. Coordenador abre Definir Premiações na fase desejada.
  2. Modal lê via get_configs_fase (modo detectado + faixas atuais + níveis disponíveis).
  3. Toggle Global ↔ Por Nível. Se houver dados do outro modo, modal de confirmação avisa que serão apagados.
  4. Em Por Nível, tabs por nível usam nivel.titulo (nunca UUID). Atalho "aplicar a todos os níveis" copia faixas entre tabs e marca todos como editados (touched) para que o save persista a réplica.
  5. Validação local: negativo, min > max, faixas sobrepostas — bloqueiam submit.
  6. Submit dispara set_premiacao_batch com diff por hash; só linhas alteradas vão para upsert. 1 recompute + 1 snapshot ao final.

Semântica de save incremental (modo por_nivel)

Contrato: o payload de set_premiacao_batch em modo por_nivel contém apenas níveis editados nesta sessão (touched) ou já persistidos no backend ao abrir o modal (existentes). Níveis nunca tocados e ainda sem config NÃO vão no payload.

  • O modal não seeda defaults sintéticos no state. Valores como 90/80/70/60 aparecem apenas como placeholder visual nos <Input /> — não viram write.
  • handleSetPremiacaoBatch faz upsert fiel do payload. Em modo por_nivel, o único cleanup é remover a linha global (nivel_id IS NULL) da fase; níveis ausentes do payload são preservados intactos no DB.
  • Coordenador pode salvar um nível, fechar o modal, reabrir e editar outro — sem risco do segundo save apagar o primeiro.
  • Recompute é reativo: apenas reclassifica conforme as faixas efetivamente persistidas. Nunca inventa faixas.

→ Ver: PROBLEM_SOLVING — Caso 19 para o anti-padrão "synthetic UI default promovido a write real".

Invariantes & segurança

  • IDOR: faseId deve pertencer à escola_id do JWT; cada nivelId deve pertencer à olimpiada_id da fase. Cross-school/cross-olimpíada → 403.
  • Idempotência: zero mudanças → zero recompute → zero log de update.
  • Snapshot do mural só refresh quando há diff real.

→ Ver: ADR-014 — Premiação por Nível


15. Override manual de situação (set_situacao_aluno)

Coordenador pode ajustar classificação ou premiação de um aluno individual em qualquer fase, sobrevivendo a recálculos futuros enquanto a situação continuar coerente com a config.

Casos de uso reais

  • Polo de aplicação — vaga extra concedida pela coordenação geral mesmo abaixo da nota de corte.
  • Desempate manual — duas pontuações iguais na fronteira da faixa; decisão pedagógica de quem sobe.
  • Correção pós-fato — revisão de prova ajusta nota sem alterar a classificação previamente comunicada.

Action set_situacao_aluno

ts
invokeAction('gestao-resultados', 'set_situacao_aluno', {
  inscricaoId: uuid,
  faseId: uuid,
  situacao?: 'medalha_ouro' | 'medalha_prata' | 'medalha_bronze'
           | 'mencao_honrosa' | 'sem_premiacao'
           | 'classificado' | 'nao_classificado',
  pontuacao?: number | null,   // null = limpar; undefined = não tocar
  origem: 'manual' | 'auto',   // 'auto' reseta o override
});

Regras:

  • origem='manual'situacao obrigatória e validada contra configuracoes_fase_nivel (faixas vs nota_corte). Persiste situacao_origem='manual'.
  • origem='auto'situacao ignorada; backend recalcula via computeSituacaoFromConfig(pontuacao, cfg). Persiste situacao_origem='auto'.
  • Pontuação acima do máximo da fase → 400.
  • Inscrição de outra escola → 403 (ownership).

Preservação no recompute

recomputeFaseForOlimpiada (chamado por toda mutation em lote) preserva situacao_origem='manual' somente enquanto isSituacaoValidaParaConfig(situacao, cfg) for verdadeiro. Se a config mudou de forma que invalide o override (ex.: faixas removidas, medalha_ouro em fase só-nota-corte), o sistema rebaixa silenciosamente para auto — fail-safe.

Observabilidade

Toda mutation em lote retorna manualPreservados: number (contagem total na fase). O frontend exibe aviso olpToast.info quando > 0 após upsert_batch, orientando o coordenador a revisar.

UI

  • Botão "Editar" (ícone Pencil) por linha em DetalhesTabela abre EditarResultadoAlunoDialog.
  • Badge "Manual" + tooltip quando situacaoOrigem === 'manual'.
  • Switch "Voltar para cálculo automático" envia origem='auto' sem situacao (não vaza estado obsoleto).

→ Ver: ADR-015 — Manual Override de Situação


16. Referências

  • docs/features/MURAL.md — Liberação de resultados e snapshots
  • docs/development/IMPORT_SYSTEM_STANDARD.md — Padrão de importação
  • docs/security/AUTHENTICATION.md — Auth e roles
  • docs/audits/RESULTADOS_REFACTOR_AUDIT.md — Auditoria de refactor
  • docs/audits/AUDIT_FASES_POR_NIVEL_2026-04-12.md — Auditoria fases por nível
  • docs/architecture/adrs/ADR-014-premiacao-por-nivel.md — Exclusividade global vs por-nível
  • docs/architecture/adrs/ADR-015-manual-override-situacao-aluno.md — Override manual (situacao_origem)