Files
gameno-dashboard/src/components/layout/AppSidebar.vue
T

295 lines
9.0 KiB
Vue

<!-- /src/components/layout/AppSidebar.vue -->
<template>
<!-- Desktop Sidebar -->
<aside
class="app-sidebar desktop-sidebar surface-card border-left-1 border-color flex flex-column h-full flex-shrink-0"
:class="{ 'sidebar-collapsed': !uiStore.isSidebarOpen }"
>
<div class="sidebar-inner flex flex-column h-full p-3">
<div class="logo flex align-items-center gap-3 px-2 py-3 mb-3 border-bottom-1 border-color flex-shrink-0">
<i class="pi pi-graduation-cap text-primary text-3xl"></i>
<span class="font-bold text-xl text-color">گام نو</span>
</div>
<div class="menu-list flex-grow-1 overflow-y-auto">
<div v-for="group in menuGroups" :key="group.key" class="menu-group mb-3">
<div class="menu-group-label px-3 py-2 text-xs font-bold text-muted uppercase">
{{ group.label }}
</div>
<router-link
v-for="item in group.items"
:key="item.to"
:to="item.to"
class="menu-item flex align-items-center gap-3 px-3 py-2 border-round text-color mb-1 transition-colors"
:class="{ 'active-item': isMenuActive(item.to) }"
>
<i :class="item.icon" class="text-lg"></i>
<span class="font-medium text-sm">{{ item.label }}</span>
</router-link>
</div>
</div>
</div>
</aside>
<!-- Mobile Drawer & Backdrop -->
<Teleport to="body">
<Transition name="sidebar-fade">
<div
v-if="uiStore.isMobileSidebarOpen"
class="mobile-sidebar-backdrop"
@click="uiStore.closeMobileSidebar"
/>
</Transition>
<Transition name="sidebar-slide">
<div
v-if="uiStore.isMobileSidebarOpen"
class="mobile-sidebar-drawer surface-card flex flex-column p-3 shadow-8 border-left-1 border-color"
dir="rtl"
>
<div class="flex align-items-center justify-content-between px-2 py-3 mb-3 border-bottom-1 border-color flex-shrink-0">
<div class="logo flex align-items-center gap-2">
<i class="pi pi-graduation-cap text-primary text-2xl"></i>
<span class="font-bold text-lg text-color">گام نو</span>
</div>
<Button
icon="pi pi-times"
text
rounded
severity="secondary"
size="small"
@click="uiStore.closeMobileSidebar"
/>
</div>
<div class="menu-list flex-grow-1 overflow-y-auto">
<div v-for="group in menuGroups" :key="group.key" class="menu-group mb-3">
<div class="menu-group-label px-3 py-2 text-xs font-bold text-muted uppercase">
{{ group.label }}
</div>
<router-link
v-for="item in group.items"
:key="item.to"
:to="item.to"
class="menu-item flex align-items-center gap-3 px-3 py-2 border-round text-color mb-1 transition-colors"
:class="{ 'active-item': isMenuActive(item.to) }"
@click="uiStore.closeMobileSidebar"
>
<i :class="item.icon" class="text-lg"></i>
<span class="font-medium text-sm">{{ item.label }}</span>
</router-link>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup>
import { computed, watch } from 'vue';
import { useRoute } from 'vue-router';
import { usePermissionStore } from '@/stores/permissionStore';
import { useUiStore } from '@/stores/uiStore';
import { PERMISSIONS } from '@/constants/permissions';
import Button from 'primevue/button';
const route = useRoute();
const permissionStore = usePermissionStore();
const uiStore = useUiStore();
// Automatically close mobile sidebar on navigation
watch(
() => route.path,
() => {
uiStore.closeMobileSidebar();
}
);
const menuGroups = computed(() => {
const groups = [
{
key: 'main',
label: 'اصلی',
items: [
{ label: 'داشبورد اصلی', icon: 'pi pi-home', to: '/' }
]
},
{
key: 'education',
label: 'آموزش',
items: [
{ label: 'مدیریت کاربران', icon: 'pi pi-users', to: '/users' },
{ label: 'مدیریت اساتید', icon: 'pi pi-id-card', to: '/professors' },
{ label: 'دوره‌های آموزشی', icon: 'pi pi-book', to: '/courses' },
{ label: 'کلاس‌ها', icon: 'pi pi-desktop', to: '/classes' },
{ label: 'لیست انتظار', icon: 'pi pi-clock', to: '/waitlist', permission: PERMISSIONS.WAITLIST_READ },
{ label: 'جلسات آموزشی', icon: 'pi pi-calendar', to: '/sessions' }
]
},
{
key: 'finance',
label: 'مالی و ارتباطات',
items: [
{ label: 'امور مالی و پرداخت‌ها', icon: 'pi pi-wallet', to: '/payments' },
{ label: 'گزارش‌های مالی', icon: 'pi pi-chart-line', to: '/financial-reports', permission: PERMISSIONS.FINANCIAL_REPORTS_READ },
{ label: 'هزینه‌های موسسه', icon: 'pi pi-money-bill', to: '/expenses', permission: PERMISSIONS.EXPENSES_READ },
{ label: 'اطلاعیه‌ها', icon: 'pi pi-bell', to: '/notifications' },
{ label: 'درخواست‌های تماس', icon: 'pi pi-comments', to: '/contact-inquiries' },
{ label: 'ثبت‌نام‌های در انتظار', icon: 'pi pi-user-plus', to: '/pending-students', permission: PERMISSIONS.PENDING_STUDENTS_READ }
]
},
{
key: 'system',
label: 'مدیریت سیستم',
items: [
{ label: 'ورود و خروج کارمندان', icon: 'pi pi-clock', to: '/employee-timings', permission: PERMISSIONS.EMPLOYEE_TIMINGS_READ },
{ label: 'گزارش فعالیت‌ها', icon: 'pi pi-history', to: '/logs', permission: PERMISSIONS.LOGS_READ },
{ label: 'نقش‌ها و دسترسی‌ها', icon: 'pi pi-shield', to: '/roles' }
]
},
{
key: 'account',
label: 'حساب کاربری',
items: [
{ label: 'رابط کاربری', icon: 'pi pi-palette', to: '/interface' }
]
}
];
if (permissionStore.hasPermission(PERMISSIONS.SESSIONS_ATTENDANCE)) {
const educationGroup = groups.find((g) => g.key === 'education');
educationGroup.items.push({
label: 'حضور و غیاب',
icon: 'pi pi-check-square',
to: '/attendance'
});
}
if (permissionStore.roleName === 'SuperAdmin') {
const systemGroup = groups.find((g) => g.key === 'system');
systemGroup.items.push({ label: 'قالب‌های اعلان', icon: 'pi pi-send', to: '/notification-templates' });
systemGroup.items.push({ label: 'تنظیمات', icon: 'pi pi-cog', to: '/settings' });
}
return groups
.map((group) => ({
...group,
items: group.items.filter((item) => !item.permission || permissionStore.hasPermission(item.permission))
}))
.filter((group) => group.items.length > 0);
});
function isMenuActive(to) {
if (to === '/') {
return route.path === '/';
}
if (to === '/attendance') {
return route.path === '/attendance' || route.path.startsWith('/sessions/attendance/');
}
return route.path === to || route.path.startsWith(`${to}/`);
}
</script>
<style lang="scss" scoped>
.app-sidebar {
width: 240px;
min-width: 240px;
max-width: 240px;
flex-shrink: 0;
min-height: 100vh;
transition: width 0.25s ease, min-width 0.25s ease, max-width 0.25s ease, opacity 0.2s ease, padding 0.25s ease;
overflow: hidden;
.sidebar-inner {
width: 240px;
min-width: 240px;
}
&.sidebar-collapsed {
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
padding: 0 !important;
border-left: none !important;
border-right: none !important;
opacity: 0;
pointer-events: none;
}
}
@media (max-width: 991px) {
.desktop-sidebar {
display: none !important;
}
}
.mobile-sidebar-backdrop {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.65);
backdrop-filter: blur(3px);
z-index: 1100;
}
.mobile-sidebar-drawer {
position: fixed;
top: 0;
right: 0;
bottom: 0;
width: 280px;
max-width: 85vw;
height: 100vh;
height: 100dvh;
z-index: 1101;
background: var(--surface-card);
box-sizing: border-box;
}
.sidebar-slide-enter-active,
.sidebar-slide-leave-active {
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar-slide-enter-from,
.sidebar-slide-leave-to {
transform: translateX(100%);
}
.sidebar-fade-enter-active,
.sidebar-fade-leave-active {
transition: opacity 0.2s ease;
}
.sidebar-fade-enter-from,
.sidebar-fade-leave-to {
opacity: 0;
}
.menu-group-label {
letter-spacing: 0.04em;
opacity: 0.85;
}
.menu-item {
color: var(--text-secondary);
text-decoration: none;
white-space: nowrap;
flex-shrink: 0;
&:hover {
background-color: var(--surface-hover);
color: var(--text-primary);
}
&.active-item {
background-color: var(--primary-color-light);
color: var(--primary-color);
font-weight: 700;
}
span {
white-space: nowrap;
}
}
</style>