Files
agenciapsilmno/docs/architecture/router-shared-applayout.md

6.9 KiB

Router — AppLayout Compartilhado

Data: 2026-03-25 Arquivo: src/router/index.js e arquivos de rota relacionados


Problema

A aplicação apresentava comportamento de "reload" ao navegar entre seções distintas (ex: de /configuracoes/agenda para /therapist/agenda/recorrencias). O efeito visual era a sidebar e a topbar piscando e sendo completamente reconstruídas — mesmo sendo o mesmo layout.

Além disso, ao clicar rapidamente para trocar de página, o Vue recarregava o componente de layout inteiro, causando lentidão perceptível.

Causa raiz

Cada área da aplicação tinha seu próprio registro de rota com component: AppLayout:

/therapist    → { component: AppLayout }   ← instância A
/admin        → { component: AppLayout }   ← instância B
/configuracoes→ { component: AppLayout }   ← instância C
/account      → { component: AppLayout }   ← instância D
...

O Vue Router trata registros de rota distintos como componentes distintos. Ao navegar de /configuracoes para /therapist, o Vue desmontava a instância A e montava a instância B do zero — sidebar, topbar, watchers, stores, tudo reiniciava. Isso é o comportamento correto do Vue Router, mas estava sendo usado de forma incorreta para uma aplicação que tem um layout único.


Solução

Consolidar todas as áreas autenticadas sob um único registro de rota pai com component: AppLayout. O Vue Router reutiliza a instância do componente pai enquanto navega entre os filhos.

Arquitetura após a mudança

/ → AppLayout (instância única — nunca desmontada)
  ├── therapist/     → RouterPassthrough → página
  ├── admin/         → RouterPassthrough → página
  ├── configuracoes/ → ConfiguracoesPage → sub-página
  ├── account/       → RouterPassthrough → página
  ├── upgrade/       → RouterPassthrough → página
  ├── supervisor/    → RouterPassthrough → página
  └── features/      → RouterPassthrough → página

Antes: AppLayout era desmontado/remontado ao trocar de seção. Depois: Apenas o conteúdo (área central) é trocado. AppLayout nunca desmonta.


Componente RouterPassthrough

Criado em src/layout/RouterPassthrough.vue:

<template><router-view /></template>

Serve como "rota de grupo com componente" para as áreas que não têm layout intermediário próprio (therapist, admin, account etc.). Ele é necessário porque o Vue Router precisa de um componente em cada nível da hierarquia para renderizar os filhos via <router-view>.


Arquivos alterados

src/router/index.js

Antes: importava cada rota e espalhava todas no nível raiz do array de rotas. Depois: cria um único pai { path: '/', component: AppLayout } e coloca todas as rotas de app como filhas.

const routes = [
    // Rotas sem layout
    ...publicRoutes,
    ...authRoutes,

    // Setup wizards (fullscreen, fora do AppLayout)
    ...therapistStandalone,
    ...clinicStandalone,

    // ─── AppLayout compartilhado ───────────────────────────
    {
        path: '/',
        component: AppLayout,
        children: [
            therapistRoutes,      // path: 'therapist'
            adminRoutes,          // path: 'admin'
            accountRoutes,        // path: 'account'
            billingRoutes,        // path: 'upgrade'
            supervisorRoutes,     // path: 'supervisor'
            featuresRoutes,       // path: 'features'
            configuracoesRoutes,  // path: 'configuracoes'
        ]
    },

    // AppLayout próprio (usuários completamente distintos)
    saasRoutes,
    editorRoutes,
    portalRoutes,

    // Catch-all (sempre por último)
    ...miscRoutes,
];

src/router/routes.therapist.js e routes.clinic.js

Setup wizards (tela cheia, sem layout) foram separados como export const therapistStandalone / clinicStandalone e continuam com paths absolutos (/therapist/setup, /admin/setup).

A rota principal passou de:

{ path: '/therapist', component: AppLayout, children: [...] }

Para:

{ path: 'therapist', component: RouterPassthrough, meta: {...}, children: [...] }

src/router/routes.configs.js

Removido o double-nesting desnecessário. Antes havia:

{ path: '/configuracoes', component: AppLayout,
  children: [
    { path: '', component: ConfiguracoesPage, children: [...] }
  ]
}

Depois, ConfiguracoesPage (que já tem <router-view> próprio) assume diretamente:

{ path: 'configuracoes', component: ConfiguracoesPage,
  redirect: { name: 'ConfiguracoesAgenda' },
  meta: { requiresAuth: true, roles: [...] },
  children: [...]
}

src/router/guards.js

/configuracoes adicionado ao isTenantArea para garantir que o tenant e entitlements são carregados corretamente ao acessar a URL diretamente:

const isTenantArea =
    to.path.startsWith('/admin') ||
    to.path.startsWith('/therapist') ||
    to.path.startsWith('/supervisor') ||
    to.path.startsWith('/configuracoes');

src/App.vue

Mesma correção no isTenantArea local, que controla o checkSetupWizard.


O que foi mantido separado (e por quê)

Área Motivo para manter AppLayout próprio
/saas Usado exclusivamente por saas_admin. Nunca há navegação cruzada com áreas de tenant.
/editor Role de plataforma distinto. Área separada.
/portal Sessão de paciente. Usuário completamente diferente.

Mesclar essas áreas no AppLayout compartilhado não traria ganho algum e criaria acoplamento desnecessário entre contextos de usuário distintos.


Debug removido na mesma sessão

Foram removidos resquícios de código de debug que estavam ativos em produção e impactavam todos os usuários:

  • router/index.js: console.trace() injetado em router.push e router.replace a cada clique de navegação (gerava stack trace completo).
  • App.vue: debugSnapshot() disparava 3 queries Supabase (auth.getUser, profiles, rpc.my_tenants) a cada troca de rota, causando o principal sintoma de lentidão.

O sistema de suporte técnico (supportGuard + supportDebugStore) não foi afetado — é uma feature controlada que só ativa via ?support=TOKEN validado no banco.


Meta inheritance

O Vue Router 4 mescla automaticamente o meta de todos os registros de rota correspondidos (to.matched). Isso significa que os filhos do RouterPassthrough herdam o meta do pai sem nenhuma configuração adicional.

Exemplo: ao navegar para /therapist/agenda, to.meta conterá:

{
    area: 'therapist',        // herdado do pai 'therapist'
    requiresAuth: true,       // herdado do pai 'therapist'
    roles: ['therapist'],     // herdado do pai 'therapist'
    feature: 'agenda.view'    // da própria rota 'agenda'
}

Os guards continuam funcionando sem nenhuma mudança de lógica.