Files
agenciapsilmno/src/layout/AppMenuItem.vue

226 lines
6.5 KiB
Vue

<script setup>
import { useLayout } from '@/layout/composables/layout'
import { computed, ref, nextTick } 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'
const { layoutState, isDesktop } = useLayout()
const router = useRouter()
const pop = ref(null)
const tenantStore = useTenantStore()
const entitlementsStore = useEntitlementsStore()
const emit = defineEmits(['quick-create'])
const props = defineProps({
item: { type: Object, default: () => ({}) },
root: { type: Boolean, default: false },
parentPath: { type: String, default: null }
})
const fullPath = computed(() =>
props.item?.path
? (props.parentPath ? props.parentPath + props.item.path : props.item.path)
: null
)
// ==============================
// Active logic: mantém submenu aberto se algum descendente estiver ativo
// ==============================
function isSameRoute (current, target) {
if (!current || !target) return false
return current === target || current.startsWith(target + '/')
}
function hasActiveDescendant (node, currentPath) {
const children = node?.items || []
for (const child of children) {
if (child?.to && isSameRoute(currentPath, child.to)) return true
if (child?.items?.length && hasActiveDescendant(child, currentPath)) return true
}
return false
}
const isActive = computed(() => {
const current = 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
})
// ==============================
// Feature lock + label
// ==============================
const ownerId = computed(() => tenantStore.activeTenantId || null)
const isLocked = computed(() => {
const feature = props.item?.feature
return !!(props.item?.proBadge && feature && ownerId.value && !entitlementsStore.has(feature))
})
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
})
const itemClick = async (event, item) => {
// 🔒 locked -> CTA upgrade
if (props.item?.proBadge && isLocked.value) {
event.preventDefault()
event.stopPropagation()
layoutState.overlayMenuActive = false
layoutState.mobileMenuActive = false
layoutState.menuHoverActive = false
await nextTick()
await router.push({ name: 'upgrade', query: { feature: props.item?.feature || '' } })
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()
if (isActive.value) {
layoutState.activePath = props.parentPath || ''
} else {
layoutState.activePath = fullPath.value
layoutState.menuHoverActive = true
}
return
}
// ✅ leaf: marca ativo e NÃO fecha menu
if (item?.to) layoutState.activePath = item.to
}
const onMouseEnter = () => {
if (isDesktop() && props.root && props.item?.items && layoutState.menuHoverActive) {
layoutState.activePath = fullPath.value
}
}
/* ---------- POPUP + ---------- */
function togglePopover (event) {
if (isBlocked.value) return
pop.value?.toggle(event)
}
function closePopover () {
try { pop.value?.hide() } catch {}
}
function abrirCadastroRapido () {
closePopover()
emit('quick-create', {
entity: props.item?.quickCreateEntity || 'patient',
mode: 'rapido'
})
}
async function irCadastroCompleto () {
closePopover()
layoutState.overlayMenuActive = false
layoutState.mobileMenuActive = false
layoutState.menuHoverActive = false
await nextTick()
router.push('/admin/patients/cadastro')
}
</script>
<template>
<li :class="{ 'layout-root-menuitem': root, 'active-menuitem': isActive }">
<div v-if="root && item.visible !== false" 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">
<component
:is="item.to && !item.items ? 'router-link' : 'a'"
v-bind="item.to && !item.items ? { to: item.to } : { href: item.url }"
@click="itemClick($event, item)"
:class="[item.class, isBlocked ? 'opacity-60 cursor-pointer' : '']"
:target="item.target"
tabindex="0"
@mouseenter="onMouseEnter"
class="flex align-items-center flex-1"
:aria-disabled="isBlocked ? 'true' : 'false'"
>
<i :class="item.icon" class="layout-menuitem-icon" />
<span class="layout-menuitem-text">
{{ labelText }}
<!-- (debug) pode remover depois -->
<small style="opacity:.6">[locked={{ isLocked }}]</small>
</span>
<i v-if="item.items" class="pi pi-fw pi-angle-down layout-submenu-toggler" />
</component>
<Button
v-if="item.quickCreate"
icon="pi pi-plus"
text
rounded
size="small"
class="ml-2"
:disabled="isBlocked"
@click.stop="togglePopover"
/>
</div>
<Popover v-if="item.quickCreate" ref="pop">
<div class="flex flex-column gap-2 min-w-[180px]">
<Button label="Cadastro rápido" icon="pi pi-bolt" text @click="abrirCadastroRapido" />
<Button label="Cadastro completo" icon="pi pi-user-plus" text @click="irCadastroCompleto" />
</div>
</Popover>
<Transition v-if="item.items && item.visible !== false" name="layout-submenu">
<ul v-show="root ? true : isActive" class="layout-submenu">
<app-menu-item
v-for="child in item.items"
:key="(child.to || '') + '|' + (child.path || '') + '|' + child.label"
:item="child"
:root="false"
:parentPath="fullPath"
@quick-create="emit('quick-create', $event)"
/>
</ul>
</Transition>
</li>
</template>