(() => { 'use strict'; const token = localStorage.getItem('token'); const role = localStorage.getItem('role'); if (!token || role !== 'ADMIN') { window.location.href = '/'; return; } // ---- DOM refs ---- const pageTitle = document.getElementById('page-title'); const btnLogout = document.getElementById('btn-logout'); const menuToggle = document.getElementById('menu-toggle'); const sidebar = document.querySelector('.sidebar'); const sidebarOverlay = document.getElementById('sidebar-overlay'); // Global Ripple Effect document.addEventListener('click', function (e) { const btn = e.target.closest('.btn-create, .btn-delete, .btn-logout'); if (!btn) return; const rect = btn.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const ripple = document.createElement('span'); ripple.classList.add('ripple'); ripple.style.left = `${x}px`; ripple.style.top = `${y}px`; if (getComputedStyle(btn).position === 'static') { btn.style.position = 'relative'; } btn.style.overflow = 'hidden'; btn.appendChild(ripple); setTimeout(() => ripple.remove(), 600); }); // Users const usersTbody = document.getElementById('users-tbody'); const createForm = document.getElementById('create-form'); const createAlert = document.getElementById('create-alert'); // Groups const groupsTbody = document.getElementById('groups-tbody'); const createGroupForm = document.getElementById('create-group-form'); const createGroupAlert = document.getElementById('create-group-alert'); const newGroupEfSelect = document.getElementById('new-group-ef'); const filterEfSelect = document.getElementById('filter-ef'); // Education Forms const efTbody = document.getElementById('ef-tbody'); const createEfForm = document.getElementById('create-ef-form'); const createEfAlert = document.getElementById('create-ef-alert'); // Classrooms const classroomsTbody = document.getElementById('classrooms-tbody'); const createClassroomForm = document.getElementById('create-classroom-form'); const createClassroomAlert = document.getElementById('create-classroom-alert'); const modalEditClassroom = document.getElementById('modal-edit-classroom'); const modalEditClassroomClose = document.getElementById('modal-edit-classroom-close'); const editClassroomForm = document.getElementById('edit-classroom-form'); const editClassroomAlert = document.getElementById('edit-classroom-alert'); const editEquipmentCheckboxes = document.getElementById('edit-equipment-checkboxes'); // Equipments const equipmentsTbody = document.getElementById('equipments-tbody'); const createEquipmentForm = document.getElementById('create-equipment-form'); const createEquipmentAlert = document.getElementById('create-equipment-alert'); const equipmentCheckboxes = document.getElementById('equipment-checkboxes'); const navItems = document.querySelectorAll('.nav-item[data-tab]'); const tabContents = document.querySelectorAll('.tab-content'); // ---- State ---- let allGroups = []; let allEducationForms = []; let allEquipments = []; // ---- Tab Switching ---- const TAB_TITLES = { users: 'Управление пользователями', groups: 'Управление группами', 'edu-forms': 'Формы обучения', equipments: 'Оборудование', classrooms: 'Аудитории' }; navItems.forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); switchTab(item.dataset.tab); }); }); function switchTab(tab) { navItems.forEach(n => n.classList.remove('active')); document.querySelector(`.nav-item[data-tab="${tab}"]`)?.classList.add('active'); tabContents.forEach(tc => tc.style.display = 'none'); const target = document.getElementById('tab-' + tab); if (target) target.style.display = ''; pageTitle.textContent = TAB_TITLES[tab] || ''; if (tab === 'users') loadUsers(); if (tab === 'groups') { loadEducationForms().then(() => loadGroups()); } if (tab === 'edu-forms') loadEducationForms(); if (tab === 'equipments') loadEquipments(); if (tab === 'classrooms') { loadEquipments().then(() => loadClassrooms()); } sidebar.classList.remove('open'); sidebarOverlay.classList.remove('open'); } // ---- Mobile Menu ---- menuToggle.addEventListener('click', () => { sidebar.classList.toggle('open'); sidebarOverlay.classList.toggle('open'); }); sidebarOverlay.addEventListener('click', () => { sidebar.classList.remove('open'); sidebarOverlay.classList.remove('open'); }); // ---- Helpers ---- const ROLE_LABELS = { ADMIN: 'Администратор', TEACHER: 'Преподаватель', STUDENT: 'Студент' }; const ROLE_BADGE = { ADMIN: 'badge-admin', TEACHER: 'badge-teacher', STUDENT: 'badge-student' }; function escapeHtml(str) { const div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function showAlert(el, msg, type) { el.className = 'form-alert ' + type; el.textContent = msg; } function hideAlert(el) { el.className = 'form-alert'; el.textContent = ''; } // ============================================================ // USERS // ============================================================ async function loadUsers() { try { const res = await fetch('/api/users', { headers: { 'Authorization': 'Bearer ' + token }, }); const users = await res.json(); renderUsers(users); } catch (e) { usersTbody.innerHTML = 'Ошибка загрузки'; } } function renderUsers(users) { if (!users.length) { usersTbody.innerHTML = 'Нет пользователей'; return; } usersTbody.innerHTML = users.map(u => ` ${u.id} ${escapeHtml(u.username)} ${ROLE_LABELS[u.role] || u.role} `).join(''); } createForm.addEventListener('submit', async (e) => { e.preventDefault(); hideAlert(createAlert); const username = document.getElementById('new-username').value.trim(); const password = document.getElementById('new-password').value; const role = document.getElementById('new-role').value; if (!username || !password) { showAlert(createAlert, 'Заполните все поля', 'error'); return; } try { const res = await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ username, password, role }), }); const data = await res.json(); if (res.ok) { showAlert(createAlert, `Пользователь "${data.username}" создан`, 'success'); createForm.reset(); loadUsers(); } else { showAlert(createAlert, data.message || 'Ошибка создания', 'error'); } } catch (e) { showAlert(createAlert, 'Ошибка соединения', 'error'); } }); usersTbody.addEventListener('click', async (e) => { const btn = e.target.closest('.btn-delete'); if (!btn) return; if (!confirm('Удалить пользователя?')) return; try { const res = await fetch('/api/users/' + btn.dataset.id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token }, }); if (res.ok) loadUsers(); else alert('Ошибка удаления'); } catch (e) { alert('Ошибка соединения'); } }); // ============================================================ // EDUCATION FORMS // ============================================================ async function loadEducationForms() { try { const res = await fetch('/api/education-forms', { headers: { 'Authorization': 'Bearer ' + token }, }); allEducationForms = await res.json(); renderEfTable(allEducationForms); populateEfSelects(allEducationForms); } catch (e) { efTbody.innerHTML = 'Ошибка загрузки'; } } function renderEfTable(forms) { if (!forms.length) { efTbody.innerHTML = 'Нет форм обучения'; return; } efTbody.innerHTML = forms.map(ef => ` ${ef.id} ${escapeHtml(ef.name)} `).join(''); } function populateEfSelects(forms) { // Group creation select const currentVal = newGroupEfSelect.value; newGroupEfSelect.innerHTML = forms.map(ef => `` ).join(''); if (currentVal && forms.find(f => f.id == currentVal)) { newGroupEfSelect.value = currentVal; } // Filter select const currentFilter = filterEfSelect.value; filterEfSelect.innerHTML = '' + forms.map(ef => `` ).join(''); if (currentFilter) filterEfSelect.value = currentFilter; } createEfForm.addEventListener('submit', async (e) => { e.preventDefault(); hideAlert(createEfAlert); const name = document.getElementById('new-ef-name').value.trim(); if (!name) { showAlert(createEfAlert, 'Введите название', 'error'); return; } try { const res = await fetch('/api/education-forms', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ name }), }); const data = await res.json(); if (res.ok) { showAlert(createEfAlert, `Форма "${data.name}" создана`, 'success'); createEfForm.reset(); loadEducationForms(); } else { showAlert(createEfAlert, data.message || 'Ошибка создания', 'error'); } } catch (e) { showAlert(createEfAlert, 'Ошибка соединения', 'error'); } }); efTbody.addEventListener('click', async (e) => { const btn = e.target.closest('.btn-delete'); if (!btn) return; if (!confirm('Удалить форму обучения?')) return; try { const res = await fetch('/api/education-forms/' + btn.dataset.id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token }, }); if (res.ok) { loadEducationForms(); } else { const data = await res.json(); alert(data.message || 'Ошибка удаления'); } } catch (e) { alert('Ошибка соединения'); } }); // ============================================================ // GROUPS // ============================================================ async function loadGroups() { try { const res = await fetch('/api/groups', { headers: { 'Authorization': 'Bearer ' + token }, }); allGroups = await res.json(); applyGroupFilter(); } catch (e) { groupsTbody.innerHTML = 'Ошибка загрузки'; } } function applyGroupFilter() { const filterId = filterEfSelect.value; const filtered = filterId ? allGroups.filter(g => g.educationFormId == filterId) : allGroups; renderGroups(filtered); } filterEfSelect.addEventListener('change', applyGroupFilter); function renderGroups(groups) { if (!groups.length) { groupsTbody.innerHTML = 'Нет групп'; return; } groupsTbody.innerHTML = groups.map(g => ` ${g.id} ${escapeHtml(g.name)} ${escapeHtml(g.educationFormName)} `).join(''); } createGroupForm.addEventListener('submit', async (e) => { e.preventDefault(); hideAlert(createGroupAlert); const name = document.getElementById('new-group-name').value.trim(); const educationFormId = newGroupEfSelect.value; if (!name) { showAlert(createGroupAlert, 'Введите название группы', 'error'); return; } if (!educationFormId) { showAlert(createGroupAlert, 'Выберите форму обучения', 'error'); return; } try { const res = await fetch('/api/groups', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ name, educationFormId: Number(educationFormId) }), }); const data = await res.json(); if (res.ok) { showAlert(createGroupAlert, `Группа "${data.name}" создана`, 'success'); createGroupForm.reset(); loadGroups(); } else { showAlert(createGroupAlert, data.message || 'Ошибка создания', 'error'); } } catch (e) { showAlert(createGroupAlert, 'Ошибка соединения', 'error'); } }); groupsTbody.addEventListener('click', async (e) => { const btn = e.target.closest('.btn-delete'); if (!btn) return; if (!confirm('Удалить группу?')) return; try { const res = await fetch('/api/groups/' + btn.dataset.id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token }, }); if (res.ok) loadGroups(); else alert('Ошибка удаления'); } catch (e) { alert('Ошибка соединения'); } }); // ============================================================ // EQUIPMENTS // ============================================================ async function loadEquipments() { try { const res = await fetch('/api/equipments', { headers: { 'Authorization': 'Bearer ' + token }, }); allEquipments = await res.json(); renderEquipments(allEquipments); renderEquipmentCheckboxes(allEquipments); } catch (e) { if (equipmentsTbody) equipmentsTbody.innerHTML = 'Ошибка загрузки'; if (equipmentCheckboxes) equipmentCheckboxes.innerHTML = '

Ошибка загрузки

'; } } function renderEquipments(equipments) { if (!equipments.length) { equipmentsTbody.innerHTML = 'Нет оборудования'; return; } equipmentsTbody.innerHTML = equipments.map(eq => ` ${eq.id} ${escapeHtml(eq.name)} `).join(''); } function renderEquipmentCheckboxes(equipments) { if (!equipments.length) { equipmentCheckboxes.innerHTML = '

Нет доступного оборудования

'; return; } equipmentCheckboxes.innerHTML = equipments.map(eq => ` `).join(''); } createEquipmentForm.addEventListener('submit', async (e) => { e.preventDefault(); hideAlert(createEquipmentAlert); const name = document.getElementById('new-equipment-name').value.trim(); if (!name) { showAlert(createEquipmentAlert, 'Введите название', 'error'); return; } try { const res = await fetch('/api/equipments', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ name }), }); const data = await res.json(); if (res.ok) { showAlert(createEquipmentAlert, `Оборудование "${data.name}" добавлено`, 'success'); createEquipmentForm.reset(); loadEquipments(); } else { showAlert(createEquipmentAlert, data.message || 'Ошибка создания', 'error'); } } catch (e) { showAlert(createEquipmentAlert, 'Ошибка соединения', 'error'); } }); equipmentsTbody.addEventListener('click', async (e) => { const btn = e.target.closest('.btn-delete'); if (!btn) return; if (!confirm('Удалить оборудование?')) return; try { const res = await fetch('/api/equipments/' + btn.dataset.id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token }, }); if (res.ok) { loadEquipments(); } else { const data = await res.json(); alert(data.message || 'Ошибка удаления'); } } catch (e) { alert('Ошибка соединения'); } }); // ============================================================ // CLASSROOMS // ============================================================ async function loadClassrooms() { try { const res = await fetch('/api/classrooms', { headers: { 'Authorization': 'Bearer ' + token }, }); const classrooms = await res.json(); renderClassrooms(classrooms); } catch (e) { classroomsTbody.innerHTML = 'Ошибка загрузки'; } } function renderClassrooms(classrooms) { if (!classrooms.length) { classroomsTbody.innerHTML = 'Нет аудиторий'; return; } classroomsTbody.innerHTML = classrooms.map(c => { const equipHtml = c.equipments && c.equipments.length ? c.equipments.map(eq => escapeHtml(eq.name)).join(', ') : '—'; return ` ${c.id} ${escapeHtml(c.name)} ${c.capacity} чел. ${equipHtml}
${c.isAvailable ? 'Доступна' : 'Не доступна'}
`; }).join(''); } createClassroomForm.addEventListener('submit', async (e) => { e.preventDefault(); hideAlert(createClassroomAlert); const name = document.getElementById('new-classroom-name').value.trim(); const capacity = parseInt(document.getElementById('new-classroom-capacity').value, 10); const checkedBoxes = Array.from(equipmentCheckboxes.querySelectorAll('input:checked')); const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10)); if (!name || isNaN(capacity)) { showAlert(createClassroomAlert, 'Заполните обязательные поля', 'error'); return; } try { const res = await fetch('/api/classrooms', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ name, capacity, equipmentIds, isAvailable: true }), }); const data = await res.json(); if (res.ok) { showAlert(createClassroomAlert, `Аудитория "${data.name}" добавлена`, 'success'); createClassroomForm.reset(); loadClassrooms(); } else { showAlert(createClassroomAlert, data.message || 'Ошибка создания', 'error'); } } catch (e) { showAlert(createClassroomAlert, 'Ошибка соединения', 'error'); } }); classroomsTbody.addEventListener('click', async (e) => { const btnDelete = e.target.closest('.btn-delete'); const btnToggleStatus = e.target.closest('.btn-icon-toggle'); const btnEdit = e.target.closest('.btn-edit-classroom'); if (btnDelete) { if (!confirm('Удалить аудиторию?')) return; try { const res = await fetch('/api/classrooms/' + btnDelete.dataset.id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token }, }); if (res.ok) loadClassrooms(); else alert('Ошибка удаления'); } catch (err) { alert('Ошибка соединения'); } } if (btnToggleStatus) { const id = btnToggleStatus.dataset.id; const currentStatus = btnToggleStatus.dataset.currentStatus === 'true'; try { const res = await fetch('/api/classrooms/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ isAvailable: !currentStatus }), }); if (res.ok) loadClassrooms(); else alert('Ошибка изменения статуса'); } catch (err) { alert('Ошибка соединения'); } } if (btnEdit) { const id = btnEdit.dataset.id; openEditClassroomModal(id); } }); let editingClassroomData = null; async function openEditClassroomModal(id) { try { const res = await fetch('/api/classrooms', { headers: { 'Authorization': 'Bearer ' + token } }); const classrooms = await res.json(); editingClassroomData = classrooms.find(c => c.id == id); if (!editingClassroomData) return; document.getElementById('edit-classroom-id').value = editingClassroomData.id; document.getElementById('edit-classroom-name').value = editingClassroomData.name; document.getElementById('edit-classroom-capacity').value = editingClassroomData.capacity; if (allEquipments.length) { editEquipmentCheckboxes.innerHTML = allEquipments.map(eq => { const isChecked = editingClassroomData.equipments.some(e => e.id === eq.id) ? 'checked' : ''; return ` `; }).join(''); } else { editEquipmentCheckboxes.innerHTML = '

Нет доступного оборудования

'; } hideAlert(editClassroomAlert); modalEditClassroom.classList.add('open'); } catch (e) { alert('Ошибка загрузки данных аудитории'); } } modalEditClassroomClose.addEventListener('click', () => { modalEditClassroom.classList.remove('open'); }); modalEditClassroom.addEventListener('click', (e) => { if (e.target === modalEditClassroom) { modalEditClassroom.classList.remove('open'); } }); editClassroomForm.addEventListener('submit', async (e) => { e.preventDefault(); hideAlert(editClassroomAlert); const id = document.getElementById('edit-classroom-id').value; const name = document.getElementById('edit-classroom-name').value.trim(); const capacity = parseInt(document.getElementById('edit-classroom-capacity').value, 10); const checkedBoxes = Array.from(editEquipmentCheckboxes.querySelectorAll('input:checked')); const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10)); if (!name || isNaN(capacity)) { showAlert(editClassroomAlert, 'Заполните обязательные поля', 'error'); return; } try { const res = await fetch('/api/classrooms/' + id, { method: 'PUT', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ name, capacity, equipmentIds, isAvailable: editingClassroomData.isAvailable }), }); const data = await res.json(); if (res.ok) { modalEditClassroom.classList.remove('open'); showAlert(createClassroomAlert, `Аудитория "${data.name}" обновлена`, 'success'); loadClassrooms(); } else { showAlert(editClassroomAlert, data.message || 'Ошибка обновления', 'error'); } } catch (e) { showAlert(editClassroomAlert, 'Ошибка соединения', 'error'); } }); // ============================================================ // LOGOUT & INIT // ============================================================ btnLogout.addEventListener('click', () => { localStorage.removeItem('token'); localStorage.removeItem('role'); window.location.href = '/'; }); loadUsers(); })();