Adicionada compressão Brotli/Gzip, auto-import de Vue e PrimeVue, e análise visual do bundle para otimização de produção e Remove AppLayout duplicado de cada área (therapist, admin, configuracoes, account, supervisor, billing, features) e consolida sob um único pai no router/index.js. Adiciona RouterPassthrough para grupos de rota sem layout intermediário. Remove debug ativo (console.trace em router.push e queries Supabase em todo watch de rota) que degradava performance para todos os usuários.

This commit is contained in:
Leonardo
2026-03-25 12:14:43 -03:00
parent bfe148ef12
commit 0658e2e9bf
18 changed files with 979 additions and 991 deletions
+4 -114
View File
@@ -19,7 +19,6 @@ import { onMounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { supabase } from '@/lib/supabase/client';
import { useTenantStore } from '@/stores/tenantStore';
import { useEntitlementsStore } from '@/stores/entitlementsStore';
import { fetchDocsForPath } from '@/composables/useAjuda';
import AjudaDrawer from '@/components/AjudaDrawer.vue';
@@ -28,25 +27,14 @@ import AppOfflineOverlay from '@/components/AppOfflineOverlay.vue';
const route = useRoute();
const router = useRouter();
const tenantStore = useTenantStore();
const entStore = useEntitlementsStore();
function isTenantArea(path = '') {
return path.startsWith('/admin') || path.startsWith('/therapist');
}
function isPortalArea(path = '') {
return path.startsWith('/portal');
}
function isSaasArea(path = '') {
return path.startsWith('/saas');
return path.startsWith('/admin') || path.startsWith('/therapist') || path.startsWith('/configuracoes');
}
// ── Setup Wizard redirect ────────────────────────────────────────
async function checkSetupWizard() {
// Só verifica em área de tenant
if (!isTenantArea(route.path)) return;
// Não redireciona se já está no setup
if (route.path.includes('/setup')) return;
const uid = tenantStore.user?.id;
@@ -56,7 +44,6 @@ async function checkSetupWizard() {
if (!data) return;
// Determina o kind do tenant ativo para saber qual flag checar
const activeMembership = tenantStore.memberships?.find((m) => m.id === tenantStore.activeTenantId);
const kind = activeMembership?.kind ?? tenantStore.activeRole ?? '';
const isClinic = kind.startsWith('clinic');
@@ -68,113 +55,16 @@ async function checkSetupWizard() {
}
}
async function debugSnapshot(label = 'snapshot') {
console.group(`🧭 [APP DEBUG] ${label}`);
try {
// 0) rota + meta
console.log('route.fullPath:', route.fullPath);
console.log('route.path:', route.path);
console.log('route.name:', route.name);
console.log('route.meta:', route.meta);
// 1) storage
console.groupCollapsed('📦 Storage');
console.log('localStorage.tenant_id:', localStorage.getItem('tenant_id'));
console.log('localStorage.currentTenantId:', localStorage.getItem('currentTenantId'));
console.log('localStorage.tenant:', localStorage.getItem('tenant'));
console.log('sessionStorage.redirect_after_login:', sessionStorage.getItem('redirect_after_login'));
console.log('sessionStorage.intended_area:', sessionStorage.getItem('intended_area'));
console.groupEnd();
// 2) sessão auth (fonte real)
const { data: authData, error: authErr } = await supabase.auth.getUser();
if (authErr) console.warn('[auth.getUser] error:', authErr);
const user = authData?.user || null;
console.log('auth.user:', user ? { id: user.id, email: user.email } : null);
// 3) profiles.role (identidade global)
let profileRole = null;
if (user?.id) {
const { data: profile, error: pErr } = await supabase.from('profiles').select('role').eq('id', user.id).single();
if (pErr) console.warn('[profiles] error:', pErr);
profileRole = profile?.role || null;
}
console.log('profiles.role (global):', profileRole);
// 4) memberships via RPC (fonte de verdade do tenantStore)
let rpcTenants = null;
if (user?.id) {
const { data: rpcData, error: rpcErr } = await supabase.rpc('my_tenants');
if (rpcErr) console.warn('[rpc my_tenants] error:', rpcErr);
rpcTenants = rpcData ?? null;
}
console.log('rpc.my_tenants():', rpcTenants);
// 5) stores (sempre logar)
console.groupCollapsed('🏪 Stores (before optional loads)');
console.log('tenantStore.activeTenantId:', tenantStore.activeTenantId);
console.log('tenantStore.activeRole:', tenantStore.activeRole);
console.log('tenantStore.memberships:', tenantStore.memberships);
console.log('entStore.loaded:', entStore.loaded);
console.log('entStore.tenantId:', entStore.activeTenantId || entStore.tenantId);
console.groupEnd();
// 6) IMPORTANTÍSSIMO: não carregar tenant fora da área tenant
const path = route.path || '';
if (isTenantArea(path)) {
console.log('✅ Tenant area detected → will loadSessionAndTenant + entitlements');
await tenantStore.loadSessionAndTenant();
if (tenantStore.activeTenantId) {
await entStore.loadForTenant(tenantStore.activeTenantId);
}
console.groupCollapsed('🏪 Stores (after tenant loads)');
console.log('tenantStore.activeTenantId:', tenantStore.activeTenantId);
console.log('tenantStore.activeRole:', tenantStore.activeRole);
console.log('tenantStore.memberships:', tenantStore.memberships);
console.log("entStore.can('online_scheduling.manage'):", entStore.can?.('online_scheduling.manage'));
console.groupEnd();
// Redireciona para o wizard se setup não foi concluído
await checkSetupWizard();
} else if (isPortalArea(path)) {
console.log('🟣 Portal area detected → SKIP tenantStore.loadSessionAndTenant()');
} else if (isSaasArea(path)) {
console.log('🟠 SaaS area detected → SKIP tenantStore.loadSessionAndTenant()');
} else {
console.log('⚪ Other/public area detected → SKIP tenantStore.loadSessionAndTenant()');
}
} catch (e) {
console.error('[APP DEBUG] snapshot error:', e);
} finally {
console.groupEnd();
}
}
onMounted(async () => {
// 🔥 PRIMEIRO LOG — TENANT ID BRUTO (mantive sua ideia)
console.log('[SEU_TENANT_ID]', localStorage.getItem('tenant_id'));
// snapshot inicial
await debugSnapshot('mounted');
// Carrega docs de ajuda para a rota inicial
onMounted(() => {
fetchDocsForPath(route.path);
});
// snapshot a cada navegação (isso é o que vai te salvar)
watch(
() => route.fullPath,
async (to, from) => {
await debugSnapshot(`route change: ${from} -> ${to}`);
// Atualiza docs de ajuda ao navegar
() => {
fetchDocsForPath(route.path);
// Verifica setup sempre que entrar em área tenant
if (isTenantArea(route.path) && tenantStore.loaded) {
await checkSetupWizard();
checkSetupWizard();
}
}
);
+57 -57
View File
@@ -6,68 +6,68 @@
// biome-ignore lint: disable
export {}
declare global {
const EffectScope: typeof import('vue').EffectScope
const computed: typeof import('vue').computed
const createApp: typeof import('vue').createApp
const customRef: typeof import('vue').customRef
const defineAsyncComponent: typeof import('vue').defineAsyncComponent
const defineComponent: typeof import('vue').defineComponent
const effectScope: typeof import('vue').effectScope
const getCurrentInstance: typeof import('vue').getCurrentInstance
const getCurrentScope: typeof import('vue').getCurrentScope
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
const h: typeof import('vue').h
const inject: typeof import('vue').inject
const isProxy: typeof import('vue').isProxy
const isReactive: typeof import('vue').isReactive
const isReadonly: typeof import('vue').isReadonly
const isRef: typeof import('vue').isRef
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const isShallow: typeof import('vue').isShallow
const markRaw: typeof import('vue').markRaw
const nextTick: typeof import('vue').nextTick
const onActivated: typeof import('vue').onActivated
const onBeforeMount: typeof import('vue').onBeforeMount
const onBeforeUnmount: typeof import('vue').onBeforeUnmount
const onBeforeUpdate: typeof import('vue').onBeforeUpdate
const onDeactivated: typeof import('vue').onDeactivated
const onErrorCaptured: typeof import('vue').onErrorCaptured
const onMounted: typeof import('vue').onMounted
const onRenderTracked: typeof import('vue').onRenderTracked
const onRenderTriggered: typeof import('vue').onRenderTriggered
const onScopeDispose: typeof import('vue').onScopeDispose
const onServerPrefetch: typeof import('vue').onServerPrefetch
const onUnmounted: typeof import('vue').onUnmounted
const onUpdated: typeof import('vue').onUpdated
const onWatcherCleanup: typeof import('vue').onWatcherCleanup
const provide: typeof import('vue').provide
const reactive: typeof import('vue').reactive
const readonly: typeof import('vue').readonly
const ref: typeof import('vue').ref
const resolveComponent: typeof import('vue').resolveComponent
const shallowReactive: typeof import('vue').shallowReactive
const shallowReadonly: typeof import('vue').shallowReadonly
const shallowRef: typeof import('vue').shallowRef
const toRaw: typeof import('vue').toRaw
const toRef: typeof import('vue').toRef
const toRefs: typeof import('vue').toRefs
const toValue: typeof import('vue').toValue
const triggerRef: typeof import('vue').triggerRef
const unref: typeof import('vue').unref
const useAttrs: typeof import('vue').useAttrs
const useCssModule: typeof import('vue').useCssModule
const useCssVars: typeof import('vue').useCssVars
const useId: typeof import('vue').useId
const useModel: typeof import('vue').useModel
const useSlots: typeof import('vue').useSlots
const useTemplateRef: typeof import('vue').useTemplateRef
const watch: typeof import('vue').watch
const watchEffect: typeof import('vue').watchEffect
const watchPostEffect: typeof import('vue').watchPostEffect
const watchSyncEffect: typeof import('vue').watchSyncEffect
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useId: typeof import('vue')['useId']
const useModel: typeof import('vue')['useModel']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}
// for type re-export
declare global {
// @ts-ignore
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, ShallowRef, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
import('vue')
}
+7 -7
View File
@@ -15,21 +15,21 @@
|--------------------------------------------------------------------------
-->
<script setup>
import { ref, reactive, computed, watch, onMounted, onBeforeUnmount } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { useConfirm } from 'primevue/useconfirm';
import { supabase } from '@/lib/supabase/client';
import Menu from 'primevue/menu';
import MultiSelect from 'primevue/multiselect';
import Popover from 'primevue/popover';
import Menu from 'primevue/menu';
import ProgressSpinner from 'primevue/progressspinner';
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import PatientProntuario from '@/features/patients/prontuario/PatientProntuario.vue';
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue';
import PatientActionMenu from '@/components/patients/PatientActionMenu.vue';
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
import PatientCadastroDialog from '@/components/ui/PatientCadastroDialog.vue';
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
import PatientProntuario from '@/features/patients/prontuario/PatientProntuario.vue';
import { getSysGroupColor, getSystemGroupDefaultColor } from '@/utils/systemGroupColors.js';
// ── Descontos por paciente ────────────────────────────────────────
+1
View File
@@ -0,0 +1 @@
<template><router-view /></template>
+39 -63
View File
@@ -1,56 +1,42 @@
/*
|--------------------------------------------------------------------------
| Agência PSI
|--------------------------------------------------------------------------
| Criado e desenvolvido por Leonardo Nohama
|
| Tecnologia aplicada à escuta.
| Estrutura para o cuidado.
|
| Arquivo: src/main.js
| Data: 2026
| Local: São Carlos/SP — Brasil
|--------------------------------------------------------------------------
| © 2026 — Todos os direitos reservados
| Agência PSI (OTIMIZADO)
|--------------------------------------------------------------------------
*/
import { pinia } from '@/plugins/pinia';
import router from '@/router';
import { createApp } from 'vue';
import App from './App.vue';
import router from '@/router';
import { pinia } from '@/plugins/pinia'; // ← singleton criado antes do router
import { setOnSignedOut, initSession, listenAuthChanges, refreshSession } from '@/app/session';
import { initSession, listenAuthChanges, refreshSession, setOnSignedOut } from '@/app/session';
// PrimeVue core
import Aura from '@primeuix/themes/aura';
import PrimeVue from 'primevue/config';
// serviços (ok global)
import ConfirmationService from 'primevue/confirmationservice';
import ToastService from 'primevue/toastservice';
// ✅ SOMENTE COMPONENTES LEVES GLOBAIS
import Button from 'primevue/button';
import Divider from 'primevue/divider';
import InputText from 'primevue/inputtext';
import Tag from 'primevue/tag';
import Toast from 'primevue/toast';
// seus componentes leves
import AppLoadingPhrases from '@/components/ui/AppLoadingPhrases.vue';
import LoadedPhraseBlock from '@/components/ui/LoadedPhraseBlock.vue';
// ── Componentes PrimeVue globais (≥ 10 usos no projeto) ──────────────────────
import Button from 'primevue/button';
import InputText from 'primevue/inputtext';
import Tag from 'primevue/tag';
import FloatLabel from 'primevue/floatlabel';
import Toast from 'primevue/toast';
import IconField from 'primevue/iconfield';
import InputIcon from 'primevue/inputicon';
import Divider from 'primevue/divider';
import Card from 'primevue/card';
import SelectButton from 'primevue/selectbutton';
import Dialog from 'primevue/dialog';
import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import ConfirmDialog from 'primevue/confirmdialog';
import Menu from 'primevue/menu';
// ─────────────────────────────────────────────────────────────────────────────
import '@/assets/tailwind.css';
// estilos
import '@/assets/styles.scss';
import '@/assets/tailwind.css';
import { supabase } from '@/lib/supabase/client';
// ✅ pt-BR (PrimeVue locale global)
// locale
const ptBR = {
firstDayOfWeek: 1,
dayNames: ['domingo', 'segunda-feira', 'terça-feira', 'quarta-feira', 'quinta-feira', 'sexta-feira', 'sábado'],
@@ -64,15 +50,16 @@ const ptBR = {
dateFormat: 'dd/mm/yy'
};
// theme antecipado
async function applyUserThemeEarly() {
try {
const { data } = await supabase.auth.getUser();
const user = data?.user;
if (!user) return;
const { data: settings, error } = await supabase.from('user_settings').select('theme_mode').eq('user_id', user.id).maybeSingle();
const { data: settings } = await supabase.from('user_settings').select('theme_mode').eq('user_id', user.id).maybeSingle();
if (error || !settings?.theme_mode) return;
if (!settings?.theme_mode) return;
const isDark = settings.theme_mode === 'dark';
document.documentElement.classList.toggle('app-dark', isDark);
@@ -80,15 +67,15 @@ async function applyUserThemeEarly() {
} catch {}
}
// logout
setOnSignedOut(() => {
router.replace('/auth/login');
});
// ===== flags globais (debug/controle) =====
// flags
window.__sessionRefreshing = false;
window.__fromVisibilityRefresh = false;
window.__appBootstrapped = false;
// ========================================
let lastVisibilityRefreshAt = 0;
@@ -97,7 +84,7 @@ document.addEventListener('visibilitychange', async () => {
if (!window.__appBootstrapped) return;
const now = Date.now();
if (now - lastVisibilityRefreshAt < 10_000) return;
if (now - lastVisibilityRefreshAt < 10000) return;
if (window.__sessionRefreshing) return;
try {
@@ -106,7 +93,6 @@ document.addEventListener('visibilitychange', async () => {
} catch {}
lastVisibilityRefreshAt = now;
console.log('[VISIBILITY] Aba voltou -> refreshSession()');
try {
window.__sessionRefreshing = true;
@@ -114,16 +100,16 @@ document.addEventListener('visibilitychange', async () => {
await refreshSession();
try {
const path = router.currentRoute.value?.path || '';
const isTenantArea = path.startsWith('/admin') || path.startsWith('/therapist') || path.startsWith('/saas');
const path = router.currentRoute.value?.path || '';
const isTenantArea = path.startsWith('/admin') || path.startsWith('/therapist') || path.startsWith('/saas');
if (isTenantArea) {
window.dispatchEvent(new CustomEvent('app:session-refreshed', { detail: { source: 'visibility' } }));
} else {
console.log('[VISIBILITY] refresh ok (skip event) - area não-tenant:', path);
}
} catch {}
if (isTenantArea) {
window.dispatchEvent(
new CustomEvent('app:session-refreshed', {
detail: { source: 'visibility' }
})
);
}
} finally {
window.__fromVisibilityRefresh = false;
window.__sessionRefreshing = false;
@@ -133,12 +119,10 @@ document.addEventListener('visibilitychange', async () => {
async function bootstrap() {
await initSession({ initial: true });
listenAuthChanges();
await applyUserThemeEarly();
const app = createApp(App);
// ✅ usa o pinia singleton — o mesmo que o router/guards já conhecem
app.use(pinia);
app.use(router);
@@ -155,21 +139,13 @@ async function bootstrap() {
app.use(ToastService);
app.use(ConfirmationService);
// ✅ globais leves
app.component('Button', Button);
app.component('InputText', InputText);
app.component('Tag', Tag);
app.component('FloatLabel', FloatLabel);
app.component('Toast', Toast);
app.component('IconField', IconField);
app.component('InputIcon', InputIcon);
app.component('Divider', Divider);
app.component('Card', Card);
app.component('SelectButton', SelectButton);
app.component('Dialog', Dialog);
app.component('DataTable', DataTable);
app.component('Column', Column);
app.component('ConfirmDialog', ConfirmDialog);
app.component('Menu', Menu);
app.component('Toast', Toast);
app.component('AppLoadingPhrases', AppLoadingPhrases);
app.component('LoadedPhraseBlock', LoadedPhraseBlock);
+5 -1
View File
@@ -341,7 +341,11 @@ export function applyGuards(router) {
return { path: '/auth/login' };
}
const isTenantArea = to.path.startsWith('/admin') || to.path.startsWith('/therapist') || to.path.startsWith('/supervisor');
const isTenantArea =
to.path.startsWith('/admin') ||
to.path.startsWith('/therapist') ||
to.path.startsWith('/supervisor') ||
to.path.startsWith('/configuracoes');
// ======================================
// ✅ IDENTIDADE GLOBAL (cached por uid — sem query a cada navegação)
+54 -69
View File
@@ -14,48 +14,77 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import { createRouter, createWebHistory, isNavigationFailure, NavigationFailureType } from 'vue-router';
import { createRouter, createWebHistory } from 'vue-router';
import AppLayout from '@/layout/AppLayout.vue';
import configuracoesRoutes from './routes.configs';
import meRoutes from './routes.account';
import adminRoutes from './routes.clinic';
import authRoutes from './routes.auth';
// ── Rotas de app (filhas do AppLayout compartilhado) ──────────────────────
import therapistRoutes, { therapistStandalone } from './routes.therapist';
import adminRoutes, { clinicStandalone } from './routes.clinic';
import accountRoutes from './routes.account';
import billingRoutes from './routes.billing';
import miscRoutes from './routes.misc';
import portalRoutes from './routes.portal';
import publicRoutes from './routes.public';
import saasRoutes from './routes.saas';
import therapistRoutes from './routes.therapist';
import supervisorRoutes from './routes.supervisor';
import editorRoutes from './routes.editor';
import featuresRoutes from './routes.features';
import configuracoesRoutes from './routes.configs';
import { pinia } from '@/plugins/pinia'; // ← singleton compartilhado
// ── Rotas com AppLayout próprio (usuários distintos, sem navegação cruzada) ──
import saasRoutes from './routes.saas';
import editorRoutes from './routes.editor';
import portalRoutes from './routes.portal';
// ── Rotas sem AppLayout ───────────────────────────────────────────────────
import authRoutes from './routes.auth';
import publicRoutes from './routes.public';
import miscRoutes from './routes.misc';
import { pinia } from '@/plugins/pinia';
import { supportGuard } from '@/support/supportGuard';
import { applyGuards } from './guards';
const routes = [
// ── Sem layout (public + auth) ─────────────────────────────────────────
...(Array.isArray(publicRoutes) ? publicRoutes : [publicRoutes]),
...(Array.isArray(authRoutes) ? authRoutes : [authRoutes]),
...(Array.isArray(miscRoutes) ? miscRoutes : [miscRoutes]),
...(Array.isArray(billingRoutes) ? billingRoutes : [billingRoutes]),
// ── Fullscreen — fora do AppLayout (setup wizards etc.) ────────────────
...therapistStandalone,
...clinicStandalone,
// ══════════════════════════════════════════════════════════════════════
// AppLayout ÚNICO compartilhado por todas as áreas autenticadas.
//
// Benefício: sidebar, topbar e estado do layout NUNCA são desmontados
// ao navegar entre /therapist, /admin, /configuracoes, /account etc.
// Cada área usa um RouterPassthrough (path relativo sem componente visual)
// que mantém o AppLayout intacto enquanto só o conteúdo é trocado.
// ══════════════════════════════════════════════════════════════════════
{
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: áreas de usuários distintos ────────────────────
// Saas, editor e portal são sessões de usuário completamente diferentes.
// Nunca há navegação cruzada entre essas áreas e as áreas de app acima.
...(Array.isArray(saasRoutes) ? saasRoutes : [saasRoutes]),
...(Array.isArray(meRoutes) ? meRoutes : [meRoutes]),
...(Array.isArray(adminRoutes) ? adminRoutes : [adminRoutes]),
...(Array.isArray(therapistRoutes) ? therapistRoutes : [therapistRoutes]),
...(Array.isArray(supervisorRoutes) ? supervisorRoutes : [supervisorRoutes]),
...(Array.isArray(editorRoutes) ? editorRoutes : [editorRoutes]),
...(Array.isArray(portalRoutes) ? portalRoutes : [portalRoutes]),
...(Array.isArray(configuracoesRoutes) ? configuracoesRoutes : [configuracoesRoutes]),
...(Array.isArray(featuresRoutes) ? featuresRoutes : [featuresRoutes]),
// ✅ compat: rota antiga /login → /auth/login
// ── Misc (catch-all SEMPRE por último) ────────────────────────────────
...(Array.isArray(miscRoutes) ? miscRoutes : [miscRoutes]),
// ── Compat: rota legada /login → /auth/login ──────────────────────────
{
path: '/login',
redirect: (to) => ({
path: '/auth/login',
query: to.query || {}
})
redirect: (to) => ({ path: '/auth/login', query: to.query || {} })
}
];
@@ -68,39 +97,6 @@ const router = createRouter({
}
});
/* 🔎 DEBUG: listar todas as rotas registradas */
console.log(
'[ROUTES]',
router
.getRoutes()
.map((r) => r.path)
.sort()
);
// ===== DEBUG NAV + TRACE (remover depois) =====
const _push = router.push.bind(router);
router.push = async (loc) => {
console.log('[router.push]', loc);
console.trace('[push caller]');
const res = await _push(loc);
if (isNavigationFailure(res, NavigationFailureType.duplicated)) console.warn('[NAV FAIL] duplicated', res);
else if (isNavigationFailure(res, NavigationFailureType.cancelled)) console.warn('[NAV FAIL] cancelled', res);
else if (isNavigationFailure(res, NavigationFailureType.aborted)) console.warn('[NAV FAIL] aborted', res);
else if (isNavigationFailure(res, NavigationFailureType.redirected)) console.warn('[NAV FAIL] redirected', res);
return res;
};
const _replace = router.replace.bind(router);
router.replace = async (loc) => {
console.log('[router.replace]', loc);
console.trace('[replace caller]');
const res = await _replace(loc);
if (isNavigationFailure(res, NavigationFailureType.cancelled)) console.warn('[NAV FAIL replace] cancelled', res);
else if (isNavigationFailure(res, NavigationFailureType.aborted)) console.warn('[NAV FAIL replace] aborted', res);
else if (isNavigationFailure(res, NavigationFailureType.redirected)) console.warn('[NAV FAIL replace] redirected', res);
return res;
};
router.onError((e) => console.error('[router.onError]', e));
// ✅ support guard — passa pinia para garantir acesso ao store antes do app.use(pinia)
@@ -108,17 +104,6 @@ router.beforeEach(async (to) => {
await supportGuard(to, pinia);
});
router.beforeEach((to, from) => {
console.log('[beforeEach]', from.fullPath, '->', to.fullPath);
return true;
});
router.afterEach((to, from, failure) => {
if (failure) console.warn('[afterEach failure]', failure);
else console.log('[afterEach ok]', from.fullPath, '->', to.fullPath);
});
// ===== /DEBUG NAV + TRACE =====
applyGuards(router);
export default router;
+3 -3
View File
@@ -14,11 +14,11 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import AppLayout from '@/layout/AppLayout.vue';
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
export default {
path: '/account',
component: AppLayout,
path: 'account',
component: RouterPassthrough,
meta: { requiresAuth: true, area: 'account' },
children: [
{
+3 -3
View File
@@ -14,11 +14,11 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import AppLayout from '@/layout/AppLayout.vue';
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
export default {
path: '/upgrade',
component: AppLayout,
path: 'upgrade',
component: RouterPassthrough,
meta: { requiresAuth: true },
children: [
{
+161 -214
View File
@@ -14,225 +14,172 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import AppLayout from '@/layout/AppLayout.vue';
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
export default [
// ======================================================
// 🚀 SETUP WIZARD — fora do AppLayout (fullscreen)
// ======================================================
// ── Rotas fullscreen — ficam fora do AppLayout compartilhado ──────────────
export const clinicStandalone = [
{
path: '/admin/setup',
name: 'admin.setup',
component: () => import('@/features/setup/SetupWizardPage.vue'),
meta: { area: 'admin', requiresAuth: true, roles: ['clinic_admin'], fullscreen: true }
},
{
path: '/admin',
component: AppLayout,
meta: {
// 🔐 Tudo aqui dentro exige login
area: 'admin',
requiresAuth: true,
// 👤 Perfil de acesso (tenant-level)
// tenantStore normaliza tenant_admin -> clinic_admin, mas mantemos compatibilidade
roles: ['clinic_admin']
},
children: [
// ======================================================
// 📊 DASHBOARD
// ======================================================
{ path: '', name: 'admin.dashboard', component: () => import('@/views/pages/clinic/ClinicDashboard.vue') },
// ======================================================
// 🧩 CLÍNICA — MÓDULOS (tenant_features)
// ======================================================
{
path: 'clinic/features',
name: 'admin-clinic-features',
component: () => import('@/views/pages/clinic/clinic/ClinicFeaturesPage.vue'),
meta: {
// opcional: restringir apenas para admin canônico
roles: ['clinic_admin', 'tenant_admin']
}
},
{
path: 'clinic/professionals',
name: 'admin-clinic-professionals',
component: () => import('@/views/pages/clinic/clinic/ClinicProfessionalsPage.vue'),
meta: {
requiresAuth: true,
roles: ['clinic_admin', 'tenant_admin']
}
},
// ======================================================
// 💳 MEU PLANO
// ======================================================
{
path: 'meu-plano',
name: 'admin-meu-plano',
component: () => import('@/views/pages/billing/ClinicMeuPlanoPage.vue')
},
// ======================================================
// 📅 AGENDA DA CLÍNICA
// ======================================================
{
path: 'agenda/clinica',
name: 'admin-agenda-clinica',
component: () => import('@/features/agenda/pages/AgendaClinicaPage.vue'),
meta: {
feature: 'agenda.view',
roles: ['clinic_admin', 'tenant_admin']
}
},
// Recorrências
{
path: 'agenda/recorrencias',
name: 'admin-agenda-recorrencias',
component: () => import('@/features/agenda/pages/AgendaRecorrenciasPage.vue'),
meta: { feature: 'agenda.view', roles: ['clinic_admin', 'tenant_admin'], mode: 'clinic' }
},
// ✅ NOVO: Compromissos determinísticos (tipos)
{
path: 'agenda/compromissos',
name: 'admin-agenda-compromissos',
component: () => import('@/features/agenda/pages/CompromissosDeterminados.vue'),
meta: {
feature: 'agenda.view',
roles: ['clinic_admin', 'tenant_admin']
// ✅ sem tenantScope: a área /admin já está no tenant da clínica pelo fluxo normal
}
},
// ======================================================
// 👥 PACIENTES (módulo ativável por clínica)
// ======================================================
// 📋 Lista de pacientes
{
path: 'pacientes',
name: 'admin-pacientes',
component: () => import('@/features/patients/PatientsListPage.vue'),
meta: {
// ✅ depende do tenant_features.patients
tenantFeature: 'patients'
}
},
// Cadastro de paciente (novo)
{
path: 'pacientes/cadastro',
name: 'admin-pacientes-cadastro',
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
meta: {
tenantFeature: 'patients'
}
},
// ✏️ Editar paciente
{
path: 'pacientes/cadastro/:id',
name: 'admin-pacientes-cadastro-edit',
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
props: true,
meta: {
tenantFeature: 'patients'
}
},
// 👥 Grupos de pacientes
{
path: 'pacientes/grupos',
name: 'admin-pacientes-grupos',
component: () => import('@/features/patients/grupos/GruposPacientesPage.vue'),
meta: {
tenantFeature: 'patients'
}
},
// 🏷️ Tags de pacientes
{
path: 'pacientes/tags',
name: 'admin-pacientes-tags',
component: () => import('@/features/patients/tags/TagsPage.vue'),
meta: {
tenantFeature: 'patients'
}
},
// 🔗 Link externo para cadastro
{
path: 'pacientes/link-externo',
name: 'admin-pacientes-link-externo',
component: () => import('@/features/patients/cadastro/PatientsExternalLinkPage.vue'),
meta: {
tenantFeature: 'patients'
}
},
// 📥 Cadastros recebidos via link externo
{
path: 'pacientes/cadastro/recebidos',
name: 'admin-pacientes-recebidos',
component: () => import('@/features/patients/cadastro/recebidos/CadastrosRecebidosPage.vue'),
meta: {
tenantFeature: 'patients'
}
},
// ======================================================
// 🔐 SEGURANÇA
// ======================================================
{
path: 'settings/security',
name: 'admin-settings-security',
component: () => import('@/views/pages/auth/SecurityPage.vue')
},
// ======================================================
// 🔒 MÓDULO PRO — Online Scheduling
// ======================================================
{
path: 'online-scheduling',
name: 'admin-online-scheduling',
component: () => import('@/views/pages/clinic/OnlineSchedulingAdminPage.vue'),
meta: {
feature: 'online_scheduling.manage'
}
},
// ======================================================
// 🔒 PRO — Agendamentos Recebidos
// ======================================================
{
path: 'agendamentos-recebidos',
name: 'admin-agendamentos-recebidos',
component: () => import('@/features/agenda/pages/AgendamentosRecebidosPage.vue'),
meta: {
feature: 'online_scheduling.manage'
}
},
// ======================================================
// 💰 FINANCEIRO
// ======================================================
{
path: 'financeiro',
name: 'admin-financeiro',
component: () => import('@/features/financeiro/pages/FinanceiroDashboardPage.vue')
},
{
path: 'financeiro/lancamentos',
name: 'admin-financeiro-lancamentos',
component: () => import('@/features/financeiro/pages/FinanceiroPage.vue')
}
]
}
];
// ── Rotas de app — serão filhas do AppLayout compartilhado no index.js ────
export default {
path: 'admin',
component: RouterPassthrough,
meta: {
area: 'admin',
requiresAuth: true,
roles: ['clinic_admin']
},
children: [
// ======================================================
// 📊 DASHBOARD
// ======================================================
{ path: '', name: 'admin.dashboard', component: () => import('@/views/pages/clinic/ClinicDashboard.vue') },
// ======================================================
// 🧩 CLÍNICA — MÓDULOS (tenant_features)
// ======================================================
{
path: 'clinic/features',
name: 'admin-clinic-features',
component: () => import('@/views/pages/clinic/clinic/ClinicFeaturesPage.vue'),
meta: { roles: ['clinic_admin', 'tenant_admin'] }
},
{
path: 'clinic/professionals',
name: 'admin-clinic-professionals',
component: () => import('@/views/pages/clinic/clinic/ClinicProfessionalsPage.vue'),
meta: { requiresAuth: true, roles: ['clinic_admin', 'tenant_admin'] }
},
// ======================================================
// 💳 MEU PLANO
// ======================================================
{
path: 'meu-plano',
name: 'admin-meu-plano',
component: () => import('@/views/pages/billing/ClinicMeuPlanoPage.vue')
},
// ======================================================
// 📅 AGENDA DA CLÍNICA
// ======================================================
{
path: 'agenda/clinica',
name: 'admin-agenda-clinica',
component: () => import('@/features/agenda/pages/AgendaClinicaPage.vue'),
meta: { feature: 'agenda.view', roles: ['clinic_admin', 'tenant_admin'] }
},
// Recorrências
{
path: 'agenda/recorrencias',
name: 'admin-agenda-recorrencias',
component: () => import('@/features/agenda/pages/AgendaRecorrenciasPage.vue'),
meta: { feature: 'agenda.view', roles: ['clinic_admin', 'tenant_admin'], mode: 'clinic' }
},
// ✅ Compromissos determinísticos
{
path: 'agenda/compromissos',
name: 'admin-agenda-compromissos',
component: () => import('@/features/agenda/pages/CompromissosDeterminados.vue'),
meta: { feature: 'agenda.view', roles: ['clinic_admin', 'tenant_admin'] }
},
// ======================================================
// 👥 PACIENTES
// ======================================================
{
path: 'pacientes',
name: 'admin-pacientes',
component: () => import('@/features/patients/PatientsListPage.vue'),
meta: { tenantFeature: 'patients' }
},
{
path: 'pacientes/cadastro',
name: 'admin-pacientes-cadastro',
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
meta: { tenantFeature: 'patients' }
},
{
path: 'pacientes/cadastro/:id',
name: 'admin-pacientes-cadastro-edit',
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
props: true,
meta: { tenantFeature: 'patients' }
},
{
path: 'pacientes/grupos',
name: 'admin-pacientes-grupos',
component: () => import('@/features/patients/grupos/GruposPacientesPage.vue'),
meta: { tenantFeature: 'patients' }
},
{
path: 'pacientes/tags',
name: 'admin-pacientes-tags',
component: () => import('@/features/patients/tags/TagsPage.vue'),
meta: { tenantFeature: 'patients' }
},
{
path: 'pacientes/link-externo',
name: 'admin-pacientes-link-externo',
component: () => import('@/features/patients/cadastro/PatientsExternalLinkPage.vue'),
meta: { tenantFeature: 'patients' }
},
{
path: 'pacientes/cadastro/recebidos',
name: 'admin-pacientes-recebidos',
component: () => import('@/features/patients/cadastro/recebidos/CadastrosRecebidosPage.vue'),
meta: { tenantFeature: 'patients' }
},
// ======================================================
// 🔐 SEGURANÇA
// ======================================================
{
path: 'settings/security',
name: 'admin-settings-security',
component: () => import('@/views/pages/auth/SecurityPage.vue')
},
// ======================================================
// 🔒 MÓDULO PRO — Online Scheduling
// ======================================================
{
path: 'online-scheduling',
name: 'admin-online-scheduling',
component: () => import('@/views/pages/clinic/OnlineSchedulingAdminPage.vue'),
meta: { feature: 'online_scheduling.manage' }
},
// ======================================================
// 🔒 PRO — Agendamentos Recebidos
// ======================================================
{
path: 'agendamentos-recebidos',
name: 'admin-agendamentos-recebidos',
component: () => import('@/features/agenda/pages/AgendamentosRecebidosPage.vue'),
meta: { feature: 'online_scheduling.manage' }
},
// ======================================================
// 💰 FINANCEIRO
// ======================================================
{
path: 'financeiro',
name: 'admin-financeiro',
component: () => import('@/features/financeiro/pages/FinanceiroDashboardPage.vue')
},
{
path: 'financeiro/lancamentos',
name: 'admin-financeiro-lancamentos',
component: () => import('@/features/financeiro/pages/FinanceiroPage.vue')
}
]
};
+84 -92
View File
@@ -14,11 +14,13 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import AppLayout from '@/layout/AppLayout.vue';
const configuracoesRoutes = {
path: '/configuracoes',
component: AppLayout,
// ConfiguracoesPage já tem <router-view> próprio — serve de layout intermediário.
// Não precisa de RouterPassthrough.
export default {
path: 'configuracoes',
component: () => import('@/layout/ConfiguracoesPage.vue'),
redirect: { name: 'ConfiguracoesAgenda' },
meta: {
requiresAuth: true,
@@ -27,94 +29,84 @@ const configuracoesRoutes = {
children: [
{
path: '',
component: () => import('@/layout/ConfiguracoesPage.vue'),
redirect: { name: 'ConfiguracoesAgenda' },
children: [
{
path: 'agenda',
name: 'ConfiguracoesAgenda',
component: () => import('@/layout/configuracoes/ConfiguracoesAgendaPage.vue')
},
{
path: 'bloqueios',
name: 'ConfiguracoesBloqueios',
component: () => import('@/layout/configuracoes/BloqueiosPage.vue')
},
{
path: 'agendador',
name: 'ConfiguracoesAgendador',
component: () => import('@/layout/configuracoes/ConfiguracoesAgendadorPage.vue')
},
{
path: 'pagamento',
name: 'ConfiguracoesPagamento',
component: () => import('@/layout/configuracoes/ConfiguracoesPagamentoPage.vue')
},
{
path: 'precificacao',
name: 'ConfiguracoesPrecificacao',
component: () => import('@/layout/configuracoes/ConfiguracoesPrecificacaoPage.vue')
},
{
path: 'descontos',
name: 'ConfiguracoesDescontos',
component: () => import('@/layout/configuracoes/ConfiguracoesDescontosPage.vue')
},
{
path: 'excecoes-financeiras',
name: 'ConfiguracoesExcecoesFinanceiras',
component: () => import('@/layout/configuracoes/ConfiguracoesExcecoesFinanceirasPage.vue')
},
{
path: 'convenios',
name: 'ConfiguracoesConvenios',
component: () => import('@/layout/configuracoes/ConfiguracoesConveniosPage.vue')
},
{
path: 'email-templates',
name: 'ConfiguracoesEmailTemplates',
component: () => import('@/layout/configuracoes/ConfiguracoesEmailTemplatesPage.vue')
},
{
path: 'empresa',
name: 'ConfiguracoesMinhaEmpresa',
component: () => import('@/layout/configuracoes/ConfiguracoesMinhaEmpresaPage.vue')
},
{
path: 'canais',
name: 'ConfiguracoesCanais',
component: () => import('@/layout/configuracoes/ConfiguracoesCanaisPage.vue')
},
{
path: 'whatsapp',
name: 'ConfiguracoesWhatsapp',
component: () => import('@/layout/configuracoes/ConfiguracoesWhatsappPage.vue')
},
{
path: 'whatsapp-twilio',
name: 'ConfiguracoesWhatsappTwilio',
component: () => import('@/layout/configuracoes/ConfiguracoesTwilioWhatsappPage.vue')
},
{
path: 'sms',
name: 'ConfiguracoesSms',
component: () => import('@/layout/configuracoes/ConfiguracoesSmsPage.vue')
},
{
path: 'sms-canal',
name: 'ConfiguracoesSmsCanal',
component: () => import('@/views/pages/notifications/SmsChannelSetupPage.vue')
},
{
path: 'recursos-extras',
name: 'ConfiguracoesRecursosExtras',
component: () => import('@/layout/configuracoes/ConfiguracoesRecursosExtrasPage.vue')
}
]
path: 'agenda',
name: 'ConfiguracoesAgenda',
component: () => import('@/layout/configuracoes/ConfiguracoesAgendaPage.vue')
},
{
path: 'bloqueios',
name: 'ConfiguracoesBloqueios',
component: () => import('@/layout/configuracoes/BloqueiosPage.vue')
},
{
path: 'agendador',
name: 'ConfiguracoesAgendador',
component: () => import('@/layout/configuracoes/ConfiguracoesAgendadorPage.vue')
},
{
path: 'pagamento',
name: 'ConfiguracoesPagamento',
component: () => import('@/layout/configuracoes/ConfiguracoesPagamentoPage.vue')
},
{
path: 'precificacao',
name: 'ConfiguracoesPrecificacao',
component: () => import('@/layout/configuracoes/ConfiguracoesPrecificacaoPage.vue')
},
{
path: 'descontos',
name: 'ConfiguracoesDescontos',
component: () => import('@/layout/configuracoes/ConfiguracoesDescontosPage.vue')
},
{
path: 'excecoes-financeiras',
name: 'ConfiguracoesExcecoesFinanceiras',
component: () => import('@/layout/configuracoes/ConfiguracoesExcecoesFinanceirasPage.vue')
},
{
path: 'convenios',
name: 'ConfiguracoesConvenios',
component: () => import('@/layout/configuracoes/ConfiguracoesConveniosPage.vue')
},
{
path: 'email-templates',
name: 'ConfiguracoesEmailTemplates',
component: () => import('@/layout/configuracoes/ConfiguracoesEmailTemplatesPage.vue')
},
{
path: 'empresa',
name: 'ConfiguracoesMinhaEmpresa',
component: () => import('@/layout/configuracoes/ConfiguracoesMinhaEmpresaPage.vue')
},
{
path: 'canais',
name: 'ConfiguracoesCanais',
component: () => import('@/layout/configuracoes/ConfiguracoesCanaisPage.vue')
},
{
path: 'whatsapp',
name: 'ConfiguracoesWhatsapp',
component: () => import('@/layout/configuracoes/ConfiguracoesWhatsappPage.vue')
},
{
path: 'whatsapp-twilio',
name: 'ConfiguracoesWhatsappTwilio',
component: () => import('@/layout/configuracoes/ConfiguracoesTwilioWhatsappPage.vue')
},
{
path: 'sms',
name: 'ConfiguracoesSms',
component: () => import('@/layout/configuracoes/ConfiguracoesSmsPage.vue')
},
{
path: 'sms-canal',
name: 'ConfiguracoesSmsCanal',
component: () => import('@/views/pages/notifications/SmsChannelSetupPage.vue')
},
{
path: 'recursos-extras',
name: 'ConfiguracoesRecursosExtras',
component: () => import('@/layout/configuracoes/ConfiguracoesRecursosExtrasPage.vue')
}
]
};
export default configuracoesRoutes;
+5 -6
View File
@@ -14,18 +14,17 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import AppLayout from '@/layout/AppLayout.vue';
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
export default {
path: '/features',
component: AppLayout,
meta: { requiresAuth: true }, // roles: se você quiser travar aqui também
path: 'features',
component: RouterPassthrough,
meta: { requiresAuth: true },
children: [
// Patients
{
path: 'patients',
name: 'features.patients.list',
component: () => import('@/features/patients/PatientsListPage.vue') // ajuste se seu arquivo tiver outro nome
component: () => import('@/features/patients/PatientsListPage.vue')
},
{
path: 'patients/cadastro',
+3 -5
View File
@@ -14,14 +14,12 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import AppLayout from '@/layout/AppLayout.vue';
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
export default {
path: '/supervisor',
component: AppLayout,
path: 'supervisor',
component: RouterPassthrough,
// tenantScope: 'supervisor' → o guard troca automaticamente para o tenant
// com kind='supervisor' quando o usuário navega para esta área.
meta: {
area: 'supervisor',
requiresAuth: true,
+153 -162
View File
@@ -14,173 +14,164 @@
| © 2026 — Todos os direitos reservados
|--------------------------------------------------------------------------
*/
import AppLayout from '@/layout/AppLayout.vue';
import RouterPassthrough from '@/layout/RouterPassthrough.vue';
export default [
// ======================================================
// 🚀 SETUP WIZARD — fora do AppLayout (fullscreen)
// ======================================================
// ── Rotas fullscreen — ficam fora do AppLayout compartilhado ──────────────
export const therapistStandalone = [
{
path: '/therapist/setup',
name: 'therapist.setup',
component: () => import('@/features/setup/SetupWizardPage.vue'),
meta: { area: 'therapist', requiresAuth: true, roles: ['therapist'], fullscreen: true }
},
{
path: '/therapist',
component: AppLayout,
meta: { area: 'therapist', requiresAuth: true, roles: ['therapist'] },
children: [
// ======================================================
// 📊 DASHBOARD
// ======================================================
{ path: '', name: 'therapist.dashboard', component: () => import('@/views/pages/therapist/TherapistDashboard.vue') },
// ======================================================
// 📅 AGENDA
// ======================================================
{
path: 'agenda',
name: 'therapist-agenda',
component: () => import('@/features/agenda/pages/AgendaTerapeutaPage.vue'),
meta: {
feature: 'agenda.view'
}
},
// Recorrências
{
path: 'agenda/recorrencias',
name: 'therapist-agenda-recorrencias',
component: () => import('@/features/agenda/pages/AgendaRecorrenciasPage.vue'),
meta: { feature: 'agenda.view', mode: 'therapist' }
},
// ✅ Compromissos determinísticos
{
path: 'agenda/compromissos',
name: 'therapist-agenda-compromissos',
component: () => import('@/features/agenda/pages/CompromissosDeterminados.vue'),
meta: {
feature: 'agenda.view',
roles: ['therapist']
// ✅ sem tenantScope
}
},
// ======================================================
// 💳 MEU PLANO
// ======================================================
{
path: 'meu-plano',
name: 'therapist-meu-plano',
component: () => import('@/views/pages/billing/TherapistMeuPlanoPage.vue')
},
{
path: 'upgrade',
name: 'therapist-upgrade',
component: () => import('@/views/pages/billing/TherapistUpgradePage.vue')
},
// ======================================================
// 👥 PATIENTS
// ======================================================
{
path: 'patients',
name: 'therapist-patients',
component: () => import('@/features/patients/PatientsListPage.vue')
},
{
path: 'patients/cadastro',
name: 'therapist-patients-create',
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue')
},
{
path: 'patients/cadastro/:id',
name: 'therapist-patients-edit',
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
props: true
},
{
path: 'patients/grupos',
name: 'therapist-patients-groups',
component: () => import('@/features/patients/grupos/GruposPacientesPage.vue')
},
{
path: 'patients/tags',
name: 'therapist-patients-tags',
component: () => import('@/features/patients/tags/TagsPage.vue')
},
{
path: 'patients/link-externo',
name: 'therapist-patients-link-externo',
component: () => import('@/features/patients/cadastro/PatientsExternalLinkPage.vue')
},
{
path: 'patients/cadastro/recebidos',
name: 'therapist-patients-recebidos',
component: () => import('@/features/patients/cadastro/recebidos/CadastrosRecebidosPage.vue')
},
// ======================================================
// 🔒 PRO — Online Scheduling
// ======================================================
{
path: 'online-scheduling',
name: 'therapist-online-scheduling',
component: () => import('@/views/pages/therapist/OnlineSchedulingPage.vue'),
meta: {
feature: 'online_scheduling.manage'
}
},
// ======================================================
// 🔒 PRO — Agendamentos Recebidos
// ======================================================
{
path: 'agendamentos-recebidos',
name: 'therapist-agendamentos-recebidos',
component: () => import('@/features/agenda/pages/AgendamentosRecebidosPage.vue'),
meta: {
feature: 'online_scheduling.manage'
}
},
// ======================================================
// 💰 FINANCEIRO
// ======================================================
{
path: 'financeiro',
name: 'therapist-financeiro',
component: () => import('@/features/financeiro/pages/FinanceiroDashboardPage.vue')
},
{
path: 'financeiro/lancamentos',
name: 'therapist-financeiro-lancamentos',
component: () => import('@/features/financeiro/pages/FinanceiroPage.vue')
},
// ======================================================
// 📈 RELATÓRIOS
// ======================================================
{
path: 'relatorios',
name: 'therapist-relatorios',
component: () => import('@/views/pages/therapist/RelatoriosPage.vue'),
meta: { feature: 'agenda.view' }
},
// ======================================================
// 🔐 SECURITY
// ======================================================
{
path: 'settings/security',
name: 'therapist-settings-security',
component: () => import('@/views/pages/auth/SecurityPage.vue')
}
]
}
];
// ── Rotas de app — serão filhas do AppLayout compartilhado no index.js ────
export default {
path: 'therapist',
component: RouterPassthrough,
meta: { area: 'therapist', requiresAuth: true, roles: ['therapist'] },
children: [
// ======================================================
// 📊 DASHBOARD
// ======================================================
{ path: '', name: 'therapist.dashboard', component: () => import('@/views/pages/therapist/TherapistDashboard.vue') },
// ======================================================
// 📅 AGENDA
// ======================================================
{
path: 'agenda',
name: 'therapist-agenda',
component: () => import('@/features/agenda/pages/AgendaTerapeutaPage.vue'),
meta: { feature: 'agenda.view' }
},
// Recorrências
{
path: 'agenda/recorrencias',
name: 'therapist-agenda-recorrencias',
component: () => import('@/features/agenda/pages/AgendaRecorrenciasPage.vue'),
meta: { feature: 'agenda.view', mode: 'therapist' }
},
// ✅ Compromissos determinísticos
{
path: 'agenda/compromissos',
name: 'therapist-agenda-compromissos',
component: () => import('@/features/agenda/pages/CompromissosDeterminados.vue'),
meta: {
feature: 'agenda.view',
roles: ['therapist']
}
},
// ======================================================
// 💳 MEU PLANO
// ======================================================
{
path: 'meu-plano',
name: 'therapist-meu-plano',
component: () => import('@/views/pages/billing/TherapistMeuPlanoPage.vue')
},
{
path: 'upgrade',
name: 'therapist-upgrade',
component: () => import('@/views/pages/billing/TherapistUpgradePage.vue')
},
// ======================================================
// 👥 PATIENTS
// ======================================================
{
path: 'patients',
name: 'therapist-patients',
component: () => import('@/features/patients/PatientsListPage.vue')
},
{
path: 'patients/cadastro',
name: 'therapist-patients-create',
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue')
},
{
path: 'patients/cadastro/:id',
name: 'therapist-patients-edit',
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
props: true
},
{
path: 'patients/grupos',
name: 'therapist-patients-groups',
component: () => import('@/features/patients/grupos/GruposPacientesPage.vue')
},
{
path: 'patients/tags',
name: 'therapist-patients-tags',
component: () => import('@/features/patients/tags/TagsPage.vue')
},
{
path: 'patients/link-externo',
name: 'therapist-patients-link-externo',
component: () => import('@/features/patients/cadastro/PatientsExternalLinkPage.vue')
},
{
path: 'patients/cadastro/recebidos',
name: 'therapist-patients-recebidos',
component: () => import('@/features/patients/cadastro/recebidos/CadastrosRecebidosPage.vue')
},
// ======================================================
// 🔒 PRO — Online Scheduling
// ======================================================
{
path: 'online-scheduling',
name: 'therapist-online-scheduling',
component: () => import('@/views/pages/therapist/OnlineSchedulingPage.vue'),
meta: { feature: 'online_scheduling.manage' }
},
// ======================================================
// 🔒 PRO — Agendamentos Recebidos
// ======================================================
{
path: 'agendamentos-recebidos',
name: 'therapist-agendamentos-recebidos',
component: () => import('@/features/agenda/pages/AgendamentosRecebidosPage.vue'),
meta: { feature: 'online_scheduling.manage' }
},
// ======================================================
// 💰 FINANCEIRO
// ======================================================
{
path: 'financeiro',
name: 'therapist-financeiro',
component: () => import('@/features/financeiro/pages/FinanceiroDashboardPage.vue')
},
{
path: 'financeiro/lancamentos',
name: 'therapist-financeiro-lancamentos',
component: () => import('@/features/financeiro/pages/FinanceiroPage.vue')
},
// ======================================================
// 📈 RELATÓRIOS
// ======================================================
{
path: 'relatorios',
name: 'therapist-relatorios',
component: () => import('@/views/pages/therapist/RelatoriosPage.vue'),
meta: { feature: 'agenda.view' }
},
// ======================================================
// 🔐 SECURITY
// ======================================================
{
path: 'settings/security',
name: 'therapist-settings-security',
component: () => import('@/views/pages/auth/SecurityPage.vue')
}
]
};