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
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_coordenadoresTabelas
| Tabela | Propósito | RLS |
|---|---|---|
controle_acoes_chave | Checkboxes de progresso por olimpíada | coordenador (SELECT, INSERT, UPDATE) |
controle_aplicacoes | Dados da ata de alinhamento por fase | coordenador (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
| Action | Tipo | Descrição |
|---|---|---|
batch_init | Read | Consolida 3 queries (ações + aplicações + tarefas) em 1 chamada; popula caches individuais |
list_acoes_chave | Read | Olimpíadas ativas + checkboxes + prazo inscrição |
update_acao_chave | Write | Toggle checkbox (Zod + registrarLog) + retorna tracking enriquecido (_por_nome) |
list_aplicacoes | Read | Registros flat de aplicação + olimpíadas aderidas (para modal) |
update_aplicacao | Write | Update campo inline por ID (Zod: {id, campo, valor} + registrarLog) |
batch_update_aplicacoes | Write | Salva múltiplas mudanças por ID em lote (dirty-state) |
create_aplicacoes | Write | Cria registros por olimpíada × fase × segmento cabível |
create_aplicacao_single | Write | Cria 1 registro com segmento definido e publico: null (registro limpo para auto-split) |
delete_aplicacao | Write | Remove registro por ID; verifica linhas afetadas (retorna 403 se zero) |
list_tarefas_kanban | Read | Tarefas agrupadas por olimpíada |
count_coordenadores | Read | Count 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)
| Key | Formato |
|---|---|
all | ['controle', escolaId] |
acoesChave | ['controle', 'acoes-chave', escolaId] |
aplicacoes | ['controle', 'aplicacoes', escolaId] |
tarefasKanban | ['controle', 'tarefas-kanban', escolaId] |
Exports
| Export | Tipo | Cache Key | staleTime |
|---|---|---|---|
useControleInit | Query | all | 10min |
useAcoesChave | Query | acoesChave | 5min |
useAplicacoes | Query | aplicacoes | 5min |
useTarefasKanban | Query | tarefasKanban | 5min |
useToggleAcaoChave | Mutation | Optimistic update + patch local (sem invalidação global) | — |
useUpdateAplicacao | Mutation | Optimistic update por {id, campo, valor} + invalida aplicacoes | — |
useDeleteAplicacao | Mutation | Optimistic delete do cache + rollback + invalida aplicacoes | — |
useCreateAplicacaoSingle | Mutation | Invalida APENAS aplicacoes | — |
useBatchSaveAplicacoes | Mutation | Invalida 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 deaplicacoesnoonSettled.useDeleteAplicacao: Optimistic delete — remove do cache imediatamente, rollback em caso de erro.useBatchSaveAplicacoes: Invalidam apenascontroleKeys.aplicacoes(escolaId).
Segmentos Pedagógicos
4 segmentos fixos organizam os registros de aplicação:
| Key | Label | Séries |
|---|---|---|
4_5_ef | 4º e 5º Ano EF | 4_ano_ef, 5_ano_ef |
6_7_ef | 6º e 7º Ano EF | 6_ano_ef, 7_ano_ef |
8_9_ef | 8º e 9º Ano EF | 8_ano_ef, 9_ano_ef |
em | Ensino Médio | 1_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º):
- Registro A seleciona "2º EM" → nenhum outro registro →
trulyUncovered = [1º, 3º]→ cria Registro B (limpo) - Registro B seleciona "3º EM" → Registro A cobre "2º EM" →
trulyUncovered = [1º]→ cria Registro C (limpo) - Registro C seleciona "1º EM" → A cobre "2º", B cobre "3º" →
trulyUncovered = []→ não cria mais nada
Dropdown de Séries (filtrado por segmento)
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:
- Persiste
publicoimediatamente viaupdate_aplicacao(não espera "Salvar") - 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) viacreate_aplicacao_single - Toast de feedback: "Público atualizado" ou "Registro criado para séries restantes"
- Spinner no botão durante a operação
Ordenação dos Registros
Registros dentro de cada segmento são ordenados por:
- Sigla da olimpíada (alfabética,
localeCompare pt-BR) - 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.
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 + constantesLazy Mount Once
As tabs usam o padrão "mount once, hide with CSS":
{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:
const { data, isLoading, error } = useAcoesChave();
if (isLoading && !data) return <AcoesChaveSkeleton />;→ Ref: docs/development/ATOMIC_RENDERING.md
Testes
| Arquivo | Tipo | Cobertura |
|---|---|---|
coordenador-controle/index.test.ts | Contract (Deno) | CORS, auth, anti-leak |
useControle.integration.test.ts | Hook integration | Queries, optimistic, rollback, invalidação, isolamento cross-tab |
controle/__tests__/helpers.test.ts | Unit | getPrazoStatus |
Testes de Isolamento
3 testes negativos garantem que mutations de uma tab não invalidam caches de outras:
useToggleAcaoChaveNÃO invalidaaplicacoesnemtarefas-kanbanuseBatchSaveAplicacoesinvalida APENASaplicacoesuseUpdateAplicacaoinvalida APENASaplicacoes
Fases por Nível (modo_config_fases: 'por_nivel')
Adicionado em: 2026-04-12 — Migration
nivel_idemfases_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_id | Significado |
|---|---|
NULL | Fase uniforme (compartilhada entre todos os níveis) |
<uuid> | Fase específica daquele nível |
Tabelas Envolvidas
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:
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 × segmentosModal "Criar Aplicação"
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
Navegação
- sections-registry.ts:
id: 'controle',icon: ClipboardCheck,permissionKey: 'controle' - permissions.ts:
controlenoCOORDENADOR_PERMISSIONS - Feature flag:
controle(deve estar ativa emfeature_flags)