Skip to content

Seção Controle — Coordenador

Última atualização: 2026-04-12
Migrado de: docs/architecture/CONTROLE_SECTION.md

Painel centralizado de gestão da temporada olímpica. Acessível ao papel coordenador via permissão controle e feature flag controle.

Arquitetura

text
Frontend                           Backend                           Banco
────────                           ───────                           ─────
Container (ControleSkeleton)       coordenador-controle              controle_acoes_chave
├── TabAcoesChave ─┐               ├── batch_init (3-in-1)           controle_aplicacoes
├── TabAplicacoes ─┤── useControle ├── list_acoes_chave              tarefas (read-only)
└── TabTarefasKanban─┘             ├── update_acao_chave
                                   ├── list_aplicacoes
                                   ├── update_aplicacao
                                   ├── batch_update_aplicacoes
                                   ├── create_aplicacoes
                                   ├── create_aplicacao_single
                                   ├── delete_aplicacao
                                   ├── list_tarefas_kanban
                                   └── count_coordenadores

Tabelas

TabelaPropósitoRLS
controle_acoes_chaveCheckboxes de progresso por olimpíadacoordenador (SELECT, INSERT, UPDATE)
controle_aplicacoesDados da ata de alinhamento por fasecoordenador (SELECT, INSERT, UPDATE, DELETE)

Ambas com trigger update_updated_at_column. A constraint de unicidade por segmento foi removida para permitir split (múltiplos registros no mesmo segmento).

Edge Function: coordenador-controle

ActionTipoDescrição
batch_initReadConsolida 3 queries (ações + aplicações + tarefas) em 1 chamada; popula caches individuais
list_acoes_chaveReadOlimpíadas ativas + checkboxes + prazo inscrição
update_acao_chaveWriteToggle checkbox (Zod + registrarLog) + retorna tracking enriquecido (_por_nome)
list_aplicacoesReadRegistros flat de aplicação + olimpíadas aderidas (para modal)
update_aplicacaoWriteUpdate campo inline por ID (Zod: {id, campo, valor} + registrarLog)
batch_update_aplicacoesWriteSalva múltiplas mudanças por ID em lote (dirty-state)
create_aplicacoesWriteCria registros por olimpíada × fase × segmento cabível
create_aplicacao_singleWriteCria 1 registro com segmento definido e publico: null (registro limpo para auto-split)
delete_aplicacaoWriteRemove registro por ID; verifica linhas afetadas (retorna 403 se zero)
list_tarefas_kanbanReadTarefas agrupadas por olimpíada
count_coordenadoresReadCount coordenadores ativos na escola

Validação: Zod schemas (UpdateAcaoChaveSchema, UpdateAplicacaoSchema, etc).
Logging: controle.toggle_acao_chave, controle.update_aplicacao, controle.delete_aplicacao, controle.create_aplicacao_single (fire-and-forget).

Hardening do Delete

delete_aplicacao usa .select("id").maybeSingle() após o delete para verificar que pelo menos uma linha foi deletada. Se zero linhas foram afetadas, retorna 403 Forbidden em vez de success: true silencioso.

Hook: useControle.ts

Query Keys (com escola_id para isolamento cross-escola)

KeyFormato
all['controle', escolaId]
acoesChave['controle', 'acoes-chave', escolaId]
aplicacoes['controle', 'aplicacoes', escolaId]
tarefasKanban['controle', 'tarefas-kanban', escolaId]

Exports

ExportTipoCache KeystaleTime
useControleInitQueryall10min
useAcoesChaveQueryacoesChave5min
useAplicacoesQueryaplicacoes5min
useTarefasKanbanQuerytarefasKanban5min
useToggleAcaoChaveMutationOptimistic update + patch local (sem invalidação global)
useUpdateAplicacaoMutationOptimistic update por {id, campo, valor} + invalida aplicacoes
useDeleteAplicacaoMutationOptimistic delete do cache + rollback + invalida aplicacoes
useCreateAplicacaoSingleMutationInvalida APENAS aplicacoes
useBatchSaveAplicacoesMutationInvalida APENAS aplicacoes

Erros sanitizados via getUserFriendlyError.

Invalidação Granular

As mutations operam com invalidação cirúrgica para evitar refetches desnecessários:

  • useToggleAcaoChave: Optimistic update imediato + patch com response do backend. Nenhuma invalidação de query — o cache é atualizado localmente.
  • useUpdateAplicacao: Optimistic patch no cache + invalidação de aplicacoes no onSettled.
  • useDeleteAplicacao: Optimistic delete — remove do cache imediatamente, rollback em caso de erro.
  • useBatchSaveAplicacoes: Invalidam apenas controleKeys.aplicacoes(escolaId).

Segmentos Pedagógicos

4 segmentos fixos organizam os registros de aplicação:

KeyLabelSéries
4_5_ef4º e 5º Ano EF4_ano_ef, 5_ano_ef
6_7_ef6º e 7º Ano EF6_ano_ef, 7_ano_ef
8_9_ef8º e 9º Ano EF8_ano_ef, 9_ano_ef
emEnsino Médio1_ano_em, 2_ano_em, 3_ano_em

Registros legados com segmento = 1_em, 2_em ou 3_em são resolvidos via resolveOriginSegment() (fallback pelo campo publico).

Público Inteligente (Auto-split com Cobertura Real)

Conceitos

  • Registro original: Registro no segmento de origem. Editável.
  • Registro espelhado (mirror): Mesmo registro renderizado em outro segmento. Read-only com badge "Origem: X" e botão "Ir para origem".
  • Registro limpo: Registro com publico: null, criado automaticamente pelo auto-split para séries ainda não cobertas.
  • Auto-split: Quando o coordenador seleciona apenas algumas séries de um segmento, o sistema verifica a cobertura real de todas as séries e cria um registro limpo para as restantes — recursivamente até cobertura total.

Verificação de Cobertura Real

O sistema coleta as séries cobertas por todos os registros do mesmo segmento+olimpíada+fase (excluindo o registro atual). Registros com publico: null contam como "sem cobertura declarada" (não cobrem nenhuma série).

Exemplo com Ensino Médio (3 séries: 1º, 2º, 3º):

  1. Registro A seleciona "2º EM" → nenhum outro registro → trulyUncovered = [1º, 3º] → cria Registro B (limpo)
  2. Registro B seleciona "3º EM" → Registro A cobre "2º EM" → trulyUncovered = [1º] → cria Registro C (limpo)
  3. Registro C seleciona "1º EM" → A cobre "2º", B cobre "3º" → trulyUncovered = [] → não cria mais nada

O dropdown de público exibe apenas as séries pertencentes ao segmento atual (interseção de segDef.series com olimpiadas_aderidas[].series_participantes). Isso impede seleção cross-segment.

Resolução de Origem

Registros com segmento="" (legados) têm a origem inferida a partir do campo publico, seguindo a ordem pedagógica dos segmentos (resolveOriginSegment()).

Fluxo Atômico

O botão "Confirmar" no dropdown de Público executa:

  1. Persiste publico imediatamente via update_aplicacao (não espera "Salvar")
  2. Verifica cobertura real: coleta séries de todos os outros registros no mesmo segmento+olimpíada+fase. Se há séries truly uncovered → cria registro limpo (publico: null) via create_aplicacao_single
  3. Toast de feedback: "Público atualizado" ou "Registro criado para séries restantes"
  4. Spinner no botão durante a operação

Ordenação dos Registros

Registros dentro de cada segmento são ordenados por:

  1. Sigla da olimpíada (alfabética, localeCompare pt-BR)
  2. Ordem da fase (ordem_fase, numérica crescente)

Nativos são listados primeiro, espelhados depois — ambos seguindo a mesma ordenação. Registros criados via auto-split entram automaticamente na posição correta.

text
src/components/controle/
├── index.tsx                  Container com Tabs (Lazy Mount Once)
├── tab-acoes-chave.tsx        Tabela checkboxes + prazo
├── tab-aplicacoes.tsx         Tabela editável inline (dirty-state + batch save + auto-split)
├── tab-tarefas-kanban.tsx     Kanban horizontal
└── helpers.ts                 Tipos + getPrazoStatus + constantes

Lazy Mount Once

As tabs usam o padrão "mount once, hide with CSS":

tsx
{visitedTabs.has("acoes") && (
  <div className={cn(activeTab !== "acoes" && "hidden")}>
    <TabAcoesChave />
  </div>
)}

Cada tab é montada na primeira visita e nunca desmontada. Ao trocar de tab, a anterior é escondida via display: none (classe hidden), preservando:

  • Estado local (inputs, scroll position)
  • Cache React Query já hidratado
  • Zero re-render ao retornar

Anti-Flash Pattern (Renderização Atômica)

As tabs usam isLoading && !data em vez de apenas isLoading para evitar flash de skeleton quando o cache já foi populado pelo batch_init:

tsx
const { data, isLoading, error } = useAcoesChave();
if (isLoading && !data) return <AcoesChaveSkeleton />;

→ Ref: docs/development/ATOMIC_RENDERING.md

Testes

ArquivoTipoCobertura
coordenador-controle/index.test.tsContract (Deno)CORS, auth, anti-leak
useControle.integration.test.tsHook integrationQueries, optimistic, rollback, invalidação, isolamento cross-tab
controle/__tests__/helpers.test.tsUnitgetPrazoStatus

Testes de Isolamento

3 testes negativos garantem que mutations de uma tab não invalidam caches de outras:

  • useToggleAcaoChave NÃO invalida aplicacoes nem tarefas-kanban
  • useBatchSaveAplicacoes invalida APENAS aplicacoes
  • useUpdateAplicacao invalida APENAS aplicacoes

Fases por Nível (modo_config_fases: 'por_nivel')

Adicionado em: 2026-04-12 — Migration nivel_id em fases_olimpiada

Conceito

Olimpíadas com modo_config_fases = 'por_nivel' possuem fases específicas por nível de competição. A tabela fases_olimpiada é a fonte de verdade consolidada, com a coluna nivel_id (nullable) indicando a qual nível cada fase pertence.

nivel_idSignificado
NULLFase uniforme (compartilhada entre todos os níveis)
<uuid>Fase específica daquele nível

Tabelas Envolvidas

text
fases_olimpiada (nivel_id nullable)

    ├── FK → niveis_competicao (id, titulo)
    │         └── niveis_competicao_series (series participantes)

    └── FK ← controle_aplicacoes (fase_id)
              configuracoes_fase_nivel (fase_id + nivel_id)
              resultados_aluno (fase_id)
              atividades_cronograma (fase_id)
              mural_liberacoes (fase_id)
              mural_dados_publicados (fase_id)
              importacao_resultados_sessoes (fase_id)

Lógica de create_aplicacoes por Nível

Para olimpíadas por_nivel, o backend itera por nível:

text
Para cada nível da olimpíada:
  fases_do_nivel = fases_olimpiada WHERE nivel_id = nivel.id
  series_do_nivel = niveis_competicao_series WHERE nivel_id = nivel.id
  segmentos = getSegmentosCabiveis(series_do_nivel)
  criar registros: fases_do_nivel × segmentos

Para olimpíadas por_nivel, o modal exibe fases agrupadas por nível com sub-grupos visuais. O backend resolve automaticamente os segmentos corretos por nível.

Campo Legado JSONB

O campo olimpiadas.fases_por_nivel (JSONB) está depreciado. Ainda é lido por 3 Edge Functions legadas (eventos-calendario, eventos-calendario-trial, coordenador-olimpiadas) — ver dívida técnica em docs/audits/AUDIT_FASES_POR_NIVEL_2026-04-12.md.

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


  • sections-registry.ts: id: 'controle', icon: ClipboardCheck, permissionKey: 'controle'
  • permissions.ts: controle no COORDENADOR_PERMISSIONS
  • Feature flag: controle (deve estar ativa em feature_flags)