ZERADO
This commit is contained in:
+68
-39
@@ -1,10 +1,10 @@
|
||||
<!-- src/layout/AppMenuItem.vue -->
|
||||
<script setup>
|
||||
import { useLayout } from '@/layout/composables/layout'
|
||||
import { computed, ref, nextTick } from 'vue'
|
||||
import { computed, ref, nextTick, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
import Popover from 'primevue/popover'
|
||||
import Button from 'primevue/button'
|
||||
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
import { useEntitlementsStore } from '@/stores/entitlementsStore'
|
||||
@@ -31,54 +31,85 @@ const fullPath = computed(() =>
|
||||
)
|
||||
|
||||
// ==============================
|
||||
// Active logic: mantém submenu aberto se algum descendente estiver ativo
|
||||
// Visible: boolean OU function() -> boolean
|
||||
// ==============================
|
||||
const isVisible = computed(() => {
|
||||
const v = props.item?.visible
|
||||
if (typeof v === 'function') return !!v()
|
||||
if (v === undefined || v === null) return true
|
||||
return v !== false
|
||||
})
|
||||
|
||||
// ==============================
|
||||
// Helpers de rota: aceita string OU objeto
|
||||
// ==============================
|
||||
function toPath (to) {
|
||||
if (!to) return ''
|
||||
if (typeof to === 'string') return to
|
||||
try { return router.resolve(to).path || '' } catch { return '' }
|
||||
}
|
||||
|
||||
// ==============================
|
||||
// Active logic
|
||||
// ==============================
|
||||
function isSameRoute (current, target) {
|
||||
if (!current || !target) return false
|
||||
return current === target || current.startsWith(target + '/')
|
||||
const cur = typeof current === 'string' ? current : toPath(current)
|
||||
const tar = typeof target === 'string' ? target : toPath(target)
|
||||
if (!cur || !tar) return false
|
||||
return cur === tar || cur.startsWith(tar + '/')
|
||||
}
|
||||
|
||||
function hasActiveDescendant (node, currentPath) {
|
||||
const children = node?.items || []
|
||||
for (const child of children) {
|
||||
if (child?.to && isSameRoute(currentPath, child.to)) return true
|
||||
const childTo = toPath(child?.to)
|
||||
if (childTo && isSameRoute(currentPath, childTo)) return true
|
||||
if (child?.items?.length && hasActiveDescendant(child, currentPath)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const isActive = computed(() => {
|
||||
const current = layoutState.activePath || ''
|
||||
const current = typeof layoutState.activePath === 'string'
|
||||
? layoutState.activePath
|
||||
: toPath(layoutState.activePath)
|
||||
|
||||
const item = props.item
|
||||
|
||||
// grupo com submenu: active se qualquer descendente estiver ativo
|
||||
if (item?.items?.length) {
|
||||
if (hasActiveDescendant(item, current)) return true
|
||||
|
||||
// fallback pelo "path" (útil para rotas internas tipo /saas/plans/123)
|
||||
return item.path ? current.startsWith(fullPath.value || '') : false
|
||||
}
|
||||
|
||||
// folha: active se rota igual ao to
|
||||
return item?.to ? isSameRoute(current, item.to) : false
|
||||
const leafTo = toPath(item?.to)
|
||||
return leafTo ? isSameRoute(current, leafTo) : false
|
||||
})
|
||||
|
||||
// ==============================
|
||||
// Feature lock + label
|
||||
// ✅ PRO badge (agora 100% por entitlementsStore)
|
||||
// ==============================
|
||||
const ownerId = computed(() => tenantStore.activeTenantId || null)
|
||||
|
||||
const isLocked = computed(() => {
|
||||
const showProBadge = computed(() => {
|
||||
const feature = props.item?.feature
|
||||
return !!(props.item?.proBadge && feature && ownerId.value && !entitlementsStore.has(feature))
|
||||
if (!props.item?.proBadge || !feature) return false
|
||||
|
||||
// se tiver a feature, NÃO é PRO (não mostra badge)
|
||||
// se NÃO tiver, mostra PRO
|
||||
try {
|
||||
return !entitlementsStore.has(feature)
|
||||
} catch {
|
||||
// se der erro, não mostra (evita “PRO fantasma”)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// 🔒 locked quando precisa mostrar PRO (ou seja, não tem feature)
|
||||
const isLocked = computed(() => !!(props.item?.proBadge && showProBadge.value))
|
||||
|
||||
const itemDisabled = computed(() => !!props.item?.disabled)
|
||||
const isBlocked = computed(() => itemDisabled.value || isLocked.value)
|
||||
|
||||
const labelText = computed(() => {
|
||||
const base = props.item?.label || ''
|
||||
return props.item?.proBadge && isLocked.value ? `${base} (PRO)` : base
|
||||
return props.item?.label || ''
|
||||
})
|
||||
|
||||
const itemClick = async (event, item) => {
|
||||
@@ -96,17 +127,14 @@ const itemClick = async (event, item) => {
|
||||
return
|
||||
}
|
||||
|
||||
// 🚫 disabled -> bloqueia
|
||||
if (itemDisabled.value) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
// commands
|
||||
if (item?.command) item.command({ originalEvent: event, item })
|
||||
|
||||
// ✅ submenu: expande/colapsa e não navega
|
||||
if (item?.items?.length) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
@@ -114,24 +142,22 @@ const itemClick = async (event, item) => {
|
||||
if (isActive.value) {
|
||||
layoutState.activePath = props.parentPath || ''
|
||||
} else {
|
||||
layoutState.activePath = fullPath.value
|
||||
layoutState.activePath = fullPath.value || ''
|
||||
layoutState.menuHoverActive = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// ✅ leaf: marca ativo e NÃO fecha menu
|
||||
if (item?.to) layoutState.activePath = item.to
|
||||
if (item?.to) layoutState.activePath = toPath(item.to)
|
||||
}
|
||||
|
||||
const onMouseEnter = () => {
|
||||
if (isDesktop() && props.root && props.item?.items && layoutState.menuHoverActive) {
|
||||
layoutState.activePath = fullPath.value
|
||||
layoutState.activePath = fullPath.value || ''
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- POPUP + ---------- */
|
||||
|
||||
function togglePopover (event) {
|
||||
if (isBlocked.value) return
|
||||
pop.value?.toggle(event)
|
||||
@@ -143,10 +169,7 @@ function closePopover () {
|
||||
|
||||
function abrirCadastroRapido () {
|
||||
closePopover()
|
||||
emit('quick-create', {
|
||||
entity: props.item?.quickCreateEntity || 'patient',
|
||||
mode: 'rapido'
|
||||
})
|
||||
emit('quick-create', { entity: props.item?.quickCreateEntity || 'patient', mode: 'rapido' })
|
||||
}
|
||||
|
||||
async function irCadastroCompleto () {
|
||||
@@ -157,17 +180,17 @@ async function irCadastroCompleto () {
|
||||
layoutState.menuHoverActive = false
|
||||
|
||||
await nextTick()
|
||||
router.push('/admin/patients/cadastro')
|
||||
router.push({ name: 'admin-pacientes-cadastro' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li :class="{ 'layout-root-menuitem': root, 'active-menuitem': isActive }">
|
||||
<div v-if="root && item.visible !== false" class="layout-menuitem-root-text">
|
||||
<li v-show="isVisible" :class="{ 'layout-root-menuitem': root, 'active-menuitem': isActive }">
|
||||
<div v-if="root" class="layout-menuitem-root-text">
|
||||
{{ item.label }}
|
||||
</div>
|
||||
|
||||
<div v-if="!root && item.visible !== false" class="flex align-items-center justify-content-between w-full">
|
||||
<div v-if="!root" class="flex align-items-center justify-content-between w-full">
|
||||
<component
|
||||
:is="item.to && !item.items ? 'router-link' : 'a'"
|
||||
v-bind="item.to && !item.items ? { to: item.to } : { href: item.url }"
|
||||
@@ -183,8 +206,14 @@ async function irCadastroCompleto () {
|
||||
|
||||
<span class="layout-menuitem-text">
|
||||
{{ labelText }}
|
||||
<!-- (debug) pode remover depois -->
|
||||
<small style="opacity:.6">[locked={{ isLocked }}]</small>
|
||||
</span>
|
||||
|
||||
<!-- ✅ Badge PRO some quando tem entitlements -->
|
||||
<span
|
||||
v-if="item.proBadge && showProBadge"
|
||||
class="ml-2 text-xs px-2 py-0.5 rounded border border-[var(--surface-border)] opacity-80"
|
||||
>
|
||||
PRO
|
||||
</span>
|
||||
|
||||
<i v-if="item.items" class="pi pi-fw pi-angle-down layout-submenu-toggler" />
|
||||
@@ -209,7 +238,7 @@ async function irCadastroCompleto () {
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<Transition v-if="item.items && item.visible !== false" name="layout-submenu">
|
||||
<Transition v-if="item.items" name="layout-submenu">
|
||||
<ul v-show="root ? true : isActive" class="layout-submenu">
|
||||
<app-menu-item
|
||||
v-for="child in item.items"
|
||||
@@ -222,4 +251,4 @@ async function irCadastroCompleto () {
|
||||
</ul>
|
||||
</Transition>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
Reference in New Issue
Block a user