first commit

This commit is contained in:
Leonardo
2026-02-18 22:36:45 -03:00
parent ec6b6ef53a
commit 676042268b
122 changed files with 26354 additions and 1615 deletions

248
src/router/guards.js Normal file
View File

@@ -0,0 +1,248 @@
// ⚠️ Guard depende de sessão estável.
// Nunca disparar refresh concorrente durante navegação protegida.
// Ver comentário em session.js sobre race condition.
import { supabase } from '@/lib/supabase/client'
import { useTenantStore } from '@/stores/tenantStore'
import { useEntitlementsStore } from '@/stores/entitlementsStore'
import { buildUpgradeUrl } from '@/utils/upgradeContext'
import { sessionUser, sessionReady, sessionRefreshing, initSession } from '@/app/session'
// cache simples (evita bater no banco em toda navegação)
let sessionUidCache = null
// cache de saas admin por uid (pra não consultar tabela toda vez)
let saasAdminCacheUid = null
let saasAdminCacheIsAdmin = null
function roleToPath (role) {
if (role === 'tenant_admin') return '/admin'
if (role === 'therapist') return '/therapist'
if (role === 'patient') return '/patient'
if (role === 'saas_admin') return '/saas'
return '/'
}
function sleep (ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function waitSessionIfRefreshing () {
if (!sessionReady.value) {
try { await initSession({ initial: true }) } catch (e) {
console.warn('[guards] initSession falhou:', e)
}
}
for (let i = 0; i < 30; i++) {
if (!sessionRefreshing.value) return
await sleep(50)
}
}
async function isSaasAdmin (uid) {
if (!uid) return false
if (saasAdminCacheUid === uid && typeof saasAdminCacheIsAdmin === 'boolean') {
return saasAdminCacheIsAdmin
}
const { data, error } = await supabase
.from('saas_admins')
.select('user_id')
.eq('user_id', uid)
.maybeSingle()
const ok = !error && !!data
saasAdminCacheUid = uid
saasAdminCacheIsAdmin = ok
return ok
}
// heurística segura: carrega entitlements se ainda não carregou ou mudou tenant
function shouldLoadEntitlements (ent, tenantId) {
if (!tenantId) return false
const loaded = typeof ent.loaded === 'boolean' ? ent.loaded : false
const entTenantId = ent.activeTenantId ?? ent.tenantId ?? null
if (!loaded) return true
if (entTenantId && entTenantId !== tenantId) return true
return false
}
// wrapper: chama loadForTenant sem depender de force:false existir
async function loadEntitlementsSafe (ent, tenantId, force) {
if (!ent?.loadForTenant) return
try {
await ent.loadForTenant(tenantId, { force: !!force })
} catch (e) {
// se quebrou tentando force false (store não suporta), tenta força true uma vez
if (!force) {
console.warn('[guards] ent.loadForTenant(force:false) falhou, tentando force:true', e)
await ent.loadForTenant(tenantId, { force: true })
return
}
throw e
}
}
// util: roles guard (plural)
function matchesRoles (roles, activeRole) {
if (!Array.isArray(roles) || !roles.length) return true
return roles.includes(activeRole)
}
export function applyGuards (router) {
if (window.__guardsBound) return
window.__guardsBound = true
router.beforeEach(async (to) => {
const tlabel = `[guard] ${to.fullPath}`
console.time(tlabel)
try {
// públicos
if (to.meta?.public) { console.timeEnd(tlabel); return true }
if (to.path.startsWith('/auth')) { console.timeEnd(tlabel); return true }
// se rota não exige auth, libera
if (!to.meta?.requiresAuth) { console.timeEnd(tlabel); return true }
// não decide nada no meio do refresh do session.js
console.timeLog(tlabel, 'waitSessionIfRefreshing')
await waitSessionIfRefreshing()
// precisa estar logado (fonte estável do session.js)
const uid = sessionUser.value?.id || null
if (!uid) {
sessionStorage.setItem('redirect_after_login', to.fullPath)
console.timeEnd(tlabel)
return { path: '/auth/login' }
}
// se uid mudou, invalida caches e stores dependentes
if (sessionUidCache !== uid) {
sessionUidCache = uid
saasAdminCacheUid = null
saasAdminCacheIsAdmin = null
const ent0 = useEntitlementsStore()
if (typeof ent0.invalidate === 'function') ent0.invalidate()
}
// saas admin (com cache)
if (to.meta?.saasAdmin) {
console.timeLog(tlabel, 'isSaasAdmin')
const ok = await isSaasAdmin(uid)
if (!ok) { console.timeEnd(tlabel); return { path: '/pages/access' } }
}
// carrega tenant + role
const tenant = useTenantStore()
console.timeLog(tlabel, 'tenant.loadSessionAndTenant?')
if (!tenant.loaded && !tenant.loading) {
await tenant.loadSessionAndTenant()
}
// se não tem user no store, trata como não logado
if (!tenant.user) {
sessionStorage.setItem('redirect_after_login', to.fullPath)
console.timeEnd(tlabel)
return { path: '/auth/login' }
}
// se não tem tenant ativo:
// - se não tem memberships active -> manda pro access (sem clínica)
// - se tem memberships active mas activeTenantId está null -> seta e segue
if (!tenant.activeTenantId) {
const mem = Array.isArray(tenant.memberships) ? tenant.memberships : []
const firstActive = mem.find(m => m && m.status === 'active' && m.tenant_id)
if (!firstActive) {
if (to.path === '/pages/access') { console.timeEnd(tlabel); return true }
console.timeEnd(tlabel)
return { path: '/pages/access' }
}
if (typeof tenant.setActiveTenant === 'function') {
tenant.setActiveTenant(firstActive.tenant_id)
} else {
tenant.activeTenantId = firstActive.tenant_id
tenant.activeRole = firstActive.role
}
}
const tenantId = tenant.activeTenantId
if (!tenantId) {
if (to.path === '/pages/access') { console.timeEnd(tlabel); return true }
console.timeEnd(tlabel)
return { path: '/pages/access' }
}
// entitlements (✅ carrega só quando precisa)
const ent = useEntitlementsStore()
if (shouldLoadEntitlements(ent, tenantId)) {
console.timeLog(tlabel, 'ent.loadForTenant')
await loadEntitlementsSafe(ent, tenantId, true)
}
// roles guard (plural)
const allowedRoles = to.meta?.roles
if (Array.isArray(allowedRoles) && allowedRoles.length) {
if (!matchesRoles(allowedRoles, tenant.activeRole)) {
const fallback = roleToPath(tenant.activeRole)
if (to.path === fallback) { console.timeEnd(tlabel); return { path: '/pages/access' } }
console.timeEnd(tlabel)
return { path: fallback }
}
}
// role guard (singular) - mantém compatibilidade
const requiredRole = to.meta?.role
if (requiredRole && tenant.activeRole !== requiredRole) {
const fallback = roleToPath(tenant.activeRole)
if (to.path === fallback) { console.timeEnd(tlabel); return { path: '/pages/access' } }
console.timeEnd(tlabel)
return { path: fallback }
}
// feature guard
const requiredFeature = to.meta?.feature
if (requiredFeature && ent?.can && !ent.can(requiredFeature)) {
if (to.name === 'upgrade') { console.timeEnd(tlabel); return true }
const url = buildUpgradeUrl({
missingKeys: [requiredFeature],
redirectTo: to.fullPath
})
console.timeEnd(tlabel)
return { path: url }
}
console.timeEnd(tlabel)
return true
} catch (e) {
console.error('[guards] erro no beforeEach:', e)
// fallback seguro
if (to.meta?.public || to.path.startsWith('/auth')) return true
if (to.path === '/pages/access') return true
sessionStorage.setItem('redirect_after_login', to.fullPath)
return { path: '/auth/login' }
}
})
// auth listener (reset caches)
if (!window.__supabaseAuthListenerBound) {
window.__supabaseAuthListenerBound = true
supabase.auth.onAuthStateChange(() => {
sessionUidCache = null
saasAdminCacheUid = null
saasAdminCacheIsAdmin = null
})
}
}

View File

@@ -1,146 +1,111 @@
import AppLayout from '@/layout/AppLayout.vue';
import { createRouter, createWebHistory } from 'vue-router';
import {
createRouter,
createWebHistory,
isNavigationFailure,
NavigationFailureType
} from 'vue-router'
import publicRoutes from './routes.public'
import adminRoutes from './routes.admin'
import therapistRoutes from './routes.therapist'
import patientRoutes from './routes.patient'
import miscRoutes from './routes.misc'
import authRoutes from './routes.auth'
import configuracoesRoutes from './router.configuracoes'
import billingRoutes from './routes.billing'
import saasRoutes from './routes.saas'
import demoRoutes from './routes.demo'
import meRoutes from './router.me'
import { applyGuards } from './guards'
const routes = [
...(Array.isArray(publicRoutes) ? publicRoutes : [publicRoutes]),
...(Array.isArray(authRoutes) ? authRoutes : [authRoutes]),
...(Array.isArray(miscRoutes) ? miscRoutes : [miscRoutes]),
...(Array.isArray(billingRoutes) ? billingRoutes : [billingRoutes]),
...(Array.isArray(saasRoutes) ? saasRoutes : [saasRoutes]),
...(Array.isArray(meRoutes) ? meRoutes : [meRoutes]),
...(Array.isArray(adminRoutes) ? adminRoutes : [adminRoutes]),
...(Array.isArray(therapistRoutes) ? therapistRoutes : [therapistRoutes]),
...(Array.isArray(patientRoutes) ? patientRoutes : [patientRoutes]),
...(Array.isArray(demoRoutes) ? demoRoutes : [demoRoutes]),
...(Array.isArray(configuracoesRoutes) ? configuracoesRoutes : [configuracoesRoutes]),
{ path: '/:pathMatch(.*)*', name: 'notfound', component: () => import('@/views/pages/NotFound.vue') }
]
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: AppLayout,
children: [
{
path: '/',
name: 'dashboard',
component: () => import('@/views/Dashboard.vue')
},
{
path: '/uikit/formlayout',
name: 'formlayout',
component: () => import('@/views/uikit/FormLayout.vue')
},
{
path: '/uikit/input',
name: 'input',
component: () => import('@/views/uikit/InputDoc.vue')
},
{
path: '/uikit/button',
name: 'button',
component: () => import('@/views/uikit/ButtonDoc.vue')
},
{
path: '/uikit/table',
name: 'table',
component: () => import('@/views/uikit/TableDoc.vue')
},
{
path: '/uikit/list',
name: 'list',
component: () => import('@/views/uikit/ListDoc.vue')
},
{
path: '/uikit/tree',
name: 'tree',
component: () => import('@/views/uikit/TreeDoc.vue')
},
{
path: '/uikit/panel',
name: 'panel',
component: () => import('@/views/uikit/PanelsDoc.vue')
},
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// volta/avançar do navegador mantém posição
if (savedPosition) return savedPosition
{
path: '/uikit/overlay',
name: 'overlay',
component: () => import('@/views/uikit/OverlayDoc.vue')
},
{
path: '/uikit/media',
name: 'media',
component: () => import('@/views/uikit/MediaDoc.vue')
},
{
path: '/uikit/message',
name: 'message',
component: () => import('@/views/uikit/MessagesDoc.vue')
},
{
path: '/uikit/file',
name: 'file',
component: () => import('@/views/uikit/FileDoc.vue')
},
{
path: '/uikit/menu',
name: 'menu',
component: () => import('@/views/uikit/MenuDoc.vue')
},
{
path: '/uikit/charts',
name: 'charts',
component: () => import('@/views/uikit/ChartDoc.vue')
},
{
path: '/uikit/misc',
name: 'misc',
component: () => import('@/views/uikit/MiscDoc.vue')
},
{
path: '/uikit/timeline',
name: 'timeline',
component: () => import('@/views/uikit/TimelineDoc.vue')
},
{
path: '/blocks/free',
name: 'blocks',
meta: {
breadcrumb: ['Prime Blocks', 'Free Blocks']
},
component: () => import('@/views/utilities/Blocks.vue')
},
{
path: '/pages/empty',
name: 'empty',
component: () => import('@/views/pages/Empty.vue')
},
{
path: '/pages/crud',
name: 'crud',
component: () => import('@/views/pages/Crud.vue')
},
{
path: '/start/documentation',
name: 'documentation',
component: () => import('@/views/pages/Documentation.vue')
}
]
},
{
path: '/landing',
name: 'landing',
component: () => import('@/views/pages/Landing.vue')
},
{
path: '/pages/notfound',
name: 'notfound',
component: () => import('@/views/pages/NotFound.vue')
},
// qualquer navegação normal NÃO altera o scroll
return false
}
})
{
path: '/auth/login',
name: 'login',
component: () => import('@/views/pages/auth/Login.vue')
},
{
path: '/auth/access',
name: 'accessDenied',
component: () => import('@/views/pages/auth/Access.vue')
},
{
path: '/auth/error',
name: 'error',
component: () => import('@/views/pages/auth/Error.vue')
}
]
});
/* 🔎 DEBUG: listar todas as rotas registradas */
console.log(
'[ROUTES]',
router.getRoutes().map(r => r.path).sort()
)
export default router;
// ===== 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))
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 =====
// ✅ mantém seus guards, mas agora a landing tem meta.public
applyGuards(router)
export default router

View File

@@ -0,0 +1,36 @@
// src/router/router.configuracoes.js
import AppLayout from '@/layout/AppLayout.vue'
const configuracoesRoutes = {
path: '/configuracoes',
component: AppLayout,
meta: {
requiresAuth: true,
roles: ['admin', 'tenant_admin', 'therapist']
},
children: [
{
path: '',
component: () => import('@/layout/ConfiguracoesPage.vue'),
redirect: { name: 'ConfiguracoesAgenda' },
children: [
{
path: 'agenda',
name: 'ConfiguracoesAgenda',
component: () => import('@/layout/configuracoes/ConfiguracoesAgendaPage.vue')
}
// Futuro:
// { path: 'clinica', name: 'ConfiguracoesClinica', component: () => import('@/layout/configuracoes/ConfiguracoesClinicaPage.vue') },
// { path: 'intake', name: 'ConfiguracoesIntake', component: () => import('@/layout/configuracoes/ConfiguracoesIntakePage.vue') },
// { path: 'conta', name: 'ConfiguracoesConta', component: () => import('@/layout/configuracoes/ConfiguracoesContaPage.vue') },
]
}
]
}
export default configuracoesRoutes

32
src/router/router.me.js Normal file
View File

@@ -0,0 +1,32 @@
// src/router/router.me.js
import AppLayout from '@/layout/AppLayout.vue'
const meRoutes = {
path: '/me',
component: AppLayout,
meta: {
requiresAuth: true,
roles: ['admin', 'tenant_admin', 'therapist', 'patient']
},
children: [
{
// ✅ quando entrar em /me, manda pro perfil
path: '',
redirect: { name: 'MeuPerfil' }
},
{
path: 'perfil',
name: 'MeuPerfil',
component: () => import('@/views/pages/me/MeuPerfilPage.vue')
}
// Futuro:
// { path: 'preferencias', name: 'MePreferencias', component: () => import('@/pages/me/PreferenciasPage.vue') },
// { path: 'notificacoes', name: 'MeNotificacoes', component: () => import('@/pages/me/NotificacoesPage.vue') },
]
}
export default meRoutes

View File

@@ -0,0 +1,93 @@
// src/router/routes.admin.js
import AppLayout from '@/layout/AppLayout.vue'
export default {
path: '/admin',
component: AppLayout,
meta: {
// 🔐 Tudo aqui dentro exige login
requiresAuth: true,
// 👤 Perfil de acesso
role: 'tenant_admin'
},
children: [
// DASHBOARD
{
path: '',
name: 'admin-dashboard',
component: () => import('@/views/pages/admin/AdminDashboard.vue')
},
// PACIENTES - LISTA
{
path: 'pacientes',
name: 'admin-pacientes',
component: () => import('@/views/pages/admin/pacientes/PatientsIndexPage.vue')
},
// PACIENTES - CADASTRO (NOVO / EDITAR)
{
path: 'pacientes/cadastro',
name: 'admin-pacientes-cadastro',
component: () => import('@/views/pages/admin/pacientes/cadastro/PatientsCadastroPage.vue')
},
{
path: 'pacientes/cadastro/:id',
name: 'admin-pacientes-cadastro-edit',
component: () => import('@/views/pages/admin/pacientes/cadastro/PatientsCadastroPage.vue'),
props: true
},
// GRUPOS DE PACIENTES ✅
{
path: 'pacientes/grupos',
name: 'admin-pacientes-grupos',
component: () => import('@/views/pages/admin/pacientes/grupos/GruposPacientesPage.vue')
},
// TAGS
{
path: 'pacientes/tags',
name: 'admin-pacientes-tags',
component: () => import('@/views/pages/admin/pacientes/tags/TagsPage.vue')
},
// LINK EXTERNO
{
path: 'pacientes/link-externo',
name: 'admin.pacientes.linkexterno',
component: () => import('@/views/pages/admin/pacientes/cadastro/PatientsExternalLinkPage.vue')
},
// CADASTROS RECEBIDOS
{
path: 'pacientes/cadastro/recebidos',
name: 'admin.pacientes.recebidos',
component: () => import('@/views/pages/admin/pacientes/cadastro/recebidos/CadastrosRecebidosPage.vue')
},
// SEGURANÇA
{
path: 'settings/security',
name: 'admin-settings-security',
component: () => import('@/views/pages/auth/SecurityPage.vue')
},
// ================================
// 🔒 MÓDULO PRO — Online Scheduling
// ================================
// Admin também gerencia agendamento online; mesma feature de gestão.
// Você pode ter uma página admin para isso, ou reaproveitar a do therapist.
{
path: 'online-scheduling',
name: 'admin-online-scheduling',
component: () => import('@/views/pages/admin/OnlineSchedulingAdminPage.vue'),
meta: {
feature: 'online_scheduling.manage'
}
}
]
}

45
src/router/routes.auth.js Normal file
View File

@@ -0,0 +1,45 @@
export default {
path: '/auth',
children: [
{
path: 'login',
name: 'login',
component: () => import('@/views/pages/auth/Login.vue'),
meta: { public: true }
},
// ✅ Signup público, mas com URL /auth/signup
{
path: 'signup',
name: 'signup',
component: () => import('@/views/pages/public/Signup.vue'),
meta: { public: true }
},
{
path: 'welcome',
name: 'auth.welcome',
component: () => import('@/views/pages/auth/Welcome.vue'),
meta: { public: true }
},
{
path: 'reset-password',
name: 'resetPassword',
component: () => import('@/views/pages/auth/ResetPasswordPage.vue'),
meta: { public: true }
},
{
path: 'access',
name: 'accessDenied',
component: () => import('@/views/pages/auth/Access.vue'),
meta: { public: true }
},
{
path: 'error',
name: 'error',
component: () => import('@/views/pages/auth/Error.vue'),
meta: { public: true }
}
]
}

View File

@@ -0,0 +1,15 @@
// src/router/routes.billing.js
import AppLayout from '@/layout/AppLayout.vue'
export default {
path: '/upgrade',
component: AppLayout,
meta: { requiresAuth: true },
children: [
{
path: '',
name: 'upgrade',
component: () => import('@/views/pages/billing/UpgradePage.vue')
}
]
}

29
src/router/routes.demo.js Normal file
View File

@@ -0,0 +1,29 @@
import AppLayout from '@/layout/AppLayout.vue'
export default {
// ✅ não use '/' aqui (conflita com HomeCards)
path: '/demo',
component: AppLayout,
meta: { requiresAuth: true, role: 'tenant_admin' },
children: [
{ path: 'uikit/formlayout', name: 'uikit-formlayout', component: () => import('@/views/uikit/FormLayout.vue') },
{ path: 'uikit/input', name: 'uikit-input', component: () => import('@/views/uikit/InputDoc.vue') },
{ path: 'uikit/button', name: 'uikit-button', component: () => import('@/views/uikit/ButtonDoc.vue') },
{ path: 'uikit/table', name: 'uikit-table', component: () => import('@/views/uikit/TableDoc.vue') },
{ path: 'uikit/list', name: 'uikit-list', component: () => import('@/views/uikit/ListDoc.vue') },
{ path: 'uikit/tree', name: 'uikit-tree', component: () => import('@/views/uikit/TreeDoc.vue') },
{ path: 'uikit/panel', name: 'uikit-panel', component: () => import('@/views/uikit/PanelsDoc.vue') },
{ path: 'uikit/overlay', name: 'uikit-overlay', component: () => import('@/views/uikit/OverlayDoc.vue') },
{ path: 'uikit/media', name: 'uikit-media', component: () => import('@/views/uikit/MediaDoc.vue') },
{ path: 'uikit/menu', name: 'uikit-menu', component: () => import('@/views/uikit/MenuDoc.vue') },
{ path: 'uikit/message', name: 'uikit-message', component: () => import('@/views/uikit/MessagesDoc.vue') },
{ path: 'uikit/file', name: 'uikit-file', component: () => import('@/views/uikit/FileDoc.vue') },
{ path: 'uikit/charts', name: 'uikit-charts', component: () => import('@/views/uikit/ChartDoc.vue') },
{ path: 'uikit/timeline', name: 'uikit-timeline', component: () => import('@/views/uikit/TimelineDoc.vue') },
{ path: 'uikit/misc', name: 'uikit-misc', component: () => import('@/views/uikit/MiscDoc.vue') },
{ path: 'utilities', name: 'blocks', component: () => import('@/views/utilities/Blocks.vue') },
{ path: 'pages', name: 'start-documentation', component: () => import('@/views/pages/Documentation.vue') },
{ path: 'pages/empty', name: 'pages-empty', component: () => import('@/views/pages/Empty.vue') },
{ path: 'pages/crud', name: 'pages-crud', component: () => import('@/views/pages/Crud.vue') }
]
}

15
src/router/routes.misc.js Normal file
View File

@@ -0,0 +1,15 @@
export default {
path: '/',
children: [
{
path: 'landing',
name: 'landing',
component: () => import('@/views/pages/Landing.vue')
},
{
path: 'pages/notfound',
name: 'notfound',
component: () => import('@/views/pages/NotFound.vue')
}
]
}

View File

@@ -0,0 +1,19 @@
import AppLayout from '@/layout/AppLayout.vue'
export default {
path: '/patient',
component: AppLayout,
meta: { requiresAuth: true, role: 'patient' },
children: [
{
path: '',
name: 'patient-dashboard',
component: () => import('@/views/pages/patient/PatientDashboard.vue')
},
{
path: 'settings/security',
name: 'patient-settings-security',
component: () => import('@/views/pages/auth/SecurityPage.vue')
}
]
}

View File

@@ -0,0 +1,26 @@
export default {
path: '/',
children: [
{
path: '',
name: 'home',
component: () => import('@/views/pages/HomeCards.vue')
},
// ✅ LP (página separada da landing do template)
{
path: 'lp',
name: 'lp',
component: () => import('@/views/pages/public/landingpage-v1.vue'),
meta: { public: true }
},
// ✅ cadastro externo
{
path: 'cadastro/paciente',
name: 'public.patient.intake',
component: () => import('@/views/pages/public/CadastroPacienteExterno.vue'),
meta: { public: true }
}
]
}

60
src/router/routes.saas.js Normal file
View File

@@ -0,0 +1,60 @@
import AppLayout from '@/layout/AppLayout.vue'
export default {
path: '/saas',
component: AppLayout,
meta: { requiresAuth: true, saasAdmin: true },
children: [
{
path: '',
name: 'saas-dashboard',
component: () => import('@/views/pages/saas/SaasDashboard.vue')
},
{
path: 'plans',
name: 'saas-plans',
component: () => import('@/views/pages/saas/SaasPlansPage.vue')
},
{
path: 'plans-public',
name: 'saas-plans-public',
component: () => import('@/views/pages/saas/SaasPlansPublicPage.vue')
},
{
path: 'features',
name: 'saas-features',
component: () => import('@/views/pages/saas/SaasFeaturesPage.vue')
},
{
path: 'plan-features',
name: 'saas-plan-features',
component: () => import('@/views/pages/saas/SaasPlanFeaturesMatrixPage.vue')
},
{
path: 'subscriptions',
name: 'saas-subscriptions',
component: () => import('@/views/pages/saas/SaasSubscriptionsPage.vue')
},
{
path: 'subscription-events',
name: 'saas-subscription-events',
component: () => import('@/views/pages/saas/SaasSubscriptionEventsPage.vue')
},
{
path: 'subscription-health',
name: 'saas-subscription-health',
component: () => import('@/views/pages/saas/SaasSubscriptionHealthPage.vue')
},
{
path: 'subscription-intents',
name: 'saas.subscriptionIntents',
component: () => import('@/views/pages/saas/SubscriptionIntentsPage.vue'),
meta: { requiresAuth: true, saasAdmin: true }
},
{
path: 'tenants',
name: 'saas-tenants',
component: () => import('@/views/pages/saas/SaasPlaceholder.vue')
}
]
}

View File

@@ -0,0 +1,71 @@
import AppLayout from '@/layout/AppLayout.vue'
export default {
path: '/therapist',
component: AppLayout,
meta: {
// 🔐 Tudo aqui dentro exige login
requiresAuth: true,
// 👤 Perfil de acesso (seu guard atual usa meta.role)
role: 'therapist'
},
children: [
// ======================
// ✅ Dashboard Therapist
// ======================
{
path: '',
name: 'therapist-dashboard',
component: () => import('@/views/pages/therapist/TherapistDashboard.vue')
// herda requiresAuth + role do pai
},
// ======================
// ✅ Segurança
// ======================
{
path: 'settings/security',
name: 'therapist-settings-security',
component: () => import('@/views/pages/auth/SecurityPage.vue')
// herda requiresAuth + role do pai
},
// ==========================================
// 🔒 PRO — Online Scheduling (gestão interna)
// ==========================================
// feature gate via meta.feature:
// - bloqueia rota (guard)
// - menu pode desabilitar/ocultar (entitlementsStore.has)
{
path: 'online-scheduling',
name: 'therapist-online-scheduling',
component: () => import('@/views/pages/therapist/OnlineSchedulingPage.vue'),
meta: {
// ✅ herda requiresAuth + role do pai
feature: 'online_scheduling.manage'
}
},
// =================================================
// 🔒 PRO — Online Scheduling (página pública/config)
// =================================================
// Se você tiver/for criar a tela para configurar/visualizar a página pública,
// use a chave granular:
// - online_scheduling.public
//
// Dica de produto:
// - "manage" = operação interna
// - "public" = ajustes/preview/links
//
// Quando criar o arquivo, descomente.
// {
// path: 'online-scheduling/public',
// name: 'therapist-online-scheduling-public',
// component: () => import('@/views/pages/therapist/OnlineSchedulingPublicPage.vue'),
// meta: { feature: 'online_scheduling.public' }
// }
]
}