295 lines
10 KiB
Vue
295 lines
10 KiB
Vue
<script setup lang="ts">
|
|
import { ref } from 'vue';
|
|
import { useRouter, useRoute } from 'vue-router';
|
|
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
|
|
interface MenuItem {
|
|
label: string;
|
|
icon: string;
|
|
to?: string;
|
|
items?: MenuItem[];
|
|
}
|
|
|
|
const menuItems = ref<MenuItem[]>([
|
|
{
|
|
label: 'Dashboard',
|
|
icon: 'pi pi-chart-line',
|
|
to: '/'
|
|
},
|
|
{
|
|
label: 'Catálogo',
|
|
icon: 'pi pi-book',
|
|
items: [
|
|
{ label: 'Unidades de Medida', icon: 'pi pi-calculator', to: '/catalog/units-of-measure' },
|
|
{ label: 'Clasificaciones Comerciales', icon: 'pi pi-tags', to: '/catalog/classifications-comercial' }
|
|
]
|
|
},
|
|
{
|
|
label: 'Almacén',
|
|
icon: 'pi pi-box',
|
|
items: [
|
|
{ label: 'Almacenes', icon: 'pi pi-warehouse', to: '/warehouse' },
|
|
{ label: 'Administrar Clasificaciones', icon: 'pi pi-sitemap', to: '/warehouse/classifications' }
|
|
]
|
|
},
|
|
{
|
|
label: 'Recursos humanos',
|
|
icon: 'pi pi-users',
|
|
items: [
|
|
{ label: 'Puestos laborales', icon: 'pi pi-user', to: '/rh/positions' },
|
|
{ label: 'Departamentos', icon: 'pi pi-briefcase', to: '/rh/departments' }
|
|
]
|
|
},
|
|
{
|
|
label: 'Productos',
|
|
icon: 'pi pi-shopping-cart',
|
|
to: '/products'
|
|
},
|
|
{
|
|
label: 'Puntos de venta',
|
|
icon: 'pi pi-cog',
|
|
to: '/stores'
|
|
},
|
|
{
|
|
label: 'Configuración',
|
|
icon: 'pi pi-cog',
|
|
to: '/configuracion'
|
|
},
|
|
{
|
|
label: 'Usuarios y Roles',
|
|
icon: 'pi pi-users',
|
|
items: [
|
|
{ label: 'Usuarios', icon: 'pi pi-user', to: '/users' },
|
|
{ label: 'Roles', icon: 'pi pi-shield', to: '/roles' }
|
|
]
|
|
}
|
|
]);
|
|
|
|
const sidebarVisible = ref(true);
|
|
const openItems = ref<string[]>([]);
|
|
|
|
const toggleSidebar = () => {
|
|
sidebarVisible.value = !sidebarVisible.value;
|
|
};
|
|
|
|
const toggleItem = (label: string) => {
|
|
const index = openItems.value.indexOf(label);
|
|
if (index > -1) {
|
|
openItems.value.splice(index, 1);
|
|
} else {
|
|
openItems.value.push(label);
|
|
}
|
|
};
|
|
|
|
const isItemOpen = (label: string) => {
|
|
return openItems.value.includes(label);
|
|
};
|
|
|
|
const isRouteActive = (to: string | undefined) => {
|
|
if (!to) return false;
|
|
|
|
// Coincidencia exacta
|
|
if (route.path === to) {
|
|
return true;
|
|
}
|
|
|
|
// Para la ruta raíz, solo coincidencia exacta
|
|
if (to === '/') {
|
|
return false;
|
|
}
|
|
|
|
// Si la ruta actual es hija (ej: /warehouse/create)
|
|
// y el item es el padre (ej: /warehouse)
|
|
// SOLO marcar activo si la ruta hija NO está explícitamente en el menú
|
|
if (route.path.startsWith(to + '/')) {
|
|
// Verificar si la ruta actual está definida como un item del menú
|
|
const isExplicitRoute = menuItems.value.some(item => {
|
|
if (item.items) {
|
|
return item.items.some(subItem => subItem.to === route.path);
|
|
}
|
|
return item.to === route.path;
|
|
});
|
|
|
|
// Si NO está explícitamente en el menú, entonces es una ruta hija (create, edit, etc)
|
|
return !isExplicitRoute;
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
// Navegar usando Vue Router
|
|
const navigateTo = (to: string) => {
|
|
router.push(to);
|
|
};
|
|
|
|
// Funciones de animación para los submenús
|
|
const onEnter = (el: Element) => {
|
|
const element = el as HTMLElement;
|
|
element.style.height = '0';
|
|
element.offsetHeight; // Force reflow
|
|
element.style.height = element.scrollHeight + 'px';
|
|
};
|
|
|
|
const onLeave = (el: Element) => {
|
|
const element = el as HTMLElement;
|
|
element.style.height = element.scrollHeight + 'px';
|
|
element.offsetHeight; // Force reflow
|
|
element.style.height = '0';
|
|
};
|
|
|
|
defineExpose({ toggleSidebar });
|
|
</script>
|
|
|
|
<template>
|
|
<aside :class="[
|
|
'bg-surface-0 dark:bg-surface-900 border-r border-surface-200 dark:border-surface-700 transition-all duration-300',
|
|
sidebarVisible ? 'w-64' : 'w-20'
|
|
]">
|
|
<div class="flex flex-col h-full">
|
|
<!-- Logo / Brand -->
|
|
<div class="p-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="flex items-center justify-center w-10 h-10 rounded-lg bg-primary transition-all"
|
|
:class="sidebarVisible ? 'bg-primary' : 'bg-primary/90'">
|
|
<i class="pi pi-chart-line text-xl text-white"></i>
|
|
</div>
|
|
<div v-if="sidebarVisible" class="flex flex-col">
|
|
<h1 class="text-xl font-bold text-surface-900 dark:text-surface-0">
|
|
Golscontrols
|
|
</h1>
|
|
<p class="text-xs text-surface-500 dark:text-surface-400">
|
|
Sistema ERP
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Navigation Menu -->
|
|
<nav class="flex-1 overflow-y-auto p-3">
|
|
<ul class="space-y-1">
|
|
<li v-for="item in menuItems" :key="item.label">
|
|
<!-- Item sin subitems -->
|
|
<a v-if="!item.items" :href="item.to" @click.prevent="item.to && navigateTo(item.to)" :class="[
|
|
'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all cursor-pointer',
|
|
isRouteActive(item.to)
|
|
? 'bg-primary text-white shadow-sm'
|
|
: 'text-surface-700 dark:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800'
|
|
]" :title="!sidebarVisible ? item.label : ''">
|
|
<i :class="[item.icon, 'shrink-0']"></i>
|
|
<span v-if="sidebarVisible">{{ item.label }}</span>
|
|
</a>
|
|
|
|
<!-- Item con subitems (Collapsible) -->
|
|
<div v-else>
|
|
<button @click="toggleItem(item.label)" :class="[
|
|
'w-full flex items-center justify-between px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
|
'text-surface-700 dark:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800'
|
|
]" :title="!sidebarVisible ? item.label : ''">
|
|
<div class="flex items-center gap-3">
|
|
<i :class="[item.icon, 'shrink-0']"></i>
|
|
<span v-if="sidebarVisible">{{ item.label }}</span>
|
|
</div>
|
|
<i v-if="sidebarVisible" :class="[
|
|
'pi pi-chevron-down text-xs transition-transform duration-200',
|
|
isItemOpen(item.label) && 'rotate-180'
|
|
]"></i>
|
|
</button>
|
|
|
|
<!-- Subitems con animación -->
|
|
<Transition name="submenu" @enter="onEnter" @leave="onLeave">
|
|
<ul v-if="sidebarVisible && isItemOpen(item.label)"
|
|
class="ml-6 mt-1 space-y-1 overflow-hidden">
|
|
<li v-for="subItem in item.items" :key="subItem.label">
|
|
<a :href="subItem.to" @click.prevent="subItem.to && navigateTo(subItem.to)"
|
|
:class="[
|
|
'flex items-center gap-2 px-3 py-2 pl-6 rounded-lg text-sm font-medium transition-all cursor-pointer',
|
|
isRouteActive(subItem.to)
|
|
? 'bg-primary text-white shadow-sm'
|
|
: 'text-surface-600 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-800'
|
|
]">
|
|
<i :class="[subItem.icon, 'text-xs shrink-0']"></i>
|
|
<span>{{ subItem.label }}</span>
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
</Transition>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</nav>
|
|
|
|
<!-- Toggle Button -->
|
|
<div class="p-3 border-t border-surface-200 dark:border-surface-700">
|
|
<button @click="toggleSidebar"
|
|
class="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-surface-700 dark:text-surface-200 hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
|
|
:title="sidebarVisible ? 'Contraer sidebar' : 'Expandir sidebar'">
|
|
<i :class="sidebarVisible ? 'pi pi-angle-left' : 'pi pi-angle-right'"></i>
|
|
<span v-if="sidebarVisible" class="text-sm font-medium">Contraer</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
</template>
|
|
|
|
<style scoped>
|
|
aside {
|
|
height: 100vh;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 40;
|
|
}
|
|
|
|
/* Animación para submenús */
|
|
.submenu-enter-active,
|
|
.submenu-leave-active {
|
|
transition: height 0.3s ease, opacity 0.3s ease;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.submenu-enter-from,
|
|
.submenu-leave-to {
|
|
height: 0;
|
|
opacity: 0;
|
|
}
|
|
|
|
.submenu-enter-to,
|
|
.submenu-leave-from {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Animación de rotación para el chevron */
|
|
.rotate-180 {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
/* Efecto de sombra en item activo */
|
|
.shadow-sm {
|
|
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
|
}
|
|
|
|
/* Scroll suave */
|
|
nav {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: rgba(155, 155, 155, 0.5) transparent;
|
|
}
|
|
|
|
nav::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
nav::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
nav::-webkit-scrollbar-thumb {
|
|
background-color: rgba(155, 155, 155, 0.5);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
nav::-webkit-scrollbar-thumb:hover {
|
|
background-color: rgba(155, 155, 155, 0.7);
|
|
}
|
|
</style>
|