Ajuste em Massa - Paciente, Terapeuta, Clinica e Admin - Inicio agenda

This commit is contained in:
Leonardo
2026-02-22 17:56:01 -03:00
parent 6eff67bf22
commit 89b4ecaba1
77 changed files with 9433 additions and 1995 deletions

View File

@@ -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 {}
})
}
}
}

View File

@@ -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;

View File

@@ -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

View 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')
}
]
}

View File

@@ -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 {
}
}
]
}
}

View File

@@ -1,4 +1,4 @@
// src/router/router.configuracoes.js
// src/router/router.configs.js
import AppLayout from '@/layout/AppLayout.vue'
const configuracoesRoutes = {

View 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')
}
]
}

View File

@@ -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')
}
]
}

View 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')
}
]
}

View File

@@ -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 }
}
]
};

View File

@@ -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' }
// }
]
}
}