295 lines
10 KiB
Vue

<script setup lang="ts">
import { ref } from 'vue';
interface MenuItem {
label: string;
icon: string;
to?: string;
items?: MenuItem[];
}
const menuItems = ref<MenuItem[]>([
{
label: 'Dashboard',
icon: 'pi pi-chart-line',
to: '/'
},
{
label: 'Ventas',
icon: 'pi pi-shopping-cart',
items: [
{ label: 'Nueva Venta', icon: 'pi pi-plus', to: '/ventas/nueva' },
{ label: 'Lista de Ventas', icon: 'pi pi-list', to: '/ventas/lista' },
{ label: 'Cotizaciones', icon: 'pi pi-file-edit', to: '/ventas/cotizaciones' }
]
},
{
label: 'Clientes',
icon: 'pi pi-users',
to: '/clientes'
},
{
label: 'Inventario',
icon: 'pi pi-box',
to: '/inventario'
},
{
label: 'Finanzas',
icon: 'pi pi-wallet',
items: [
{ label: 'Ingresos', icon: 'pi pi-arrow-up', to: '/finanzas/ingresos' },
{ label: 'Gastos', icon: 'pi pi-credit-card', to: '/finanzas/gastos' },
{ label: 'Cuentas por Cobrar', icon: 'pi pi-money-bill', to: '/finanzas/cobrar' }
]
},
{
label: 'Reportes',
icon: 'pi pi-chart-bar',
to: '/reportes'
},
{
label: 'Documentos',
icon: 'pi pi-file',
to: '/documentos'
},
{
label: 'Módulo Personalizado',
icon: 'pi pi-th-large',
to: '/modulo-personalizado'
},
{
label: 'Configuración',
icon: 'pi pi-cog',
to: '/configuracion'
}
]);
const sidebarVisible = ref(true);
const openItems = ref<string[]>([]);
const currentRoute = ref(window.location.pathname);
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;
return currentRoute.value === to;
};
// Simular cambio de ruta (en producción usarías Vue Router)
const navigateTo = (to: string) => {
currentRoute.value = to;
// window.history.pushState({}, '', 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>