226 lines
6.5 KiB
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>
|