ZERADO
This commit is contained in:
+465
-85
@@ -1,66 +1,132 @@
|
||||
<!-- src/layout/AppTopbar.vue -->
|
||||
<script setup>
|
||||
import { computed, ref, onMounted, provide, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
import { useLayout } from '@/layout/composables/layout'
|
||||
import AppConfigurator from './AppConfigurator.vue'
|
||||
|
||||
import Button from 'primevue/button'
|
||||
import Toast from 'primevue/toast'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
|
||||
import { useUserSettingsPersistence } from '@/composables/useUserSettingsPersistence'
|
||||
import { useRoleGuard } from '@/composables/useRoleGuard'
|
||||
const { canSee } = useRoleGuard()
|
||||
|
||||
// ✅ engine central
|
||||
import { useUserSettingsPersistence } from '@/composables/useUserSettingsPersistence'
|
||||
import { applyThemeEngine } from '@/theme/theme.options'
|
||||
|
||||
import { useTenantFeaturesStore } from '@/stores/tenantFeaturesStore'
|
||||
|
||||
const toast = useToast()
|
||||
const entitlementsStore = useEntitlementsStore()
|
||||
const tenantStore = useTenantStore()
|
||||
|
||||
const { toggleMenu, toggleDarkMode, isDarkTheme, layoutConfig, changeMenuMode } = useLayout()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const planBtn = ref(null)
|
||||
|
||||
/* ----------------------------
|
||||
Persistência (1 instância)
|
||||
Persistência
|
||||
----------------------------- */
|
||||
const { init: initUserSettings, queuePatch } = useUserSettingsPersistence()
|
||||
|
||||
provide('queueUserSettingsPatch', queuePatch)
|
||||
|
||||
/* ----------------------------
|
||||
Contexto (UID/Email/Tenant)
|
||||
----------------------------- */
|
||||
const sessionUid = ref(null)
|
||||
const sessionEmail = ref(null)
|
||||
|
||||
async function loadSessionIdentity () {
|
||||
try {
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error) throw error
|
||||
sessionUid.value = data?.user?.id || null
|
||||
sessionEmail.value = data?.user?.email || null
|
||||
} catch (e) {
|
||||
sessionUid.value = null
|
||||
sessionEmail.value = null
|
||||
console.warn('[Topbar][identity] falhou:', e?.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
const tenantId = computed(() => tenantStore.activeTenantId || null)
|
||||
|
||||
// ✅ tenta achar “nome/email” da clínica do jeito mais tolerante possível
|
||||
const tenantName = computed(() => {
|
||||
const t =
|
||||
tenantStore.activeTenant ||
|
||||
tenantStore.tenant ||
|
||||
tenantStore.currentTenant ||
|
||||
null
|
||||
|
||||
return (
|
||||
t?.name ||
|
||||
t?.nome ||
|
||||
t?.display_name ||
|
||||
t?.fantasy_name ||
|
||||
t?.razao_social ||
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
const tenantEmail = computed(() => {
|
||||
const t =
|
||||
tenantStore.activeTenant ||
|
||||
tenantStore.tenant ||
|
||||
tenantStore.currentTenant ||
|
||||
null
|
||||
|
||||
return (
|
||||
t?.email ||
|
||||
t?.clinic_email ||
|
||||
t?.contact_email ||
|
||||
null
|
||||
)
|
||||
})
|
||||
|
||||
const ctxItems = computed(() => {
|
||||
const items = []
|
||||
|
||||
if (tenantName.value) items.push({ k: 'Clínica', v: tenantName.value })
|
||||
if (tenantEmail.value) items.push({ k: 'Email', v: tenantEmail.value })
|
||||
|
||||
// ids (sempre úteis pra debug)
|
||||
if (tenantId.value) items.push({ k: 'Tenant', v: tenantId.value })
|
||||
if (sessionUid.value) items.push({ k: 'UID', v: sessionUid.value })
|
||||
|
||||
return items
|
||||
})
|
||||
|
||||
/* ----------------------------
|
||||
Fonte da verdade: DOM
|
||||
----------------------------- */
|
||||
function isDarkNow() {
|
||||
function isDarkNow () {
|
||||
return document.documentElement.classList.contains('app-dark')
|
||||
}
|
||||
|
||||
function setDarkMode(shouldBeDark) {
|
||||
function setDarkMode (shouldBeDark) {
|
||||
const now = isDarkNow()
|
||||
if (shouldBeDark !== now) toggleDarkMode()
|
||||
}
|
||||
|
||||
async function waitForDarkFlip(before, timeoutMs = 900) {
|
||||
async function waitForDarkFlip (before, timeoutMs = 900) {
|
||||
const start = performance.now()
|
||||
|
||||
while (performance.now() - start < timeoutMs) {
|
||||
await nextTick()
|
||||
await new Promise((r) => requestAnimationFrame(r))
|
||||
|
||||
const now = isDarkNow()
|
||||
if (now !== before) return now
|
||||
}
|
||||
return isDarkNow()
|
||||
}
|
||||
|
||||
/* ----------------------------
|
||||
Bootstrap: carrega e aplica
|
||||
----------------------------- */
|
||||
async function loadAndApplyUserSettings() {
|
||||
async function loadAndApplyUserSettings () {
|
||||
try {
|
||||
const { data: u, error: uErr } = await supabase.auth.getUser()
|
||||
if (uErr) throw uErr
|
||||
@@ -74,27 +140,27 @@ async function loadAndApplyUserSettings() {
|
||||
.maybeSingle()
|
||||
|
||||
if (error) throw error
|
||||
if (!settings) {
|
||||
console.log('[Topbar][bootstrap] sem user_settings ainda')
|
||||
return
|
||||
}
|
||||
if (!settings) return
|
||||
|
||||
console.log('[Topbar][bootstrap] settings=', settings)
|
||||
|
||||
// dark/light
|
||||
// 1) dark/light (DOM é a fonte da verdade)
|
||||
if (settings.theme_mode) setDarkMode(settings.theme_mode === 'dark')
|
||||
|
||||
// layoutConfig
|
||||
// 2) layoutConfig
|
||||
if (settings.preset) layoutConfig.preset = settings.preset
|
||||
if (settings.primary_color) layoutConfig.primary = settings.primary_color
|
||||
if (settings.surface_color) layoutConfig.surface = settings.surface_color
|
||||
if (settings.menu_mode) layoutConfig.menuMode = settings.menu_mode
|
||||
|
||||
// aplica tema via engine única
|
||||
// 3) aplica engine UMA vez
|
||||
applyThemeEngine(layoutConfig)
|
||||
|
||||
// aplica menu mode
|
||||
try { changeMenuMode() } catch (e) {
|
||||
// ✅ IMPORTANTE:
|
||||
// changeMenuMode NÃO é só "setar menuMode".
|
||||
// Ele reseta estados do sidebar/overlay/mobile e previne drift.
|
||||
try {
|
||||
changeMenuMode(layoutConfig.menuMode)
|
||||
} catch (e) {
|
||||
try { changeMenuMode({ value: layoutConfig.menuMode }) } catch {}
|
||||
console.warn('[Topbar][bootstrap] changeMenuMode falhou:', e?.message || e)
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -102,45 +168,88 @@ async function loadAndApplyUserSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------
|
||||
Atalho topbar: Dark/Light
|
||||
----------------------------- */
|
||||
async function toggleDarkAndPersistSilently() {
|
||||
async function toggleDarkAndPersistSilently () {
|
||||
try {
|
||||
const before = isDarkNow()
|
||||
console.log('[Topbar][theme] click. before=', before ? 'dark' : 'light')
|
||||
|
||||
toggleDarkMode()
|
||||
|
||||
const after = await waitForDarkFlip(before)
|
||||
const theme_mode = after ? 'dark' : 'light'
|
||||
|
||||
console.log('[Topbar][theme] after=', theme_mode, 'isDarkTheme=', !!isDarkTheme)
|
||||
|
||||
await queuePatch({ theme_mode }, { flushNow: true })
|
||||
|
||||
console.log('[Topbar][theme] saved theme_mode=', theme_mode)
|
||||
} catch (e) {
|
||||
console.error('[Topbar][theme] falhou:', e?.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
/* ----------------------------
|
||||
Plano (teu código intacto)
|
||||
Plano (DEV) — popup menu
|
||||
----------------------------- */
|
||||
const tenantId = computed(() => tenantStore.activeTenantId || null)
|
||||
const trocandoPlano = ref(false)
|
||||
|
||||
async function getPlanIdByKey(planKey) {
|
||||
const { data, error } = await supabase.from('plans').select('id, key').eq('key', planKey).single()
|
||||
const enablePlanToggle = computed(() => {
|
||||
const flag = String(import.meta.env?.VITE_ENABLE_PLAN_TOGGLE || '').toLowerCase()
|
||||
return Boolean(import.meta.env?.DEV) || flag === 'true'
|
||||
})
|
||||
|
||||
const showPlanDevMenu = computed(() => {
|
||||
return canSee('settings.view') && enablePlanToggle.value
|
||||
})
|
||||
|
||||
const planMenu = ref()
|
||||
const planMenuLoading = ref(false)
|
||||
const planMenuTarget = ref(null) // 'therapist' | 'clinic' | null
|
||||
const planMenuSub = ref(null) // subscription ativa (obj)
|
||||
const planMenuPlans = ref([]) // plans ativos do target
|
||||
|
||||
async function getMyUserId () {
|
||||
const { data, error } = await supabase.auth.getUser()
|
||||
if (error) throw error
|
||||
return data.id
|
||||
const uid = data?.user?.id
|
||||
if (!uid) throw new Error('Sessão inválida (sem user).')
|
||||
return uid
|
||||
}
|
||||
|
||||
async function getActiveSubscriptionByTenant(tid) {
|
||||
// therapist subscription: user_id — sem filtro de tenant_id (pode estar preenchido)
|
||||
async function getActiveTherapistSubscription () {
|
||||
const uid = await getMyUserId()
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('subscriptions')
|
||||
.select('id, tenant_id, plan_id, status, created_at, updated_at')
|
||||
.select('id, tenant_id, user_id, plan_id, status, updated_at')
|
||||
.eq('user_id', uid)
|
||||
.order('updated_at', { ascending: false })
|
||||
.limit(10)
|
||||
|
||||
if (error) throw error
|
||||
|
||||
const list = data || []
|
||||
if (!list.length) return null
|
||||
|
||||
const priority = (st) => {
|
||||
const s = String(st || '').toLowerCase()
|
||||
if (s === 'active') return 1
|
||||
if (s === 'trialing') return 2
|
||||
if (s === 'past_due') return 3
|
||||
if (s === 'unpaid') return 4
|
||||
if (s === 'incomplete') return 5
|
||||
if (s === 'canceled' || s === 'cancelled') return 9
|
||||
return 8
|
||||
}
|
||||
|
||||
return list.slice().sort((a, b) => {
|
||||
const pa = priority(a?.status)
|
||||
const pb = priority(b?.status)
|
||||
if (pa !== pb) return pa - pb
|
||||
return new Date(b?.updated_at || 0) - new Date(a?.updated_at || 0)
|
||||
})[0]
|
||||
}
|
||||
|
||||
async function getActiveClinicSubscription () {
|
||||
const tid = tenantId.value
|
||||
if (!tid) return null
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('subscriptions')
|
||||
.select('id, tenant_id, user_id, plan_id, status, updated_at')
|
||||
.eq('tenant_id', tid)
|
||||
.eq('status', 'active')
|
||||
.order('updated_at', { ascending: false })
|
||||
@@ -151,69 +260,273 @@ async function getActiveSubscriptionByTenant(tid) {
|
||||
return data || null
|
||||
}
|
||||
|
||||
async function getPlanKeyById(planId) {
|
||||
const { data, error } = await supabase.from('plans').select('key').eq('id', planId).single()
|
||||
async function listActivePlansByTarget (target) {
|
||||
const { data, error } = await supabase
|
||||
.from('plans')
|
||||
.select('id, key, target, is_active')
|
||||
.eq('target', target)
|
||||
.eq('is_active', true)
|
||||
.order('key', { ascending: true })
|
||||
|
||||
if (error) throw error
|
||||
return data.key
|
||||
return data || []
|
||||
}
|
||||
|
||||
async function alternarPlano() {
|
||||
async function refreshEntitlementsAfterToggle (target) {
|
||||
// ✅ aqui NÃO dá pra usar invalidate geral, porque precisamos dos dois caches
|
||||
// mas durante toggle, é mais seguro forçar recarga do escopo que foi alterado.
|
||||
|
||||
if (target === 'clinic') {
|
||||
const tid = tenantId.value
|
||||
if (!tid) return
|
||||
await entitlementsStore.loadForTenant(tid, { force: true })
|
||||
return
|
||||
}
|
||||
|
||||
// therapist
|
||||
const uid = await getMyUserId()
|
||||
await entitlementsStore.loadForUser(uid, { force: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Resolve a subscription ativa levando em conta a área da rota atual.
|
||||
*
|
||||
* Áreas de clínica (/admin, /supervisor) → contexto tenant_id (clinic)
|
||||
* Demais áreas (/therapist, /editor, /portal, etc.) → contexto user_id (pessoal)
|
||||
*
|
||||
* Isso evita que um editor que também é membro de uma clínica veja o plano
|
||||
* da clínica no botão DEV em vez do seu próprio plano.
|
||||
*/
|
||||
async function resolveActiveSubscriptionContext () {
|
||||
const path = route.path || ''
|
||||
const isClinicContext =
|
||||
path.startsWith('/admin') ||
|
||||
path.startsWith('/supervisor')
|
||||
|
||||
if (isClinicContext && tenantId.value) {
|
||||
const clinicSub = await getActiveClinicSubscription()
|
||||
if (clinicSub) return { sub: clinicSub, target: 'clinic' }
|
||||
}
|
||||
|
||||
const therapistSub = await getActiveTherapistSubscription()
|
||||
if (therapistSub) return { sub: therapistSub, target: 'therapist' }
|
||||
|
||||
// último fallback: clinic (caso não-clínica sem sub pessoal)
|
||||
if (tenantId.value) {
|
||||
const clinicSub = await getActiveClinicSubscription()
|
||||
return { sub: clinicSub || null, target: clinicSub ? 'clinic' : null }
|
||||
}
|
||||
|
||||
return { sub: null, target: null }
|
||||
}
|
||||
|
||||
function normalizeKey (k) {
|
||||
return String(k || '').trim()
|
||||
}
|
||||
|
||||
// free primeiro, depois o resto por key
|
||||
function sortPlansSmart (plans) {
|
||||
const arr = [...(plans || [])]
|
||||
arr.sort((a, b) => {
|
||||
const ak = normalizeKey(a?.key).toLowerCase()
|
||||
const bk = normalizeKey(b?.key).toLowerCase()
|
||||
|
||||
const aIsFree = ak.endsWith('_free') || ak === 'free'
|
||||
const bIsFree = bk.endsWith('_free') || bk === 'free'
|
||||
if (aIsFree && !bIsFree) return -1
|
||||
if (!aIsFree && bIsFree) return 1
|
||||
|
||||
return ak.localeCompare(bk)
|
||||
})
|
||||
return arr
|
||||
}
|
||||
|
||||
async function loadPlanMenuData () {
|
||||
planMenuLoading.value = true
|
||||
try {
|
||||
const { sub, target } = await resolveActiveSubscriptionContext()
|
||||
planMenuSub.value = sub
|
||||
planMenuTarget.value = target
|
||||
|
||||
if (!sub?.id || !target) {
|
||||
planMenuPlans.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const plans = await listActivePlansByTarget(target)
|
||||
planMenuPlans.value = sortPlansSmart(plans)
|
||||
} finally {
|
||||
planMenuLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const planMenuModel = computed(() => {
|
||||
const sub = planMenuSub.value
|
||||
const target = planMenuTarget.value
|
||||
const plans = planMenuPlans.value || []
|
||||
|
||||
if (!sub?.id || !target) {
|
||||
return [
|
||||
{ label: 'Sem assinatura ativa', icon: 'pi pi-exclamation-triangle', disabled: true },
|
||||
{ label: 'Não encontrei subscription ativa nem para therapist (user_id) nem para clinic (tenant_id).', disabled: true }
|
||||
]
|
||||
}
|
||||
|
||||
const currentPlanId = String(sub.plan_id || '')
|
||||
|
||||
const header = {
|
||||
label: `Planos (${target})`,
|
||||
icon: target === 'therapist' ? 'pi pi-user' : 'pi pi-building',
|
||||
disabled: true
|
||||
}
|
||||
|
||||
const subInfo = {
|
||||
label: `Sub: ${String(sub.id).slice(0, 8)}… • Atual: ${String(currentPlanId).slice(0, 8)}…`,
|
||||
icon: 'pi pi-info-circle',
|
||||
disabled: true
|
||||
}
|
||||
|
||||
const items = []
|
||||
let insertedSeparator = false
|
||||
|
||||
plans.forEach((p) => {
|
||||
const isCurrent = String(p.id) === currentPlanId
|
||||
const keyLower = String(p.key || '').toLowerCase()
|
||||
const isFree = keyLower.endsWith('_free') || keyLower === 'free'
|
||||
|
||||
items.push({
|
||||
label: isCurrent ? `${p.key} (atual)` : p.key,
|
||||
icon: isCurrent ? 'pi pi-check' : (isFree ? 'pi pi-star' : 'pi pi-circle'),
|
||||
disabled: isCurrent || planMenuLoading.value || trocandoPlano.value,
|
||||
command: async () => {
|
||||
await changePlanTo(p.id, p.key, target)
|
||||
}
|
||||
})
|
||||
|
||||
if (!insertedSeparator && isFree) {
|
||||
items.push({ separator: true })
|
||||
insertedSeparator = true
|
||||
}
|
||||
})
|
||||
|
||||
if (items.length && items[items.length - 1]?.separator) items.pop()
|
||||
|
||||
if (!plans.length) {
|
||||
return [header, subInfo, { separator: true }, { label: 'Nenhum plano ativo encontrado', icon: 'pi pi-info-circle', disabled: true }]
|
||||
}
|
||||
|
||||
return [header, subInfo, { separator: true }, ...items]
|
||||
})
|
||||
|
||||
async function openPlanMenu (event) {
|
||||
if (!showPlanDevMenu.value) return
|
||||
|
||||
try {
|
||||
await loadPlanMenuData()
|
||||
} catch (err) {
|
||||
console.error('[PLANO][DEV menu] erro:', err?.message || err)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Erro ao carregar planos',
|
||||
detail: err?.message || 'Falha desconhecida.',
|
||||
life: 5200
|
||||
})
|
||||
}
|
||||
|
||||
const anchorEl = planBtn.value?.$el || event?.currentTarget || event?.target
|
||||
if (!anchorEl) {
|
||||
planMenu.value?.toggle?.(event)
|
||||
return
|
||||
}
|
||||
|
||||
planMenu.value?.show?.({ currentTarget: anchorEl })
|
||||
}
|
||||
|
||||
async function changePlanTo (newPlanId, newPlanKey, target) {
|
||||
if (trocandoPlano.value) return
|
||||
trocandoPlano.value = true
|
||||
|
||||
try {
|
||||
const tid = tenantId.value
|
||||
if (!tid) {
|
||||
toast.add({ severity: 'warn', summary: 'Sem tenant ativo', detail: 'Selecione/entre em uma clínica (tenant) antes de trocar o plano.', life: 4500 })
|
||||
return
|
||||
}
|
||||
|
||||
const sub = await getActiveSubscriptionByTenant(tid)
|
||||
if (!sub?.id) {
|
||||
toast.add({ severity: 'warn', summary: 'Sem assinatura ativa', detail: 'Esse tenant ainda não tem subscription ativa. Ative via intenção/pagamento manual.', life: 5000 })
|
||||
return
|
||||
}
|
||||
|
||||
const atualKey = await getPlanKeyById(sub.plan_id)
|
||||
const novoKey = atualKey === 'pro' ? 'free' : 'pro'
|
||||
const novoPlanId = await getPlanIdByKey(novoKey)
|
||||
const sub = planMenuSub.value
|
||||
if (!sub?.id) throw new Error('Subscription inválida.')
|
||||
|
||||
const { error: rpcError } = await supabase.rpc('change_subscription_plan', {
|
||||
p_subscription_id: sub.id,
|
||||
p_new_plan_id: novoPlanId
|
||||
p_new_plan_id: newPlanId
|
||||
})
|
||||
if (rpcError) throw rpcError
|
||||
|
||||
entitlementsStore.clear?.()
|
||||
await entitlementsStore.fetch(tid, { force: true })
|
||||
planMenuSub.value = { ...sub, plan_id: newPlanId }
|
||||
|
||||
toast.add({ severity: 'success', summary: 'Plano alternado', detail: `${String(atualKey).toUpperCase()} → ${String(novoKey).toUpperCase()}`, life: 3000 })
|
||||
// ✅ recarrega o escopo certo (tenant ou user)
|
||||
await refreshEntitlementsAfterToggle(target)
|
||||
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: 'Plano alterado (DEV)',
|
||||
detail: `${String(newPlanKey).toUpperCase()} aplicado (${target})`,
|
||||
life: 3200
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[PLANO] Erro ao alternar:', err?.message || err)
|
||||
toast.add({ severity: 'error', summary: 'Erro ao alternar plano', detail: err?.message || 'Falha desconhecida.', life: 5000 })
|
||||
console.error('[PLANO] Erro ao trocar:', err?.message || err)
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: 'Erro ao trocar plano',
|
||||
detail: err?.message || 'Falha desconhecida.',
|
||||
life: 6000
|
||||
})
|
||||
} finally {
|
||||
trocandoPlano.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function logout() {
|
||||
/* ----------------------------
|
||||
Logout
|
||||
----------------------------- */
|
||||
async function logout () {
|
||||
const tenant = useTenantStore()
|
||||
const ent = useEntitlementsStore()
|
||||
const tf = useTenantFeaturesStore()
|
||||
|
||||
try {
|
||||
await supabase.auth.signOut()
|
||||
} finally {
|
||||
// limpa possíveis intenções guardadas
|
||||
sessionStorage.removeItem('redirect_after_login')
|
||||
sessionStorage.removeItem('intended_area')
|
||||
tenant.reset()
|
||||
ent.invalidate()
|
||||
tf.invalidate()
|
||||
|
||||
// ✅ vai para HomeCards
|
||||
router.replace('/')
|
||||
// Use router.replace('/') e não push,
|
||||
// assim o usuário não consegue voltar com o botão "voltar" para uma rota protegida.
|
||||
sessionStorage.clear()
|
||||
localStorage.clear()
|
||||
|
||||
router.replace('/auth/login')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ✅ Bootstrap entitlements (resolve “menu não alterna” sem depender do guard)
|
||||
* - se tem tenant ativo => carrega tenant entitlements
|
||||
* - senão => carrega user entitlements
|
||||
*/
|
||||
async function bootstrapEntitlements () {
|
||||
try {
|
||||
const uid = sessionUid.value || (await getMyUserId())
|
||||
const tid = tenantId.value
|
||||
|
||||
if (tid) {
|
||||
await entitlementsStore.loadForTenant(tid, { force: false, maxAgeMs: 60_000 })
|
||||
} else if (uid) {
|
||||
await entitlementsStore.loadForUser(uid, { force: false, maxAgeMs: 60_000 })
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Topbar][entitlements bootstrap] falhou:', e?.message || e)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await initUserSettings()
|
||||
await loadAndApplyUserSettings()
|
||||
await loadSessionIdentity()
|
||||
await bootstrapEntitlements()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -228,10 +541,24 @@ onMounted(async () => {
|
||||
|
||||
<router-link to="/" class="layout-topbar-logo">
|
||||
<svg viewBox="0 0 54 40" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- ... SVG gigante ... -->
|
||||
<!-- ... SVG ... -->
|
||||
</svg>
|
||||
<span>SAKAI</span>
|
||||
</router-link>
|
||||
|
||||
<div v-if="ctxItems.length" class="topbar-ctx">
|
||||
<div class="topbar-ctx-row">
|
||||
<span
|
||||
v-for="(it, idx) in ctxItems"
|
||||
:key="`${it.k}-${idx}`"
|
||||
class="topbar-ctx-pill"
|
||||
:title="`${it.k}: ${it.v}`"
|
||||
>
|
||||
<b class="topbar-ctx-k">{{ it.k }}:</b>
|
||||
<span class="topbar-ctx-v">{{ it.v }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-topbar-actions">
|
||||
@@ -277,13 +604,23 @@ onMounted(async () => {
|
||||
<div class="layout-topbar-menu hidden lg:block">
|
||||
<div class="layout-topbar-menu-content">
|
||||
<Button
|
||||
label="Plano"
|
||||
icon="pi pi-sync"
|
||||
v-if="showPlanDevMenu"
|
||||
ref="planBtn"
|
||||
label="Plano (DEV)"
|
||||
icon="pi pi-sliders-h"
|
||||
severity="contrast"
|
||||
outlined
|
||||
:loading="trocandoPlano"
|
||||
:disabled="trocandoPlano"
|
||||
@click="alternarPlano"
|
||||
:loading="planMenuLoading || trocandoPlano"
|
||||
:disabled="planMenuLoading || trocandoPlano"
|
||||
@click="openPlanMenu"
|
||||
/>
|
||||
|
||||
<Menu
|
||||
ref="planMenu"
|
||||
:model="planMenuModel"
|
||||
popup
|
||||
appendTo="body"
|
||||
:baseZIndex="3000"
|
||||
/>
|
||||
|
||||
<button type="button" class="layout-topbar-action">
|
||||
@@ -309,3 +646,46 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.topbar-ctx {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 0.75rem;
|
||||
max-width: min(62vw, 980px);
|
||||
}
|
||||
|
||||
.topbar-ctx-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.topbar-ctx-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.25rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--surface-border);
|
||||
background: var(--surface-card);
|
||||
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.02);
|
||||
max-width: 320px;
|
||||
}
|
||||
|
||||
.topbar-ctx-k {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.topbar-ctx-v {
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.95;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 240px;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user