295 lines
9.0 KiB
Vue
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>
|