first commit
This commit is contained in:
@@ -1,78 +1,225 @@
|
||||
<script setup>
|
||||
import { useLayout } from '@/layout/composables/layout';
|
||||
import { computed } from 'vue';
|
||||
import { useLayout } from '@/layout/composables/layout'
|
||||
import { computed, ref, nextTick } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { layoutState, isDesktop } = useLayout();
|
||||
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: true
|
||||
},
|
||||
parentPath: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
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));
|
||||
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(() => {
|
||||
return props.item.path ? layoutState.activePath?.startsWith(fullPath.value) : layoutState.activePath === props.item.to;
|
||||
});
|
||||
const current = layoutState.activePath || ''
|
||||
const item = props.item
|
||||
|
||||
const itemClick = (event, item) => {
|
||||
if (item.disabled) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
// grupo com submenu: active se qualquer descendente estiver ativo
|
||||
if (item?.items?.length) {
|
||||
if (hasActiveDescendant(item, current)) return true
|
||||
|
||||
if (item.command) {
|
||||
item.command({ originalEvent: event, item: item });
|
||||
}
|
||||
// fallback pelo "path" (útil para rotas internas tipo /saas/plans/123)
|
||||
return item.path ? current.startsWith(fullPath.value || '') : false
|
||||
}
|
||||
|
||||
if (item.items) {
|
||||
if (isActive.value) {
|
||||
layoutState.activePath = layoutState.activePath.replace(item.path, '');
|
||||
} else {
|
||||
layoutState.activePath = fullPath.value;
|
||||
layoutState.menuHoverActive = true;
|
||||
}
|
||||
// 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.overlayMenuActive = false;
|
||||
layoutState.mobileMenuActive = false;
|
||||
layoutState.menuHoverActive = false;
|
||||
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;
|
||||
}
|
||||
};
|
||||
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/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">{{ item.label }}</div>
|
||||
<a v-if="(!item.to || item.items) && item.visible !== false" :href="item.url" @click="itemClick($event, item)" :class="item.class" :target="item.target" tabindex="0" @mouseenter="onMouseEnter">
|
||||
<i :class="item.icon" class="layout-menuitem-icon" />
|
||||
<span class="layout-menuitem-text">{{ item.label }}</span>
|
||||
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" v-if="item.items" />
|
||||
</a>
|
||||
<router-link v-if="item.to && !item.items && item.visible !== false" @click="itemClick($event, item)" exactActiveClass="active-route" :class="item.class" tabindex="0" :to="item.to" @mouseenter="onMouseEnter">
|
||||
<i :class="item.icon" class="layout-menuitem-icon" />
|
||||
<span class="layout-menuitem-text">{{ item.label }}</span>
|
||||
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" v-if="item.items" />
|
||||
</router-link>
|
||||
<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.label + '_' + (child.to || child.path)" :item="child" :root="false" :parentPath="fullPath" />
|
||||
</ul>
|
||||
</Transition>
|
||||
</li>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user