From 6993ac29d5a39c1f156777040846b95d830c79c1 Mon Sep 17 00:00:00 2001 From: Zuev Date: Fri, 20 Feb 2026 02:49:51 +0300 Subject: [PATCH] feat(admin): add classroom edit modal --- frontend/admin/admin.css | 137 ++++++++++++++---- frontend/admin/admin.js | 293 ++++++++++++++++++++++++++++++++++++++ frontend/admin/index.html | 138 ++++++++++++++++++ 3 files changed, 543 insertions(+), 25 deletions(-) diff --git a/frontend/admin/admin.css b/frontend/admin/admin.css index 9a7d458..0e522bd 100644 --- a/frontend/admin/admin.css +++ b/frontend/admin/admin.css @@ -145,6 +145,74 @@ body { transform: scale(1.1); } +/* Checkbox list styling */ +.checkbox-group { + display: flex; + flex-wrap: wrap; + gap: 12px; + padding: 8px 0; +} + +.checkbox-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 0.9rem; + color: var(--text-base); + cursor: pointer; +} + +.checkbox-item input[type="checkbox"] { + cursor: pointer; + width: 16px; + height: 16px; + accent-color: var(--primary-color); +} + +/* Classroom Status Badges */ +.badge-available { + background-color: var(--success-bg); + color: var(--success-color); +} + +.badge-unavailable { + background-color: var(--error-bg); + color: var(--error-color); +} + +.status-cell { + display: flex; + align-items: center; + gap: 8px; +} + +.btn-icon-toggle { + background: var(--bg-body); + border: 1px solid var(--border-color); + color: var(--text-muted); + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + padding: 0; +} + +.btn-icon-toggle:hover { + background: var(--card-bg); + border-color: var(--primary-color); + color: var(--primary-color); + transform: rotate(45deg); + box-shadow: 0 0 10px rgba(99, 102, 241, 0.2); +} + +.btn-icon-toggle svg { + display: block; +} + .nav-item.active { background: rgba(99, 102, 241, 0.12); color: var(--accent-hover); @@ -615,41 +683,60 @@ tbody tr:hover { } /* ===== Theme Toggle Button ===== */ -.theme-toggle { - width: 38px; - height: 38px; - border-radius: 50%; - background: var(--bg-input); - border: 1px solid var(--bg-card-border); - color: var(--text-primary); - cursor: pointer; - display: flex; + +/* ===== Modals ===== */ +.modal-overlay { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + backdrop-filter: blur(2px); align-items: center; justify-content: center; - transition: background 0.3s ease, transform 0.3s ease, box-shadow 0.3s ease; - flex-shrink: 0; + padding: 1rem; } -.theme-toggle svg { - width: 18px; - height: 18px; - transition: transform 0.4s ease; +.modal-overlay.open { + display: flex; + animation: fadeIn 0.2s ease; } -.theme-toggle:hover { - transform: scale(1.1); - box-shadow: 0 4px 16px var(--accent-glow); +.modal-content { + background: var(--bg-primary); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-md); + padding: 2rem; + width: 100%; + max-width: 600px; + position: relative; + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); } -.theme-toggle:active { - transform: scale(0.95); +.modal-content h2 { + font-size: 1.2rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--text-primary); + border-bottom: 1px solid var(--bg-card-border); + padding-bottom: 1rem; } -.theme-toggle--fixed { - position: fixed; - top: 1.25rem; - right: 1.25rem; - z-index: 100; +.modal-close { + position: absolute; + top: 1.5rem; + right: 1.5rem; + background: none; + border: none; + color: var(--text-secondary); + font-size: 1.5rem; + cursor: pointer; + line-height: 1; + transition: color var(--transition); +} + +.modal-close:hover { + color: var(--error); } /* ===== Responsive ===== */ diff --git a/frontend/admin/admin.js b/frontend/admin/admin.js index ba51454..599935c 100644 --- a/frontend/admin/admin.js +++ b/frontend/admin/admin.js @@ -57,18 +57,37 @@ 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 => { @@ -91,6 +110,8 @@ 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'); @@ -365,6 +386,278 @@ } 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 // ============================================================ diff --git a/frontend/admin/index.html b/frontend/admin/index.html index 1fa9ac6..fffe4ee 100644 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -59,6 +59,21 @@ Формы обучения + + + + + + Оборудование + + + + + + Аудитории + + + + + + + + + +