Agenda, Agendador, Configurações

This commit is contained in:
Leonardo
2026-03-12 08:58:36 -03:00
parent f733db8436
commit f4b185ae17
197 changed files with 33405 additions and 6507 deletions

View File

@@ -8,6 +8,7 @@ import { useTenantStore } from '@/stores/tenantStore'
import { useEntitlementsStore } from '@/stores/entitlementsStore'
import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore'
import { buildUpgradeUrl } from '@/utils/upgradeContext'
import { logGuard, logError, logPerf } from '@/support/supportLogger'
import { useMenuStore } from '@/stores/menuStore'
import { getMenuByRole } from '@/navigation'
@@ -24,6 +25,10 @@ let sessionUidCache = null
let saasAdminCacheUid = null
let saasAdminCacheIsAdmin = null
// cache de globalRole por uid (evita query ao banco em cada navegação)
let globalRoleCacheUid = null
let globalRoleCache = null
// -----------------------------------------
// Pending invite (Modelo B) — retomada pós-login
// -----------------------------------------
@@ -94,7 +99,7 @@ function sleep (ms) {
async function waitSessionIfRefreshing () {
if (!sessionReady.value) {
try { await initSession({ initial: true }) } catch (e) {
console.warn('[guards] initSession falhou:', e)
logGuard('[guards] initSession falhou', { error: e?.message })
}
}
@@ -146,7 +151,7 @@ async function loadEntitlementsSafe (ent, tenantId, 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)
logGuard('[guards] ent.loadForTenant force fallback', { error: e?.message })
await ent.loadForTenant(tenantId, { force: true })
return
}
@@ -163,7 +168,7 @@ async function fetchTenantFeaturesSafe (tf, tenantId) {
try {
await tf.fetchForTenant(tenantId, { force: false })
} catch (e) {
console.warn('[guards] tf.fetchForTenant(force:false) falhou, tentando force:true', e)
logGuard('[guards] tf.fetchForTenant force fallback', { error: e?.message })
await tf.fetchForTenant(tenantId, { force: true })
}
}
@@ -234,9 +239,7 @@ async function ensureMenuBuilt ({ uid, tenantId, tenantRole, globalRole }) {
}
// cache com key igual mas menu errado: força rebuild
console.warn('[ensureMenuBuilt] key match mas menu incompatível com role, forçando rebuild:', {
key, safeRole, firstLabel
})
logGuard('[ensureMenuBuilt] menu incompatível com role, forçando rebuild', { key, safeRole, firstLabel })
menuStore.reset()
}
@@ -267,7 +270,7 @@ async function ensureMenuBuilt ({ uid, tenantId, tenantRole, globalRole }) {
const model = getMenuByRole(roleForMenu, ctx) || []
menuStore.setMenu(key, model)
} catch (e) {
console.warn('[guards] ensureMenuBuilt failed:', e)
logGuard('[guards] ensureMenuBuilt failed', { error: e?.message })
}
}
@@ -277,7 +280,7 @@ export function applyGuards (router) {
router.beforeEach(async (to) => {
const tlabel = `[guard] ${to.fullPath}`
console.time(tlabel)
const _perfEnd = logPerf('router.guard', tlabel)
try {
// ==========================================
@@ -285,7 +288,7 @@ export function applyGuards (router) {
// (ordem importa: /auth antes de meta.public)
// ==========================================
if (to.path.startsWith('/auth')) {
console.timeEnd(tlabel)
_perfEnd()
return true
}
@@ -293,25 +296,25 @@ export function applyGuards (router) {
// ✅ Rotas públicas
// ==========================================
if (to.meta?.public) {
console.timeEnd(tlabel)
_perfEnd()
return true
}
// se rota não exige auth, libera
if (!to.meta?.requiresAuth) {
console.timeEnd(tlabel)
_perfEnd()
return true
}
// não decide nada no meio do refresh do session.js
console.timeLog(tlabel, 'waitSessionIfRefreshing')
logGuard('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)
_perfEnd()
return { path: '/auth/login' }
}
@@ -321,21 +324,32 @@ export function applyGuards (router) {
to.path.startsWith('/supervisor')
// ======================================
// ✅ IDENTIDADE GLOBAL (1x por navegação)
// ✅ IDENTIDADE GLOBAL (cached por uid — sem query a cada navegação)
// - se falhar, NÃO nega por engano: volta pro login (seguro)
// ======================================
const { data: prof, error: profErr } = await supabase
.from('profiles')
.select('role')
.eq('id', uid)
.single()
let globalRole = null
const globalRole = !profErr ? prof?.role : null
console.timeLog(tlabel, 'profiles.role =', globalRole)
if (globalRoleCacheUid === uid && globalRoleCache) {
globalRole = globalRoleCache
logGuard('profiles.role (cache) =', globalRole)
} else {
const { data: prof, error: profErr } = await supabase
.from('profiles')
.select('role')
.eq('id', uid)
.single()
globalRole = !profErr ? prof?.role : null
if (globalRole) {
globalRoleCacheUid = uid
globalRoleCache = globalRole
}
logGuard('profiles.role (db) =', globalRole)
}
if (!globalRole) {
sessionStorage.setItem('redirect_after_login', to.fullPath)
console.timeEnd(tlabel)
_perfEnd()
return { path: '/auth/login' }
}
@@ -350,7 +364,7 @@ export function applyGuards (router) {
localStorage.removeItem('currentTenantId')
} catch (_) {}
console.timeEnd(tlabel)
_perfEnd()
return { path: '/portal' }
}
@@ -359,7 +373,7 @@ export function applyGuards (router) {
// ======================================
if (to.meta?.profileRole) {
if (globalRole !== to.meta.profileRole) {
console.timeEnd(tlabel)
_perfEnd()
return { path: '/pages/access' }
}
@@ -371,7 +385,7 @@ export function applyGuards (router) {
globalRole
})
console.timeEnd(tlabel)
_perfEnd()
return true
}
@@ -382,7 +396,7 @@ export function applyGuards (router) {
// ======================================
const isAccountArea = (to.path === '/account' || to.path.startsWith('/account/'))
if (isAccountArea) {
console.timeEnd(tlabel)
_perfEnd()
return true
}
@@ -410,7 +424,7 @@ export function applyGuards (router) {
isUuid(pendingInviteToken) &&
!to.path.startsWith('/accept-invite')
) {
console.timeEnd(tlabel)
_perfEnd()
return { path: '/accept-invite', query: { token: pendingInviteToken } }
}
@@ -419,6 +433,8 @@ export function applyGuards (router) {
sessionUidCache = uid
saasAdminCacheUid = null
saasAdminCacheIsAdmin = null
globalRoleCacheUid = null
globalRoleCache = null
const ent0 = useEntitlementsStore()
if (typeof ent0.invalidate === 'function') ent0.invalidate()
@@ -436,10 +452,10 @@ export function applyGuards (router) {
// ✅ SAAS MASTER: não depende de tenant
// ================================
if (to.meta?.saasAdmin) {
console.timeLog(tlabel, 'isSaasAdmin')
logGuard('isSaasAdmin')
// usa identidade global primeiro (evita cache fantasma)
const ok = (globalRole === 'saas_admin') ? true : await isSaasAdmin(uid)
if (!ok) { console.timeEnd(tlabel); return { path: '/pages/access' } }
if (!ok) { _perfEnd(); return { path: '/pages/access' } }
// ✅ monta menu SaaS 1x (AppMenu lê do menuStore)
await ensureMenuBuilt({
@@ -449,7 +465,7 @@ export function applyGuards (router) {
globalRole
})
console.timeEnd(tlabel)
_perfEnd()
return true
}
@@ -472,7 +488,7 @@ export function applyGuards (router) {
}
if (!platformRoles.includes('editor')) {
console.timeEnd(tlabel)
_perfEnd()
return { path: '/pages/access' }
}
@@ -483,7 +499,7 @@ export function applyGuards (router) {
globalRole
})
console.timeEnd(tlabel)
_perfEnd()
return true
}
@@ -491,7 +507,7 @@ export function applyGuards (router) {
// 🚫 SaaS master: bloqueia tenant-app por padrão
// ✅ Mas libera rotas de DEMO em DEV (Sakai)
// ================================
console.timeLog(tlabel, 'saas.lockdown?')
logGuard('saas.lockdown?')
// 🔥 PATCH: aqui NÃO consulta isSaasAdmin — usa só identidade global
const isSaas = (globalRole === 'saas_admin')
@@ -515,13 +531,13 @@ export function applyGuards (router) {
globalRole
})
console.timeEnd(tlabel)
_perfEnd()
return true
}
// Fora de /saas (e não-demo), não pode
if (!isSaasArea) {
console.timeEnd(tlabel)
_perfEnd()
return { path: '/saas' }
}
@@ -540,7 +556,7 @@ export function applyGuards (router) {
// carrega tenant + role
const tenant = useTenantStore()
console.timeLog(tlabel, 'tenant.loadSessionAndTenant?')
logGuard('tenant.loadSessionAndTenant?')
if (!tenant.loaded && !tenant.loading) {
await tenant.loadSessionAndTenant()
}
@@ -548,7 +564,7 @@ export function applyGuards (router) {
// 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)
_perfEnd()
return { path: '/auth/login' }
}
@@ -576,12 +592,12 @@ export function applyGuards (router) {
// 🔥 race/sem vínculo: em área tenant, trate como login necessário (não AccessDenied)
if (isTenantArea) {
sessionStorage.setItem('redirect_after_login', to.fullPath)
console.timeEnd(tlabel)
_perfEnd()
return { path: '/auth/login' }
}
if (to.path === '/pages/access') { console.timeEnd(tlabel); return true }
console.timeEnd(tlabel)
if (to.path === '/pages/access') { _perfEnd(); return true }
_perfEnd()
return { path: '/pages/access' }
}
@@ -596,14 +612,14 @@ export function applyGuards (router) {
// 🔥 FIX: se ainda assim não resolveu tenant/role e estamos em tenant area, não negue “por engano”
if (isTenantArea && (!tenant.activeTenantId || !tenant.activeRole)) {
sessionStorage.setItem('redirect_after_login', to.fullPath)
console.timeEnd(tlabel)
_perfEnd()
return { path: '/auth/login' }
}
let tenantId = tenant.activeTenantId
if (!tenantId) {
if (to.path === '/pages/access') { console.timeEnd(tlabel); return true }
console.timeEnd(tlabel)
if (to.path === '/pages/access') { _perfEnd(); return true }
_perfEnd()
return { path: '/pages/access' }
}
@@ -630,7 +646,7 @@ export function applyGuards (router) {
const desiredTenantId = desired?.tenant_id || null
if (desiredTenantId && tenant.activeTenantId !== desiredTenantId) {
console.timeLog(tlabel, `tenantScope.switch(${scope})`)
logGuard(`tenantScope.switch(${scope})`)
// ✅ guarda o tenant antigo para invalidar APENAS ele
const oldTenantId = tenant.activeTenantId
@@ -661,13 +677,9 @@ export function applyGuards (router) {
if (typeof menuStore.reset === 'function') menuStore.reset()
} catch {}
} else if (!desiredTenantId) {
console.warn('[guards] tenantScope sem match:', scope, {
memberships: mem.map(x => ({
tenant_id: x?.tenant_id,
role: x?.role,
kind: x?.kind,
status: x?.status
}))
logGuard('[guards] tenantScope sem match', {
scope,
memberships: mem.map(x => ({ tenant_id: x?.tenant_id, role: x?.role, kind: x?.kind, status: x?.status }))
})
}
}
@@ -682,7 +694,7 @@ export function applyGuards (router) {
// entitlements (✅ carrega só quando precisa)
const ent = useEntitlementsStore()
if (shouldLoadEntitlements(ent, tenantId)) {
console.timeLog(tlabel, 'ent.loadForTenant')
logGuard('ent.loadForTenant')
await loadEntitlementsSafe(ent, tenantId, true)
}
@@ -691,9 +703,9 @@ export function applyGuards (router) {
// user entitlements: therapist e supervisor têm assinatura pessoal (v_user_entitlements)
const activeRoleNormForEnt = normalizeRole(tenant.activeRole)
if (['therapist', 'supervisor'].includes(activeRoleNormForEnt) && uid && ent.loadedForUser !== uid) {
console.timeLog(tlabel, 'ent.loadForUser')
logGuard('ent.loadForUser')
try { await ent.loadForUser(uid) } catch (e) {
console.warn('[guards] ent.loadForUser failed:', e)
logGuard('[guards] ent.loadForUser failed', { error: e?.message })
}
}
@@ -704,7 +716,7 @@ export function applyGuards (router) {
const requiredTenantFeature = to.meta?.tenantFeature
if (requiredTenantFeature) {
const tf = useTenantFeaturesStore()
console.timeLog(tlabel, 'tenantFeatures.fetchForTenant')
logGuard('tenantFeatures.fetchForTenant')
await fetchTenantFeaturesSafe(tf, tenantId)
// ✅ IMPORTANTÍSSIMO: passa tenantId
@@ -713,9 +725,9 @@ export function applyGuards (router) {
: false
if (!enabled) {
if (to.path === '/admin/clinic/features') { console.timeEnd(tlabel); return true }
if (to.path === '/admin/clinic/features') { _perfEnd(); return true }
console.timeEnd(tlabel)
_perfEnd()
return {
path: '/admin/clinic/features',
query: { missing: requiredTenantFeature, redirectTo: to.fullPath }
@@ -746,7 +758,7 @@ export function applyGuards (router) {
if (compatible) {
tenant.activeRole = normalizeRole(compatible.role, compatible.kind)
} else {
console.timeEnd(tlabel)
_perfEnd()
return denyByRole({ to, currentRole: tenant.activeRole })
}
}
@@ -756,7 +768,7 @@ export function applyGuards (router) {
const requiredRole = requiredRoleRaw ? normalizeRole(requiredRoleRaw) : null
if (requiredRole && normalizeRole(tenant.activeRole) !== requiredRole) {
console.timeEnd(tlabel)
_perfEnd()
return denyByRole({ to, currentRole: tenant.activeRole })
}
@@ -765,7 +777,7 @@ export function applyGuards (router) {
// ------------------------------------------------
const requiredFeature = to.meta?.feature
if (requiredFeature && ent?.can && !ent.can(requiredFeature)) {
if (to.name === 'upgrade') { console.timeEnd(tlabel); return true }
if (to.name === 'upgrade') { _perfEnd(); return true }
const url = buildUpgradeUrl({
missingKeys: [requiredFeature],
@@ -773,7 +785,7 @@ export function applyGuards (router) {
role: normalizeRole(tenant.activeRole) // ✅ passa o role para a UpgradePage detectar
})
console.timeEnd(tlabel)
_perfEnd()
return url
}
@@ -787,10 +799,10 @@ export function applyGuards (router) {
globalRole
})
console.timeEnd(tlabel)
_perfEnd()
return true
} catch (e) {
console.error('[guards] erro no beforeEach:', e)
logError('router.guard', 'erro no beforeEach', e)
if (to.path.startsWith('/auth')) return true
if (to.meta?.public) return true
@@ -814,6 +826,8 @@ export function applyGuards (router) {
sessionUidCache = null
saasAdminCacheUid = null
saasAdminCacheIsAdmin = null
globalRoleCacheUid = null
globalRoleCache = null
// ✅ FIX: limpa o localStorage de tenant na saída
// Sem isso, o próximo login restaura o tenant do usuário anterior.
@@ -861,6 +875,8 @@ export function applyGuards (router) {
sessionUidCache = uid || null
saasAdminCacheUid = null
saasAdminCacheIsAdmin = null
globalRoleCacheUid = null
globalRoleCache = null
try {
const tf = useTenantFeaturesStore()