Ajuste em Massa - Paciente, Terapeuta, Clinica e Admin - Inicio agenda
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// src/router/guard.js
|
||||
// ⚠️ Guard depende de sessão estável.
|
||||
// Nunca disparar refresh concorrente durante navegação protegida.
|
||||
// Ver comentário em session.js sobre race condition.
|
||||
@@ -5,6 +6,7 @@
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||||
import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore'
|
||||
import { buildUpgradeUrl } from '@/utils/upgradeContext'
|
||||
|
||||
import { sessionUser, sessionReady, sessionRefreshing, initSession } from '@/app/session'
|
||||
@@ -16,11 +18,33 @@ let sessionUidCache = null
|
||||
let saasAdminCacheUid = null
|
||||
let saasAdminCacheIsAdmin = null
|
||||
|
||||
// -----------------------------------------
|
||||
// Pending invite (Modelo B) — retomada pós-login
|
||||
// -----------------------------------------
|
||||
const PENDING_INVITE_TOKEN_KEY = 'pending_invite_token_v1'
|
||||
|
||||
function readPendingInviteToken () {
|
||||
try { return sessionStorage.getItem(PENDING_INVITE_TOKEN_KEY) } catch (_) { return null }
|
||||
}
|
||||
|
||||
function clearPendingInviteToken () {
|
||||
try { sessionStorage.removeItem(PENDING_INVITE_TOKEN_KEY) } catch (_) {}
|
||||
}
|
||||
|
||||
function isUuid (v) {
|
||||
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(String(v || ''))
|
||||
}
|
||||
|
||||
function roleToPath (role) {
|
||||
if (role === 'tenant_admin') return '/admin'
|
||||
// ✅ clínica: aceita nomes canônicos e legado
|
||||
if (role === 'clinic_admin' || role === 'tenant_admin' || role === 'admin') return '/admin'
|
||||
|
||||
if (role === 'therapist') return '/therapist'
|
||||
if (role === 'patient') return '/patient'
|
||||
if (role === 'patient') return '/portal'
|
||||
|
||||
// ✅ saas master
|
||||
if (role === 'saas_admin') return '/saas'
|
||||
|
||||
return '/'
|
||||
}
|
||||
|
||||
@@ -103,12 +127,28 @@ export function applyGuards (router) {
|
||||
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 }
|
||||
// ==========================================
|
||||
// ✅ AUTH SEMPRE LIBERADO (blindagem total)
|
||||
// (ordem importa: /auth antes de meta.public)
|
||||
// ==========================================
|
||||
if (to.path.startsWith('/auth')) {
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ✅ Rotas públicas
|
||||
// ==========================================
|
||||
if (to.meta?.public) {
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
}
|
||||
|
||||
// se rota não exige auth, libera
|
||||
if (!to.meta?.requiresAuth) { console.timeEnd(tlabel); return true }
|
||||
if (!to.meta?.requiresAuth) {
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
}
|
||||
|
||||
// não decide nada no meio do refresh do session.js
|
||||
console.timeLog(tlabel, 'waitSessionIfRefreshing')
|
||||
@@ -122,6 +162,29 @@ export function applyGuards (router) {
|
||||
return { path: '/auth/login' }
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ✅ Pending invite (Modelo B): se o usuário logou e tem token pendente,
|
||||
// redireciona para /accept-invite antes de qualquer load pesado.
|
||||
// ==========================================
|
||||
const pendingInviteToken = readPendingInviteToken()
|
||||
|
||||
// Se tiver lixo no storage, limpa para não “travar” o app.
|
||||
if (pendingInviteToken && !isUuid(pendingInviteToken)) {
|
||||
clearPendingInviteToken()
|
||||
}
|
||||
|
||||
// Evita loop/efeito colateral:
|
||||
// - não interfere se já está em /accept-invite
|
||||
// (não precisamos checar /auth aqui porque /auth já retornou lá em cima)
|
||||
if (
|
||||
pendingInviteToken &&
|
||||
isUuid(pendingInviteToken) &&
|
||||
!to.path.startsWith('/accept-invite')
|
||||
) {
|
||||
console.timeEnd(tlabel)
|
||||
return { path: '/accept-invite', query: { token: pendingInviteToken } }
|
||||
}
|
||||
|
||||
// se uid mudou, invalida caches e stores dependentes
|
||||
if (sessionUidCache !== uid) {
|
||||
sessionUidCache = uid
|
||||
@@ -130,15 +193,27 @@ export function applyGuards (router) {
|
||||
|
||||
const ent0 = useEntitlementsStore()
|
||||
if (typeof ent0.invalidate === 'function') ent0.invalidate()
|
||||
|
||||
const tf0 = useTenantFeaturesStore()
|
||||
if (typeof tf0.invalidate === 'function') tf0.invalidate()
|
||||
}
|
||||
|
||||
// saas admin (com cache)
|
||||
// ================================
|
||||
// ✅ SAAS MASTER: não depende de tenant
|
||||
// ================================
|
||||
if (to.meta?.saasAdmin) {
|
||||
console.timeLog(tlabel, 'isSaasAdmin')
|
||||
const ok = await isSaasAdmin(uid)
|
||||
if (!ok) { console.timeEnd(tlabel); return { path: '/pages/access' } }
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
}
|
||||
|
||||
// ================================
|
||||
// ✅ Abaixo daqui é tudo tenant-app
|
||||
// ================================
|
||||
|
||||
// carrega tenant + role
|
||||
const tenant = useTenantStore()
|
||||
console.timeLog(tlabel, 'tenant.loadSessionAndTenant?')
|
||||
@@ -181,6 +256,12 @@ export function applyGuards (router) {
|
||||
return { path: '/pages/access' }
|
||||
}
|
||||
|
||||
// se trocar tenant, invalida cache de tenant_features (evita ler feature de tenant errado)
|
||||
const tfSwitch = useTenantFeaturesStore()
|
||||
if (tfSwitch.loadedForTenantId && tfSwitch.loadedForTenantId !== tenantId) {
|
||||
tfSwitch.invalidate()
|
||||
}
|
||||
|
||||
// entitlements (✅ carrega só quando precisa)
|
||||
const ent = useEntitlementsStore()
|
||||
if (shouldLoadEntitlements(ent, tenantId)) {
|
||||
@@ -188,6 +269,28 @@ export function applyGuards (router) {
|
||||
await loadEntitlementsSafe(ent, tenantId, true)
|
||||
}
|
||||
|
||||
// ================================
|
||||
// ✅ tenant_features (módulos ativáveis por clínica)
|
||||
// meta.tenantFeature = 'patients' | ...
|
||||
// ================================
|
||||
const requiredTenantFeature = to.meta?.tenantFeature
|
||||
if (requiredTenantFeature) {
|
||||
const tf = useTenantFeaturesStore()
|
||||
console.timeLog(tlabel, 'tenantFeatures.fetchForTenant')
|
||||
await tf.fetchForTenant(tenantId, { force: false })
|
||||
|
||||
if (!tf.isEnabled(requiredTenantFeature)) {
|
||||
// evita loop
|
||||
if (to.path === '/admin/clinic/features') { console.timeEnd(tlabel); return true }
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
return {
|
||||
path: '/admin/clinic/features',
|
||||
query: { missing: requiredTenantFeature, redirectTo: to.fullPath }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// roles guard (plural)
|
||||
const allowedRoles = to.meta?.roles
|
||||
if (Array.isArray(allowedRoles) && allowedRoles.length) {
|
||||
@@ -208,18 +311,18 @@ export function applyGuards (router) {
|
||||
return { path: fallback }
|
||||
}
|
||||
|
||||
// feature guard
|
||||
// feature guard (entitlements/plano → upgrade)
|
||||
const requiredFeature = to.meta?.feature
|
||||
if (requiredFeature && ent?.can && !ent.can(requiredFeature)) {
|
||||
if (to.name === 'upgrade') { console.timeEnd(tlabel); return true }
|
||||
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 }
|
||||
}
|
||||
const url = buildUpgradeUrl({
|
||||
missingKeys: [requiredFeature],
|
||||
redirectTo: to.fullPath
|
||||
})
|
||||
console.timeEnd(tlabel)
|
||||
return url
|
||||
}
|
||||
|
||||
console.timeEnd(tlabel)
|
||||
return true
|
||||
@@ -227,7 +330,8 @@ export function applyGuards (router) {
|
||||
console.error('[guards] erro no beforeEach:', e)
|
||||
|
||||
// fallback seguro
|
||||
if (to.meta?.public || to.path.startsWith('/auth')) return true
|
||||
if (to.path.startsWith('/auth')) return true
|
||||
if (to.meta?.public) return true
|
||||
if (to.path === '/pages/access') return true
|
||||
|
||||
sessionStorage.setItem('redirect_after_login', to.fullPath)
|
||||
@@ -243,6 +347,11 @@ export function applyGuards (router) {
|
||||
sessionUidCache = null
|
||||
saasAdminCacheUid = null
|
||||
saasAdminCacheIsAdmin = null
|
||||
|
||||
try {
|
||||
const tf = useTenantFeaturesStore()
|
||||
if (typeof tf.invalidate === 'function') tf.invalidate()
|
||||
} catch {}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,111 +1,120 @@
|
||||
import {
|
||||
createRouter,
|
||||
createWebHistory,
|
||||
isNavigationFailure,
|
||||
NavigationFailureType
|
||||
} 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 configuracoesRoutes from './routes.configs';
|
||||
import meRoutes from './routes.account';
|
||||
import adminRoutes from './routes.admin';
|
||||
import authRoutes from './routes.auth';
|
||||
import billingRoutes from './routes.billing';
|
||||
import demoRoutes from './routes.demo';
|
||||
import miscRoutes from './routes.misc';
|
||||
import patientRoutes from './routes.portal';
|
||||
import publicRoutes from './routes.public';
|
||||
import saasRoutes from './routes.saas';
|
||||
import therapistRoutes from './routes.therapist';
|
||||
import featuresRoutes from './routes.features'
|
||||
|
||||
import { applyGuards } from './guards'
|
||||
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]),
|
||||
...(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]),
|
||||
...(Array.isArray(featuresRoutes) ? featuresRoutes : [featuresRoutes]),
|
||||
|
||||
// ✅ compat: rota antiga /login → /auth/login (evita 404 se algum trecho legado usar /login)
|
||||
{
|
||||
path: '/login',
|
||||
redirect: (to) => ({
|
||||
path: '/auth/login',
|
||||
query: to.query || {}
|
||||
})
|
||||
},
|
||||
|
||||
{ path: '/:pathMatch(.*)*', name: 'notfound', component: () => import('@/views/pages/NotFound.vue') }
|
||||
]
|
||||
{ path: '/:pathMatch(.*)*', name: 'notfound', component: () => import('@/views/pages/NotFound.vue') }
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// volta/avançar do navegador mantém posição
|
||||
if (savedPosition) return savedPosition
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// volta/avançar do navegador mantém posição
|
||||
if (savedPosition) return savedPosition;
|
||||
|
||||
// qualquer navegação normal NÃO altera o scroll
|
||||
return false
|
||||
}
|
||||
})
|
||||
// qualquer navegação normal NÃO altera o scroll
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
/* 🔎 DEBUG: listar todas as rotas registradas */
|
||||
console.log(
|
||||
'[ROUTES]',
|
||||
router.getRoutes().map(r => r.path).sort()
|
||||
)
|
||||
'[ROUTES]',
|
||||
router
|
||||
.getRoutes()
|
||||
.map((r) => r.path)
|
||||
.sort()
|
||||
);
|
||||
|
||||
// ===== DEBUG NAV + TRACE (remover depois) =====
|
||||
const _push = router.push.bind(router)
|
||||
const _push = router.push.bind(router);
|
||||
router.push = async (loc) => {
|
||||
console.log('[router.push]', loc)
|
||||
console.trace('[push caller]')
|
||||
console.log('[router.push]', loc);
|
||||
console.trace('[push caller]');
|
||||
|
||||
const res = await _push(loc)
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
const _replace = router.replace.bind(router)
|
||||
const _replace = router.replace.bind(router);
|
||||
router.replace = async (loc) => {
|
||||
console.log('[router.replace]', loc)
|
||||
console.trace('[replace caller]')
|
||||
console.log('[router.replace]', loc);
|
||||
console.trace('[replace caller]');
|
||||
|
||||
const res = await _replace(loc)
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
router.onError((e) => console.error('[router.onError]', e))
|
||||
router.onError((e) => console.error('[router.onError]', e));
|
||||
|
||||
router.beforeEach((to, from) => {
|
||||
console.log('[beforeEach]', from.fullPath, '->', to.fullPath)
|
||||
return true
|
||||
})
|
||||
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)
|
||||
})
|
||||
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)
|
||||
applyGuards(router);
|
||||
|
||||
export default router
|
||||
export default router;
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
// 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
|
||||
23
src/router/routes.account.js
Normal file
23
src/router/routes.account.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
export default {
|
||||
path: '/account',
|
||||
component: AppLayout,
|
||||
meta: { requiresAuth: true },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirect: { name: 'account-profile' }
|
||||
},
|
||||
{
|
||||
path: 'profile',
|
||||
name: 'account-profile',
|
||||
component: () => import('@/views/pages/account/ProfilePage.vue')
|
||||
},
|
||||
{
|
||||
path: 'security',
|
||||
name: 'account-security',
|
||||
component: () => import('@/views/pages/auth/SecurityPage.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -9,78 +9,165 @@ export default {
|
||||
// 🔐 Tudo aqui dentro exige login
|
||||
requiresAuth: true,
|
||||
|
||||
// 👤 Perfil de acesso
|
||||
role: 'tenant_admin'
|
||||
// 👤 Perfil de acesso (tenant-level)
|
||||
// tenantStore normaliza tenant_admin -> clinic_admin, mas mantemos compatibilidade
|
||||
roles: ['clinic_admin', 'tenant_admin']
|
||||
},
|
||||
|
||||
children: [
|
||||
// DASHBOARD
|
||||
// ======================================================
|
||||
// 📊 DASHBOARD
|
||||
// ======================================================
|
||||
{
|
||||
path: '',
|
||||
name: 'admin-dashboard',
|
||||
component: () => import('@/views/pages/admin/AdminDashboard.vue')
|
||||
},
|
||||
|
||||
// PACIENTES - LISTA
|
||||
// ======================================================
|
||||
// 🧩 CLÍNICA — MÓDULOS (tenant_features)
|
||||
// ======================================================
|
||||
{
|
||||
path: 'clinic/features',
|
||||
name: 'admin-clinic-features',
|
||||
component: () => import('@/views/pages/admin/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/admin/clinic/ClinicProfessionalsPage.vue'),
|
||||
meta: {
|
||||
requiresAuth: true,
|
||||
roles: ['clinic_admin', 'tenant_admin']
|
||||
}
|
||||
},
|
||||
|
||||
// ======================================================
|
||||
// 📅 MINHA AGENDA
|
||||
// ======================================================
|
||||
|
||||
// 🔎 Visão geral da agenda
|
||||
{
|
||||
path: 'agenda',
|
||||
name: 'admin-agenda',
|
||||
component: () => import('@/views/pages/admin/agenda/MyAppointmentsPage.vue'),
|
||||
meta: {
|
||||
feature: 'agenda.view'
|
||||
}
|
||||
},
|
||||
|
||||
// ➕ Adicionar novo compromisso
|
||||
{
|
||||
path: 'agenda/adicionar',
|
||||
name: 'admin-agenda-adicionar',
|
||||
component: () => import('@/views/pages/admin/agenda/NewAppointmentPage.vue'),
|
||||
meta: {
|
||||
feature: 'agenda.manage'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: 'agenda/clinica',
|
||||
name: 'admin-agenda-clinica',
|
||||
component: () => import('@/features/agenda/pages/AgendaClinicaPage.vue'),
|
||||
meta: {
|
||||
feature: 'agenda.view',
|
||||
roles: ['clinic_admin', 'tenant_admin']
|
||||
}
|
||||
},
|
||||
|
||||
// ======================================================
|
||||
// 👥 PACIENTES (módulo ativável por clínica)
|
||||
// ======================================================
|
||||
|
||||
// 📋 Lista de pacientes
|
||||
{
|
||||
path: 'pacientes',
|
||||
name: 'admin-pacientes',
|
||||
component: () => import('@/views/pages/admin/pacientes/PatientsIndexPage.vue')
|
||||
component: () => import('@/features/patients/PatientsListPage.vue'),
|
||||
meta: {
|
||||
// ✅ depende do tenant_features.patients
|
||||
tenantFeature: 'patients'
|
||||
}
|
||||
},
|
||||
|
||||
// PACIENTES - CADASTRO (NOVO / EDITAR)
|
||||
// ➕ Cadastro de paciente (novo)
|
||||
{
|
||||
path: 'pacientes/cadastro',
|
||||
name: 'admin-pacientes-cadastro',
|
||||
component: () => import('@/views/pages/admin/pacientes/cadastro/PatientsCadastroPage.vue')
|
||||
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
|
||||
meta: {
|
||||
tenantFeature: 'patients'
|
||||
}
|
||||
},
|
||||
|
||||
// ✏️ Editar paciente
|
||||
{
|
||||
path: 'pacientes/cadastro/:id',
|
||||
name: 'admin-pacientes-cadastro-edit',
|
||||
component: () => import('@/views/pages/admin/pacientes/cadastro/PatientsCadastroPage.vue'),
|
||||
props: true
|
||||
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue'),
|
||||
props: true,
|
||||
meta: {
|
||||
tenantFeature: 'patients'
|
||||
}
|
||||
},
|
||||
|
||||
// GRUPOS DE PACIENTES ✅
|
||||
// 👥 Grupos de pacientes
|
||||
{
|
||||
path: 'pacientes/grupos',
|
||||
name: 'admin-pacientes-grupos',
|
||||
component: () => import('@/views/pages/admin/pacientes/grupos/GruposPacientesPage.vue')
|
||||
component: () => import('@/features/patients/grupos/GruposPacientesPage.vue'),
|
||||
meta: {
|
||||
tenantFeature: 'patients'
|
||||
}
|
||||
},
|
||||
|
||||
// TAGS
|
||||
// 🏷️ Tags de pacientes
|
||||
{
|
||||
path: 'pacientes/tags',
|
||||
name: 'admin-pacientes-tags',
|
||||
component: () => import('@/views/pages/admin/pacientes/tags/TagsPage.vue')
|
||||
component: () => import('@/features/patients/tags/TagsPage.vue'),
|
||||
meta: {
|
||||
tenantFeature: 'patients'
|
||||
}
|
||||
},
|
||||
|
||||
// LINK EXTERNO
|
||||
// 🔗 Link externo para cadastro
|
||||
{
|
||||
path: 'pacientes/link-externo',
|
||||
name: 'admin.pacientes.linkexterno',
|
||||
component: () => import('@/views/pages/admin/pacientes/cadastro/PatientsExternalLinkPage.vue')
|
||||
name: 'admin-pacientes-link-externo',
|
||||
component: () => import('@/features/patients/cadastro/PatientsExternalLinkPage.vue'),
|
||||
meta: {
|
||||
tenantFeature: 'patients'
|
||||
}
|
||||
},
|
||||
|
||||
// CADASTROS RECEBIDOS
|
||||
// 📥 Cadastros recebidos via link externo
|
||||
{
|
||||
path: 'pacientes/cadastro/recebidos',
|
||||
name: 'admin.pacientes.recebidos',
|
||||
component: () => import('@/views/pages/admin/pacientes/cadastro/recebidos/CadastrosRecebidosPage.vue')
|
||||
name: 'admin-pacientes-recebidos',
|
||||
component: () => import('@/features/patients/cadastro/recebidos/CadastrosRecebidosPage.vue'),
|
||||
meta: {
|
||||
tenantFeature: 'patients'
|
||||
}
|
||||
},
|
||||
|
||||
// SEGURANÇA
|
||||
// ======================================================
|
||||
// 🔐 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',
|
||||
@@ -90,4 +177,4 @@ export default {
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// src/router/router.configuracoes.js
|
||||
// src/router/router.configs.js
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
const configuracoesRoutes = {
|
||||
26
src/router/routes.features.js
Normal file
26
src/router/routes.features.js
Normal file
@@ -0,0 +1,26 @@
|
||||
// src/router/routes.features.js
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
export default {
|
||||
path: '/features',
|
||||
component: AppLayout,
|
||||
meta: { requiresAuth: true }, // roles: se você quiser travar aqui também
|
||||
children: [
|
||||
// Patients
|
||||
{
|
||||
path: 'patients',
|
||||
name: 'features.patients.list',
|
||||
component: () => import('@/features/patients/PatientsListPage.vue') // ajuste se seu arquivo tiver outro nome
|
||||
},
|
||||
{
|
||||
path: 'patients/cadastro',
|
||||
name: 'features.patients.create',
|
||||
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue')
|
||||
},
|
||||
{
|
||||
path: 'patients/cadastro/:id',
|
||||
name: 'features.patients.edit',
|
||||
component: () => import('@/features/patients/cadastro/PatientsCadastroPage.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
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')
|
||||
}
|
||||
]
|
||||
}
|
||||
27
src/router/routes.portal.js
Normal file
27
src/router/routes.portal.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// src/router/router.portal.js
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
export default {
|
||||
path: '/portal',
|
||||
component: AppLayout,
|
||||
meta: { requiresAuth: true, roles: ['patient'] },
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
name: 'portal-dashboard',
|
||||
component: () => import('@/views/pages/portal/PortalDashboard.vue')
|
||||
},
|
||||
|
||||
// ✅ Appointments (era agenda)
|
||||
{
|
||||
path: 'agenda',
|
||||
name: 'portal-agenda',
|
||||
component: () => import('@/views/pages/portal/agenda/MyAppointmentsPage.vue')
|
||||
},
|
||||
{
|
||||
path: 'agenda/new',
|
||||
name: 'portal-agenda-new',
|
||||
component: () => import('@/views/pages/portal/agenda/NewAppointmentPage.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -21,6 +21,13 @@ export default {
|
||||
name: 'public.patient.intake',
|
||||
component: () => import('@/views/pages/public/CadastroPacienteExterno.vue'),
|
||||
meta: { public: true }
|
||||
}
|
||||
},
|
||||
// ✅ convite de clinicas
|
||||
{
|
||||
path: '/accept-invite',
|
||||
name: 'accept-invite',
|
||||
component: () => import('@/views/pages/public/AcceptInvitePage.vue'),
|
||||
meta: { public: true }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// src/router/routes.therapist.js
|
||||
import AppLayout from '@/layout/AppLayout.vue'
|
||||
|
||||
export default {
|
||||
@@ -8,34 +9,99 @@ export default {
|
||||
// 🔐 Tudo aqui dentro exige login
|
||||
requiresAuth: true,
|
||||
|
||||
// 👤 Perfil de acesso (seu guard atual usa meta.role)
|
||||
role: 'therapist'
|
||||
// 👤 Perfil de acesso (tenant-level)
|
||||
roles: ['therapist']
|
||||
},
|
||||
|
||||
children: [
|
||||
// ======================
|
||||
// ✅ Dashboard Therapist
|
||||
// ======================
|
||||
// ======================================================
|
||||
// 📊 DASHBOARD
|
||||
// ======================================================
|
||||
{
|
||||
path: '',
|
||||
name: 'therapist-dashboard',
|
||||
component: () => import('@/views/pages/therapist/TherapistDashboard.vue')
|
||||
// herda requiresAuth + role do pai
|
||||
// herda requiresAuth + roles do pai
|
||||
},
|
||||
|
||||
// ======================
|
||||
// ✅ Segurança
|
||||
// ======================
|
||||
// ======================================================
|
||||
// 📅 AGENDA
|
||||
// ======================================================
|
||||
{
|
||||
path: 'settings/security',
|
||||
name: 'therapist-settings-security',
|
||||
component: () => import('@/views/pages/auth/SecurityPage.vue')
|
||||
// herda requiresAuth + role do pai
|
||||
path: 'agenda',
|
||||
name: 'therapist-agenda',
|
||||
//component: () => import('@/views/pages/therapist/agenda/MyAppointmentsPage.vue'),
|
||||
component: () => import('@/features/agenda/pages/AgendaTerapeutaPage.vue'),
|
||||
meta: {
|
||||
feature: 'agenda.view'
|
||||
}
|
||||
},
|
||||
|
||||
// ==========================================
|
||||
{
|
||||
path: 'agenda/adicionar',
|
||||
name: 'therapist-agenda-adicionar',
|
||||
component: () => import('@/views/pages/therapist/agenda/NewAppointmentPage.vue'),
|
||||
meta: {
|
||||
feature: 'agenda.manage'
|
||||
}
|
||||
},
|
||||
|
||||
// ======================================================
|
||||
// 👥 PATIENTS
|
||||
// ======================================================
|
||||
|
||||
{
|
||||
path: 'patients',
|
||||
name: 'therapist-patients',
|
||||
component: () => import('@/features/patients/PatientsListPage.vue')
|
||||
},
|
||||
|
||||
// ➕ Create patient
|
||||
{
|
||||
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
|
||||
},
|
||||
|
||||
// 👥 Groups
|
||||
{
|
||||
path: 'patients/grupos',
|
||||
name: 'therapist-patients-groups',
|
||||
component: () => import('@/features/patients/grupos/GruposPacientesPage.vue')
|
||||
},
|
||||
|
||||
// 🏷️ Tags
|
||||
{
|
||||
path: 'patients/tags',
|
||||
name: 'therapist-patients-tags',
|
||||
component: () => import('@/features/patients/tags/TagsPage.vue')
|
||||
},
|
||||
|
||||
// 🔗 External Link
|
||||
{
|
||||
path: 'patients/link-externo',
|
||||
name: 'therapist-patients-link-externo',
|
||||
component: () => import('@/features/patients/cadastro/PatientsExternalLinkPage.vue')
|
||||
},
|
||||
|
||||
// 📥 Received Registrations
|
||||
{
|
||||
path: 'patients/cadastro/recebidos',
|
||||
name: 'therapist-patients-recebidos',
|
||||
component: () =>
|
||||
import('@/features/patients/cadastro/recebidos/CadastrosRecebidosPage.vue')
|
||||
},
|
||||
|
||||
// ======================================================
|
||||
// 🔒 PRO — Online Scheduling (gestão interna)
|
||||
// ==========================================
|
||||
// ======================================================
|
||||
// feature gate via meta.feature:
|
||||
// - bloqueia rota (guard)
|
||||
// - menu pode desabilitar/ocultar (entitlementsStore.has)
|
||||
@@ -44,23 +110,23 @@ export default {
|
||||
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.
|
||||
// ======================================================
|
||||
// 🔐 SECURITY (temporário dentro da área)
|
||||
// ======================================================
|
||||
// ⚠️ Idealmente mover para /account/security (área global)
|
||||
{
|
||||
path: 'settings/security',
|
||||
name: 'therapist-settings-security',
|
||||
component: () => import('@/views/pages/auth/SecurityPage.vue')
|
||||
}
|
||||
|
||||
// ======================================================
|
||||
// 🔒 PRO — Online Scheduling (configuração pública)
|
||||
// ======================================================
|
||||
// {
|
||||
// path: 'online-scheduling/public',
|
||||
// name: 'therapist-online-scheduling-public',
|
||||
@@ -68,4 +134,4 @@ export default {
|
||||
// meta: { feature: 'online_scheduling.public' }
|
||||
// }
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user