304 lines
10 KiB
JavaScript
Executable File
304 lines
10 KiB
JavaScript
Executable File
import { api, initializeSession, logout } from './api.js';
|
||
import { applyRippleEffect, closeAllDropdownsOnOutsideClick, renderStatusMessage } from './utils.js';
|
||
import { startDropdownAutoObserver, initAllCustomDropdowns } from './dropdown.js';
|
||
import { ADMIN_APP_ROLES, SETTINGS_ROLES, capabilitiesForRole } from './role-capabilities.js';
|
||
import { initializeTelemetry } from '../../telemetry.js';
|
||
|
||
async function bootstrap() {
|
||
const session = await initializeSession();
|
||
if (!session || !ADMIN_APP_ROLES.includes(session.role)) {
|
||
window.location.replace('/');
|
||
return;
|
||
}
|
||
const currentRole = session.role;
|
||
void initializeTelemetry();
|
||
|
||
// Global initialization for Custom Selects
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
initAllCustomDropdowns(document.body);
|
||
startDropdownAutoObserver();
|
||
});
|
||
|
||
// Configuration
|
||
const ROUTES = {
|
||
dashboard: {
|
||
title: 'Дашборд',
|
||
file: '/admin/views/dashboard.html',
|
||
init: async () => {
|
||
const module = await import('./views/dashboard.js');
|
||
await module.initDashboard();
|
||
}
|
||
},
|
||
users: {
|
||
title: 'Управление пользователями',
|
||
file: '/admin/views/users.html',
|
||
init: async () => {
|
||
const module = await import('./views/users.js');
|
||
await module.initUsers();
|
||
}
|
||
},
|
||
'teacher-requests': {
|
||
title: 'Заявки',
|
||
file: '/admin/views/teacher-requests.html',
|
||
init: async () => {
|
||
const module = await import('./views/teacher-requests.js');
|
||
await module.initTeacherRequests();
|
||
}
|
||
},
|
||
groups: {
|
||
title: 'Управление группами',
|
||
file: '/admin/views/groups.html',
|
||
init: async () => {
|
||
const module = await import('./views/groups.js');
|
||
await module.initGroups();
|
||
}
|
||
},
|
||
classrooms: {
|
||
title: 'Аудиторный фонд',
|
||
file: '/admin/views/classrooms.html',
|
||
init: async () => {
|
||
const module = await import('./views/classrooms.js');
|
||
await module.initClassrooms();
|
||
}
|
||
},
|
||
subjects: {
|
||
title: 'Дисциплины',
|
||
file: '/admin/views/subjects.html',
|
||
init: async () => {
|
||
const module = await import('./views/subjects.js');
|
||
await module.initSubjects();
|
||
}
|
||
},
|
||
'university-structure': {
|
||
title: 'Структура вуза',
|
||
file: '/admin/views/university-structure.html',
|
||
init: async () => {
|
||
const module = await import('./views/university-structure.js');
|
||
await module.initUniversityStructure();
|
||
}
|
||
},
|
||
'department-workspace': {
|
||
title: 'Кабинет кафедры',
|
||
file: '/admin/views/department-workspace.html',
|
||
init: async () => {
|
||
const module = await import('./views/department-workspace.js');
|
||
await module.initDepartmentWorkspace();
|
||
}
|
||
},
|
||
'schedule-view': {
|
||
title: 'Просмотр расписаний',
|
||
file: '/admin/views/schedule-view.html',
|
||
init: async () => {
|
||
const module = await import('./views/schedule-view.js');
|
||
await module.initScheduleView();
|
||
}
|
||
},
|
||
schedule: {
|
||
title: 'Конструктор правил',
|
||
file: '/admin/views/schedule.html',
|
||
init: async () => {
|
||
const module = await import('./views/schedule.js');
|
||
await module.initSchedule();
|
||
}
|
||
},
|
||
'academic-calendar': {
|
||
title: 'Календарный учебный график',
|
||
file: '/admin/views/academic-calendar.html',
|
||
init: async () => {
|
||
const module = await import('./views/academic-calendar.js');
|
||
await module.initAcademicCalendar();
|
||
}
|
||
},
|
||
'auditorium-workload': {
|
||
title: 'Загруженность',
|
||
file: '/admin/views/auditorium-workload.html',
|
||
init: async () => {
|
||
const module = await import('./views/auditorium-workload.js');
|
||
await module.initAuditoriumWorkload();
|
||
}
|
||
},
|
||
};
|
||
|
||
let currentTab = null;
|
||
|
||
// DOM Elements
|
||
const appContent = document.getElementById('app-content');
|
||
const pageTitle = document.getElementById('page-title');
|
||
const navItems = document.querySelectorAll('.nav-item[data-tab]');
|
||
const sidebar = document.querySelector('.sidebar');
|
||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||
const sidebarCloseBtn = document.getElementById('sidebar-close-btn');
|
||
const btnLogout = document.getElementById('btn-logout');
|
||
const main = document.querySelector('.main');
|
||
const teacherRequestsNavBadge = document.getElementById('teacher-requests-nav-badge');
|
||
const teacherRequestsTitleBadge = document.getElementById('teacher-requests-title-badge');
|
||
const roleCapabilities = capabilitiesForRole(currentRole);
|
||
const allowedTabs = new Set(roleCapabilities.adminTabs);
|
||
|
||
// Setup Global Effects
|
||
applyRippleEffect();
|
||
closeAllDropdownsOnOutsideClick();
|
||
configureRoleInterface();
|
||
refreshTeacherRequestBadge();
|
||
|
||
// Init sidebar state from localStorage on load
|
||
if (window.innerWidth > 768 && localStorage.getItem('sidebar-collapsed') === 'true') {
|
||
sidebar.classList.add('collapsed');
|
||
main.classList.add('sidebar-collapsed');
|
||
}
|
||
|
||
// Sidebar Close (X button)
|
||
sidebarCloseBtn?.addEventListener('click', () => {
|
||
if (window.innerWidth <= 768) {
|
||
sidebar.classList.remove('open');
|
||
sidebarOverlay.classList.remove('open');
|
||
} else {
|
||
sidebar.classList.toggle('collapsed');
|
||
main.classList.toggle('sidebar-collapsed');
|
||
localStorage.setItem('sidebar-collapsed', sidebar.classList.contains('collapsed'));
|
||
}
|
||
});
|
||
|
||
sidebarOverlay.addEventListener('click', () => {
|
||
sidebar.classList.remove('open');
|
||
sidebarOverlay.classList.remove('open');
|
||
});
|
||
|
||
// Settings Dropdown
|
||
const settingsDropdown = document.getElementById('settings-dropdown');
|
||
const btnSettings = document.getElementById('btn-settings');
|
||
|
||
btnSettings.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
settingsDropdown.classList.toggle('open');
|
||
});
|
||
|
||
document.addEventListener('click', (e) => {
|
||
if (!settingsDropdown.contains(e.target)) {
|
||
settingsDropdown.classList.remove('open');
|
||
}
|
||
});
|
||
|
||
// Logout
|
||
btnLogout.addEventListener('click', async () => {
|
||
await logout();
|
||
window.location.href = '/';
|
||
});
|
||
|
||
teacherRequestsTitleBadge?.addEventListener('click', () => {
|
||
switchTab('teacher-requests');
|
||
});
|
||
|
||
// Navigation
|
||
navItems.forEach(item => {
|
||
item.addEventListener('click', (e) => {
|
||
e.preventDefault();
|
||
const tab = item.dataset.tab;
|
||
switchTab(tab);
|
||
});
|
||
});
|
||
|
||
async function switchTab(tab) {
|
||
if (currentTab === tab || !ROUTES[tab] || !allowedTabs.has(tab)) return;
|
||
|
||
// UI Update
|
||
navItems.forEach(n => n.classList.remove('active'));
|
||
document.querySelector(`.nav-item[data-tab="${tab}"]`)?.classList.add('active');
|
||
pageTitle.textContent = ROUTES[tab].title;
|
||
|
||
// Load template
|
||
try {
|
||
appContent.innerHTML = '<div class="loading-row">Загрузка...</div>';
|
||
const response = await fetch(ROUTES[tab].file);
|
||
if (!response.ok) throw new Error('Не удалось загрузить вкладку');
|
||
|
||
const html = await response.text();
|
||
appContent.innerHTML = html;
|
||
|
||
// Initialize logic for the tab
|
||
if (ROUTES[tab].init) {
|
||
await ROUTES[tab].init();
|
||
}
|
||
|
||
currentTab = tab;
|
||
if (window.location.hash !== `#${tab}`) {
|
||
window.location.hash = tab;
|
||
}
|
||
} catch (e) {
|
||
renderStatusMessage(appContent, 'Не удалось загрузить выбранную вкладку');
|
||
console.error('Не удалось загрузить вкладку');
|
||
}
|
||
|
||
// Close mobile menu if open
|
||
sidebar.classList.remove('open');
|
||
sidebarOverlay.classList.remove('open');
|
||
}
|
||
|
||
function configureRoleInterface() {
|
||
navItems.forEach(item => {
|
||
item.hidden = !allowedTabs.has(item.dataset.tab);
|
||
});
|
||
|
||
const settingsLink = document.querySelector('.settings-menu a[href="/admin/settings/"]');
|
||
if (settingsLink) {
|
||
settingsLink.hidden = !SETTINGS_ROLES.includes(currentRole);
|
||
}
|
||
|
||
const firstAllowedTitle = {
|
||
ADMIN: 'Magistr',
|
||
EDUCATION_OFFICE: 'Учебный отдел',
|
||
DEPARTMENT: 'Кафедра',
|
||
SCHEDULE_VIEWER: 'Расписания'
|
||
}[currentRole] || 'Magistr';
|
||
document.querySelector('.logo span').textContent = firstAllowedTitle;
|
||
}
|
||
|
||
async function refreshTeacherRequestBadge(countOverride = null) {
|
||
if (currentRole !== 'ADMIN') return;
|
||
try {
|
||
const count = countOverride === null
|
||
? (await api.get('/api/teacher-requests?status=PENDING') || []).length
|
||
: countOverride;
|
||
updateTeacherRequestBadge(teacherRequestsNavBadge, count, count > 99 ? '99+' : String(count));
|
||
updateTeacherRequestBadge(teacherRequestsTitleBadge, count, requestCountLabel(count));
|
||
} catch (error) {
|
||
if (teacherRequestsNavBadge) teacherRequestsNavBadge.hidden = true;
|
||
if (teacherRequestsTitleBadge) teacherRequestsTitleBadge.hidden = true;
|
||
console.warn('Не удалось загрузить счётчик заявок');
|
||
}
|
||
}
|
||
|
||
function updateTeacherRequestBadge(element, count, label) {
|
||
if (!element) return;
|
||
const normalizedCount = Number(count) || 0;
|
||
element.textContent = label;
|
||
element.hidden = normalizedCount <= 0;
|
||
element.setAttribute('aria-hidden', normalizedCount <= 0 ? 'true' : 'false');
|
||
element.setAttribute('aria-label', normalizedCount > 0 ? `Заявок в очереди: ${normalizedCount}` : 'Заявок в очереди нет');
|
||
}
|
||
|
||
function requestCountLabel(count) {
|
||
const mod10 = count % 10;
|
||
const mod100 = count % 100;
|
||
if (mod10 === 1 && mod100 !== 11) return `${count} заявка`;
|
||
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return `${count} заявки`;
|
||
return `${count} заявок`;
|
||
}
|
||
|
||
document.addEventListener('teacherRequestsChanged', (event) => {
|
||
refreshTeacherRequestBadge(Number.isFinite(event.detail?.count) ? event.detail.count : null);
|
||
});
|
||
|
||
const hashTab = window.location.hash.replace('#', '');
|
||
switchTab(allowedTabs.has(hashTab) ? hashTab : roleCapabilities.defaultAdminTab);
|
||
|
||
window.addEventListener('hashchange', () => {
|
||
const hashTab = window.location.hash.replace('#', '');
|
||
if (allowedTabs.has(hashTab) && hashTab !== currentTab) {
|
||
switchTab(hashTab);
|
||
}
|
||
});
|
||
}
|
||
|
||
await bootstrap();
|