Skip to content

Plano de Reestruturação por Papel + Sidebar Server-Driven + Cobertura Completa de Testes

Status: Aprovado (v2) — aguardando estabilização da release atual em produção para execução. Aprovação v1: 2026-03-17 Revisão v2: 2026-03-17 — Sidebar server-driven, feature flags, sem wrappers finos, fase 2 detalhada.

Contexto e Decisão

Prioridade atual: estabilidade da release v1 em produção, suporte a clientes pagantes, padronização. Esta refatoração será executada depois de fechar as funcionalidades pendentes. Este documento serve como guia de implementação futura.


1. Estrutura de Pastas — Organização por Papel

A regra é simples: cada papel é dono da sua pasta, cada feature vive dentro da pasta do papel que a usa.

text
src/components/
├── ui/                              # Design system (shadcn) — inalterado
├── shared/                          # Componentes cross-role
│   └── unified-sidebar.tsx          # Sidebar única, server-driven

├── admin/                           # Papel: administrador
│   ├── content.tsx                  # renderContent() do admin (fase 2)
│   ├── dashboard.tsx                # ex admin-dashboard.tsx
│   ├── escolas/                     # Feature: gestão de escolas
│   │   ├── lista.tsx                # ex admin-escolas.tsx
│   │   └── detalhes.tsx             # ex admin-escola-detalhes.tsx
│   ├── assinaturas/                 # Feature: assinaturas + faturas
│   │   ├── lista.tsx                # ex admin-assinaturas.tsx
│   │   ├── faturas.tsx              # ex admin-faturas.tsx
│   │   └── faturas-modal.tsx        # ex admin-faturas-modal.tsx
│   ├── usuarios.tsx                 # ex admin-usuarios.tsx
│   ├── monitoramento/               # Feature: logs, CRON, incidentes
│   │   ├── logs.tsx                 # ex admin-logs.tsx
│   │   ├── cron-monitor.tsx         # ex admin-cron-monitor.tsx
│   │   ├── incidentes.tsx           # ex admin-incidentes.tsx
│   │   └── mensagens-logs.tsx        # ex admin-sms-logs.tsx (logs WhatsApp/Wasender)
│   ├── notificacao.tsx              # ex admin-enviar-notificacao.tsx
│   └── index.ts                     # Barrel exports

├── especialista/                    # Papel: especialista
│   ├── content.tsx                  # renderContent() do especialista (fase 2)
│   ├── banners.tsx                  # ex banners-especialista.tsx
│   ├── header-novidades.tsx         # ex header-novidades-especialista.tsx
│   ├── olimpiadas/                  # Feature: olimpíadas dados
│   │   ├── lista.tsx                # ex olimpiadas-dados-especialista.tsx
│   │   └── detalhes.tsx             # ex olimpiada-detalhes-especialista.tsx
│   ├── templates/                   # Feature: templates mensagens
│   │   ├── lista.tsx                # ex templates-olimpiadas-lista.tsx
│   │   ├── detalhes.tsx             # ex templates-olimpiada-detalhes.tsx
│   │   └── edicao.tsx               # ex template-edicao.tsx
│   ├── cursos/                      # Feature: formação
│   │   ├── lista.tsx                # ex cursos-especialista.tsx
│   │   └── gerenciar-videos.tsx     # ex gerenciar-videos-curso.tsx
│   ├── tutoriais.tsx                # ex tutoriais-especialista.tsx
│   ├── configuracoes.tsx            # ex configuracoes-sistema-especialista.tsx
│   └── index.ts

├── coordenador/                     # Papel: coordenador
│   ├── content.tsx                  # renderContent() do coordenador (fase 2)
│   ├── agenda/                      # ✅ JÁ MIGRADO — ex dashboard-coordenador.tsx (7 arquivos)
│   ├── olimpiadas.tsx               # ex olimpiadas.tsx
│   ├── calendario.tsx               # ex calendario-olimpico.tsx
│   ├── resultados/                  # Feature: resultados + premiação
│   │   ├── lista.tsx                # ex resultados.tsx
│   │   ├── detalhes.tsx             # ex resultados-olimpiada-detalhes.tsx
│   │   ├── inserir-dialog.tsx       # ex resultados-inserir-dialog.tsx
│   │   ├── premiacao-modal.tsx      # ex resultados-premiacao-manual-modal.tsx
│   │   └── selecao-metodo.tsx       # ex resultados-selecao-metodo-modal.tsx
│   ├── inscricoes.tsx               # ex inscricoes.tsx
│   ├── alunos/                      # Feature: gestão de alunos
│   │   ├── lista.tsx                # ex alunos-escola.tsx
│   │   ├── importacao/              # Sub-feature — já existe como pasta
│   │   └── transferencias/          # ex transferencia-alunos-*.tsx
│   ├── turmas/                      # já existe como pasta
│   ├── comunicacao/                 # Feature: mensagens
│   │   ├── lista.tsx                # ex comunicacao.tsx
│   │   ├── mensagem-modal.tsx
│   │   └── rascunho-modal.tsx
│   ├── videos.tsx                   # ex videos.tsx
│   ├── mural-olimpico/              # já existe como pasta — mover para cá
│   ├── controle-aplicacoes/         # Feature: aplicações
│   │   ├── lista.tsx                # ex controle-aplicacoes.tsx
│   │   ├── novo.tsx                 # ex controle-aplicacoes-novo.tsx
│   │   └── config-inscricao.tsx     # ex config-arquivo-inscricao.tsx
│   └── index.ts

├── escola/                          # Papel: escola (gestão)
│   ├── content.tsx                  # renderContent() da escola (fase 2)
│   ├── dashboard.tsx                # ex dashboard-escola.tsx
│   ├── usuarios.tsx                 # ex usuarios-escola.tsx
│   ├── pagamentos.tsx               # ex pagamentos-escola.tsx
│   ├── configuracoes.tsx            # ex configuracoes-escola.tsx
│   ├── portal-config.tsx            # ex portal-config-escola.tsx
│   └── index.ts

├── diretor/                         # já existe — inalterado
│   ├── content.tsx                  # renderContent() do diretor (fase 2)
│   ├── painel-geral-diretor.tsx
│   ├── uso-plataforma-diretor.tsx
│   ├── projeto-olimpico-diretor.tsx
│   ├── financeiro-diretor.tsx
│   └── index.ts

├── portal/                          # Portal aluno/responsável (auth separada)
│   └── (já gerenciado em pages/portal/)

└── auth/                            # Login, role-switcher
    ├── login-unified.tsx
    ├── login-especialista.tsx
    ├── role-switcher.tsx
    └── index.ts

Regra de ouro

Se o componente é exclusivo de um papel → vive dentro da pasta do papel. Se é compartilhado entre papéis → vive em shared/. Se é design system → vive em ui/.

O que muda para os usuários em produção

Nada. Esta é uma refatoração interna de organização de arquivos. Nenhuma funcionalidade, tela, ou comportamento muda.


2. Sidebar Unificada — Server-Driven

Problema atual

Existem 4 implementações separadas da sidebar com código duplicado:

ArquivoPapel(is)Linhas
sidebar.tsxescola + coordenador (compartilhada via prop)180
sidebar-admin.tsxadministrador139
sidebar-especialista.tsxespecialista143
diretor/sidebar-diretor.tsxdiretor135

Todas compartilham: estado collapse/expand, animação de texto, perfil com avatar/badge, logo, scroll horizontal. A diferença é só a lista de menuItems.

Decisão: Sem wrappers por papel

A sidebar não resolve permissões, não filtra nada, não conhece flags. Ela recebe items prontos do backend via useMyPermissions().

Arquitetura

text
┌──────────────────────────────────────────────────────┐
│  Edge Function: user-permissions                     │
│                                                      │
│  1. Lê usuario_papeis → principal_role + escola_id   │
│  2. Lê usuarios_escola_permissoes → permissionKeys   │
│  3. Lê feature_flags + canary → flags ativas escola  │
│  4. sections-registry (server-side copy) → todos os  │
│     items do papel                                   │
│  5. Intersecção: permissão ∩ flag ativa = items      │
│  6. Retorna: { role, permissoes[], menuItems[] }     │
└──────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│  useMyPermissions() (React Query, 5min staleTime)    │
│                                                      │
│  Retorna: { role, permissoes, menuItems, isLoading } │
└──────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────┐
│  UnifiedSidebar                                      │
│                                                      │
│  Recebe menuItems prontos, renderiza.                │
│  Mapeia iconKey string → componente Lucide.          │
│  Zero lógica de permissão/flag.                      │
└──────────────────────────────────────────────────────┘

Interface da Sidebar

typescript
// src/components/shared/unified-sidebar.tsx

interface MenuItem {
  id: string;          // ex: "agenda", "mural", "resultados"
  label: string;       // ex: "Agenda"
  iconKey: string;     // ex: "LayoutDashboard" → mapeia para componente Lucide
}

interface UnifiedSidebarProps {
  activeSection: string;
  onSectionChange: (section: string) => void;
  username: string;
  role: string;
  menuItems: MenuItem[];   // ← 100% resolvido server-side
  escolaNome?: string;
}

Consumo no App.tsx

typescript
const { menuItems, role } = useMyPermissions();

<UnifiedSidebar
  activeSection={activeSection}
  onSectionChange={handleSectionChange}
  username={username}
  role={role}
  menuItems={menuItems}
  escolaNome={escolaNome}
/>

iconMap — único ponto de mapeamento no frontend

typescript
// src/lib/icon-map.ts
import { LayoutDashboard, Newspaper, BarChart3, ... } from 'lucide-react';

export const iconMap: Record<string, LucideIcon> = {
  LayoutDashboard,
  Newspaper,
  BarChart3,
  // ...
};

A sidebar usa iconMap[item.iconKey] para renderizar. O frontend não sabe quais items existem — só renderiza o que veio do backend.


3. Feature Flags — Controle Completo Server-Side

Objetivo

Ligar/desligar funcionalidades sem deploy, com granularidade por escola, região ou global. Tudo controlado pelo admin via UI.

Tabelas envolvidas

TabelaFunção
feature_flagsRegistro de cada flag (slug, ativa_global, descricao)
canary_groupsGrupos de escolas (ex: "Escolas SP", "Rede ABC")
canary_group_escolasVínculo escola ↔ grupo
feature_flag_canaryVínculo flag ↔ grupo (ativa flag para grupo)
usuarios_escola_permissoesPermissão individual por usuário

Fluxo de resolução (dentro de user-permissions)

text
1. Login → select-role → JWT com { usuario_id, escola_id, principal_role }

2. Frontend chama user-permissions (GET my_permissions)

3. Edge Function resolve TUDO:
   a) Busca permissões do usuário (usuarios_escola_permissoes)
   b) Busca feature flags:
      - Se flag.ativa_global = true → ativa para todos
      - Senão, verifica se escola está em canary_group com a flag ativa
   c) Carrega sections-registry do papel
   d) Para cada section: 
      - Tem permissão? (ou papel sem permissões como admin/especialista)
      - Feature flag ativa para escola? (se section tem flag vinculada)
      - Ambos verdadeiros → inclui no menuItems
   e) Retorna { role, permissoes[], menuItems[] }

4. Frontend recebe e renderiza. Não filtra nada.

Exemplo concreto

Escola A tem flag mural_v2 ativa. Escola B não.

json
// Coordenador Escola A:
{
  "role": "coordenador",
  "permissoes": ["agenda", "mural", "resultados"],
  "menuItems": [
    { "id": "agenda", "label": "Agenda", "iconKey": "LayoutDashboard" },
    { "id": "mural", "label": "Mural Olímpico", "iconKey": "Newspaper" },
    { "id": "resultados", "label": "Resultados", "iconKey": "BarChart3" }
  ]
}

// Coordenador Escola B (sem flag mural_v2):
{
  "role": "coordenador",
  "permissoes": ["agenda", "resultados"],
  "menuItems": [
    { "id": "agenda", "label": "Agenda", "iconKey": "LayoutDashboard" },
    { "id": "resultados", "label": "Resultados", "iconKey": "BarChart3" }
  ]
}

O frontend da Escola B nunca sabe que mural existe. Switch on/off puro no backend. Sem afetar estado do usuário.

Granularidade de controle (tudo via UI admin)

NívelControleMecanismo
GlobalLiga/desliga para todosfeature_flags.ativa_global = true
Por escolaLiga para escolas específicascanary_group_escolas + feature_flag_canary
Por regiãoGrupo de escolas por regiãocanary_groups (ex: "Escolas SP")
Por rede/mantenedoraGrupo de escolas da mesma redecanary_groups (ex: "Rede ABC")
Por usuárioPermissão individualusuarios_escola_permissoes

O admin gerencia tudo via painel na UI. A resolução final acontece na Edge Function user-permissions.

Integração com CANARY_RELEASE_SYSTEM.md

O plano de canary release (docs/plans/CANARY_RELEASE_SYSTEM.md) define as tabelas e fluxo de rollout progressivo. Este plano integra o canary com o sections-registry.ts e a sidebar, completando o ciclo:

text
Admin UI → feature_flags + canary_groups → user-permissions resolve → menuItems → sidebar renderiza

4. Fase 2 — Extração do renderContent() (App.tsx)

Problema atual

O App.tsx (~973 linhas) contém um renderContent() gigante com switches aninhados por papel:

typescript
// Hoje:
if (userProfile === "administrador") {
  switch(activeSection) {
    case "admin-dashboard": return <AdminDashboard />;
    case "admin-escolas": return <AdminEscolas />;
    // ... ~15 cases
  }
}
if (userProfile === "coordenacao") {
  switch(activeSection) {
    case "painel_controle": return <DashboardCoordenador />;
    // ... ~20 cases
  }
}
// etc.

Solução (fase 2, após pastas por papel existirem)

Extrair cada bloco para um componente <XContent> dentro da pasta do papel:

typescript
// App.tsx simplificado (~300 linhas)
const renderContent = () => {
  switch (userProfile) {
    case "administrador":  return <AdminContent activeSection={activeSection} />;
    case "especialista":   return <EspecialistaContent activeSection={activeSection} />;
    case "gestao":         return <EscolaContent activeSection={activeSection} />;
    case "coordenacao":    return <CoordenadorContent activeSection={activeSection} />;
    case "diretor":        return <DiretorContent activeSection={activeSection} />;
  }
};
typescript
// src/components/coordenador/content.tsx
export function CoordenadorContent({ activeSection }: { activeSection: string }) {
  switch (activeSection) {
    case "agenda":      return <Agenda />;  // src/components/agenda/ (já migrado)
    case "mural":       return <MuralOlimpico />;
    case "resultados":  return <Resultados />;
    // ...
    default: return <Agenda />;
  }
}

Proteção via feature flags

Com feature flags server-side, se um menuItem não veio na resposta, o activeSection nunca será setado para aquele valor (não existe botão na sidebar), e o componente nunca renderiza. Zero lógica condicional de flags no renderContent().

Por que é fase 2?

Depende das pastas por papel existirem (fase 1) para que os imports façam sentido. Executar após a migração de arquivos.


5. Cobertura Completa de Testes — Novo Padrão

Política a partir de agora

Toda feature que vai para produção deve ter:

CamadaFerramentaO que cobreOnde vive
UnitárioVitestLógica de negócio, helpers, transforms, validações__tests__/ dentro da feature folder
Edge FunctionDeno testCORS, auth, RBAC, contratos de request/response, errossupabase/functions/<fn>/index.test.ts
E2EPlaywrightJornadas de usuário ponta-a-ponta por papele2e/<dominio>.spec.ts

Processo para a refatoração de pastas

Para cada domínio movido:

  1. Inventário de testes existentes — listar o que já existe
  2. Escrever testes faltantes ANTES de mover — garantir baseline funcional completa
  3. Rodar suite completa do projetovitest run + deno test + playwright test
  4. Mover arquivos — criar pasta, atualizar imports, barrel export
  5. Rodar suite completa novamente — zero regressões
  6. Um domínio por iteração — nunca misturar dois

Testes que faltam hoje (inventário parcial, precisa completar)

DomínioUnitEdge FnE2EStatus
mural-olimpico✅ 82 testes✅ 20 testes✅ mural-coordenador.spec.tsCompleto
olimpiada-detalhes✅ vitest✅ deno✅ olimpiada-detalhes.spec.tsCompleto
resultados❌ gestao-resultadosPrioridade 1
alunos❌ gestao-alunosPrioridade 1
comunicacao❌ comunicacao-escolaPrioridade 2
inscricoes❌ inscricoes-olimpiadaPrioridade 2
admin-escolas❌ admin-escolasPrioridade 2
usuarios-escola❌ gestao-usuarios-escolaPrioridade 2
portal-aluno✅ visibility❌ portal-escola✅ portal-aluno.spec.tsQuase completo

6. Ordem de Execução Recomendada

Após fechar as funcionalidades pendentes da release v1 em produção:

FaseDomínioRiscoJustificativa
0shared/unified-sidebar.tsx + user-permissions retornando menuItemsMédioBase para tudo; precisa de mudança no backend
1admin/BaixoPouco acoplamento com outros papéis, poucos usuários admin
2especialista/BaixoMesmo raciocínio, papel isolado
3auth/Baixo3 arquivos, sem lógica de negócio complexa
4escola/MédioExtrair modo gestao da sidebar.tsx compartilhada
5coordenador/AltoMaior volume de features, mais arquivos, mais acoplamento
6portal/BaixoJá isolado em pages/portal/
7Fase 2: content.tsx por papel + slim App.tsxMédioDepende de todas as pastas existirem

7. Lacunas e Riscos Identificados

Acoplamento sidebar.tsx ↔ dois papéis

A sidebar.tsx atual serve escola e coordenador via prop userProfile. Na refatoração, ela é eliminada — substituída pela UnifiedSidebar que recebe menuItems do backend. Não há split, não há wrapper.

alunos-escola.tsx — 3.455 linhas

Este arquivo precisa de split antes de mover para coordenador/alunos/. É o maior monolito do projeto e impossível de testar adequadamente sem decomposição.

Hooks seguem flat

Os hooks (src/hooks/) não precisam mudar de estrutura agora. Eles já têm prefixos claros e não são exclusivos de um papel. Mover hooks para dentro de pastas de papel criaria acoplamento artificial.

Backend não muda (estrutura)

As Edge Functions (supabase/functions/) já seguem organização por papel com prefixos. A única mudança é na user-permissions para retornar menuItems além de permissoes.

sections-registry: cópia server-side

O sections-registry.ts hoje vive no frontend. Para a sidebar server-driven, uma cópia ou equivalente precisa existir no backend (dentro de _shared/ ou inline na user-permissions). A fonte de verdade de quais sections existem por papel vive no backend.

Feature flags: tabelas necessárias

As tabelas feature_flags, canary_groups, canary_group_escolas e feature_flag_canary precisam ser criadas via migration antes da fase 0. O plano completo está em docs/plans/CANARY_RELEASE_SYSTEM.md.


Resumo

DecisãoValor
OrganizaçãoPor papel → feature dentro do papel
SidebarComponente único, server-driven, sem wrappers por papel
Permissões/Flags100% resolvidos no backend, frontend só renderiza
Feature FlagsGranular: global, por escola, por região, por usuário
Fase 2Extração do renderContent() em content.tsx por papel
TestesCobertura completa obrigatória antes de qualquer move
TimingApós release v1 em produção estabilizada
ProcessoUm papel por iteração, testes antes e depois
Impacto em produçãoZero — refatoração interna