Skip to content

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

Status: Aprovado (v2) — aguardando estabilização do MVP-1 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 MVP-1, suporte a clientes em produção, 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 do MVP-1:

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 MVP-1 estabilizado
ProcessoUm papel por iteração, testes antes e depois
Impacto em produçãoZero — refatoração interna