Files
magistr/frontend/admin/js/views/users.js

568 lines
22 KiB
JavaScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { api } from '../api.js';
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
const ROLE_LABELS = { ADMIN: 'Администратор', TEACHER: 'Преподаватель', STUDENT: 'Студент' };
const ROLE_BADGE = { ADMIN: 'badge-admin', TEACHER: 'badge-teacher', STUDENT: 'badge-student' };
export async function initUsers() {
const usersTbody = document.getElementById('users-tbody');
const createForm = document.getElementById('create-form');
const modalBackdrop = document.getElementById('modal-backdrop');
// ===== 1-е модальное окно: Добавить занятие =====
const modalAddLesson = document.getElementById('modal-add-lesson');
const modalAddLessonClose = document.getElementById('modal-add-lesson-close');
const addLessonForm = document.getElementById('add-lesson-form');
const lessonGroupSelect = document.getElementById('lesson-group');
const lessonDisciplineSelect = document.getElementById('lesson-discipline');
const lessonClassroomSelect = document.getElementById('lesson-classroom');
const lessonTypeSelect = document.getElementById('lesson-type');
const lessonOnlineFormat = document.getElementById('format-online');
const lessonOfflineFormat = document.getElementById('format-offline');
const lessonUserId = document.getElementById('lesson-user-id');
const lessonDaySelect = document.getElementById('lesson-day');
const weekUpper = document.getElementById('week-upper');
const weekLower = document.getElementById('week-lower');
const lessonTimeSelect = document.getElementById('lesson-time');
// ===== 2-е модальное окно: Просмотр занятий =====
const modalViewLessons = document.getElementById('modal-view-lessons');
const modalViewLessonsClose = document.getElementById('modal-view-lessons-close');
const lessonsContainer = document.getElementById('lessons-container');
const modalTeacherName = document.getElementById('modal-teacher-name');
let currentLessonsTeacherId = null;
let currentLessonsTeacherName = '';
// ===== Данные =====
let groups = [];
let subjects = [];
let classrooms = [];
const weekdaysTimes = [
"8:00-9:30",
"9:40-11:10",
"11:40-13:10",
"13:20-14:50",
"15:00-16:30",
"16:50-18:20",
"18:30-19:00"
];
const saturdayTimes = [
"8:20-9:50",
"10:00-11:30",
"11:40-13:10",
"13:20-14:50"
];
// =========================================================
// СИНХРОНИЗАЦИЯ ВЫСОТЫ 1-й МОДАЛКИ -> CSS переменная
// =========================================================
const addLessonContent = document.querySelector('#modal-add-lesson .modal-content');
function setAddLessonHeightVar(px) {
const h = Math.max(0, Math.ceil(px || 0));
document.documentElement.style.setProperty('--add-lesson-height', `${h}px`);
}
function syncAddLessonHeight() {
if (!addLessonContent) return;
if (!modalAddLesson?.classList.contains('open')) {
// если первая модалка закрыта — "шапки" нет
setAddLessonHeightVar(0);
return;
}
setAddLessonHeightVar(addLessonContent.getBoundingClientRect().height);
}
// Авто-обновление при любом изменении размеров первой модалки
if (addLessonContent && 'ResizeObserver' in window) {
const ro = new ResizeObserver(() => syncAddLessonHeight());
ro.observe(addLessonContent);
}
window.addEventListener('resize', () => syncAddLessonHeight());
// =========================================================
// Загрузка справочников
// =========================================================
async function loadGroups() {
try {
groups = await api.get('/api/groups');
renderGroupOptions();
} catch (e) {
console.error('Ошибка загрузки групп:', e);
}
}
async function loadSubjects() {
try {
subjects = await api.get('/api/subjects');
renderSubjectOptions();
} catch (e) {
console.error('Ошибка загрузки дисциплин:', e);
}
}
async function loadClassrooms() {
try {
classrooms = await api.get('/api/classrooms');
renderClassroomsOptions();
} catch (e) {
console.error('Ошибка загрузки аудиторий:', e);
}
}
function renderGroupOptions() {
if (!groups || groups.length === 0) {
lessonGroupSelect.innerHTML = '<option value="">Нет доступных групп</option>';
return;
}
lessonGroupSelect.innerHTML =
'<option value="">Выберите группу</option>' +
groups.map(g => {
let optionText = escapeHtml(g.name);
if (g.groupSize) optionText += ` (численность: ${g.groupSize} чел.)`;
return `<option value="${g.id}">${optionText}</option>`;
}).join('');
}
function renderSubjectOptions() {
lessonDisciplineSelect.innerHTML =
'<option value="">Выберите дисциплину</option>' +
subjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
}
function renderClassroomsOptions() {
if (!classrooms || classrooms.length === 0) {
lessonClassroomSelect.innerHTML = '<option value="">Нет доступных аудиторий</option>';
return;
}
const selectedGroupId = lessonGroupSelect.value;
const selectedGroup = groups?.find(g => g.id == selectedGroupId);
const groupSize = selectedGroup?.groupSize || 0;
lessonClassroomSelect.innerHTML =
'<option value="">Выберите аудиторию</option>' +
classrooms.map(c => {
let optionText = escapeHtml(c.name);
if (c.capacity) optionText += ` (вместимость: ${c.capacity} чел.)`;
if (c.isAvailable === false) {
optionText += ` ❌ Занята`;
} else if (selectedGroupId && groupSize > 0 && c.capacity && groupSize > c.capacity) {
optionText += ` ⚠️ Недостаточно места`;
}
return `<option value="${c.id}">${optionText}</option>`;
}).join('');
}
lessonGroupSelect.addEventListener('change', function () {
renderClassroomsOptions();
requestAnimationFrame(() => syncAddLessonHeight());
});
function updateTimeOptions(dayValue) {
let times = [];
if (dayValue === "Суббота") {
times = saturdayTimes;
} else if (dayValue && dayValue !== '') {
times = weekdaysTimes;
} else {
lessonTimeSelect.innerHTML = '<option value="">Сначала выберите день</option>';
lessonTimeSelect.disabled = true;
return;
}
lessonTimeSelect.innerHTML =
'<option value="">Выберите время</option>' +
times.map(t => `<option value="${t}">${t}</option>`).join('');
lessonTimeSelect.disabled = false;
}
// =========================================================
// Пользователи
// =========================================================
async function loadUsers() {
try {
const users = await api.get('/api/users');
renderUsers(users);
} catch (e) {
usersTbody.innerHTML =
'<tr><td colspan="8" class="loading-row">Ошибка загрузки: ' +
escapeHtml(e.message) + '</td></tr>';
}
}
function renderUsers(users) {
if (!users || !users.length) {
usersTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Нет пользователей</td></tr>';
return;
}
usersTbody.innerHTML = users.map(u => `
<tr>
<td>${u.id}</td>
<td>${escapeHtml(u.username)}</td>
<td>${escapeHtml(u.fullName || '-')}</td>
<td>${escapeHtml(u.jobTitle || '-')}</td>
<td>${u.departmentId || '-'}</td>
<td><span class="badge ${ROLE_BADGE[u.role] || ''}">${ROLE_LABELS[u.role] || escapeHtml(u.role)}</span></td>
<td>
<button class="btn-delete" data-id="${u.id}">Удалить</button>
</td>
<td>
<button class="btn-add-lesson" data-id="${u.id}" data-name="${escapeHtml(u.username)}">Добавить занятие</button>
</td>
</tr>
`).join('');
}
function updateBackdrop() {
if(!modalBackdrop) return;
const anyOpen =
modalAddLesson?.classList.contains('open') ||
modalViewLessons?.classList.contains('open');
modalBackdrop.classList.toggle('open', anyOpen);
}
// Клик мимо модалок закроет их, если не надо, то закомментить этот код
modalBackdrop?.addEventListener('click', () => {
if (modalAddLesson?.classList.contains('open')) {
modalAddLesson.classList.remove('open');
resetLessonForm();
syncAddLessonHeight();
}
if (modalViewLessons?.classList.contains('open')) {
closeViewLessonsModal();
}
});
// =========================================================
// 1-я модалка: добавление занятия
// =========================================================
function resetLessonForm() {
addLessonForm.reset();
lessonUserId.value = '';
if (weekUpper) weekUpper.checked = false;
if (weekLower) weekLower.checked = false;
if (lessonOfflineFormat) lessonOfflineFormat.checked = true;
if (lessonOnlineFormat) lessonOnlineFormat.checked = false;
lessonTimeSelect.innerHTML = '<option value="">Сначала выберите день</option>';
lessonTimeSelect.disabled = true;
hideAlert('add-lesson-alert');
}
function openAddLessonModal(userId) {
lessonUserId.value = userId;
lessonDaySelect.value = '';
updateTimeOptions('');
modalAddLesson.classList.add('open');
updateBackdrop();
requestAnimationFrame(() => syncAddLessonHeight());
}
addLessonForm.addEventListener('submit', async (e) => {
e.preventDefault();
hideAlert('add-lesson-alert');
const userId = lessonUserId.value;
const groupId = lessonGroupSelect.value;
const subjectId = lessonDisciplineSelect.value;
const classroomId = lessonClassroomSelect.value;
const lessonType = lessonTypeSelect.value;
const dayOfWeek = lessonDaySelect.value;
const timeSlot = lessonTimeSelect.value;
const lessonFormat = document.querySelector('input[name="lessonFormat"]:checked')?.value;
if (!groupId) { showAlert('add-lesson-alert', 'Выберите группу', 'error'); requestAnimationFrame(() => syncAddLessonHeight()); return; }
if (!subjectId) { showAlert('add-lesson-alert', 'Выберите дисциплину', 'error'); requestAnimationFrame(() => syncAddLessonHeight()); return; }
if (!classroomId) { showAlert('add-lesson-alert', 'Выберите аудиторию', 'error'); requestAnimationFrame(() => syncAddLessonHeight()); return; }
if (!dayOfWeek) { showAlert('add-lesson-alert', 'Выберите день недели', 'error'); requestAnimationFrame(() => syncAddLessonHeight()); return; }
if (!timeSlot) { showAlert('add-lesson-alert', 'Выберите время', 'error'); requestAnimationFrame(() => syncAddLessonHeight()); return; }
const weekUpperChecked = weekUpper?.checked || false;
const weekLowerChecked = weekLower?.checked || false;
let weekType = null;
if (weekUpperChecked && weekLowerChecked) weekType = 'Обе';
else if (weekUpperChecked) weekType = 'Верхняя';
else if (weekLowerChecked) weekType = 'Нижняя';
try {
await api.post('/api/users/lessons/create', {
teacherId: parseInt(userId),
groupId: parseInt(groupId),
subjectId: parseInt(subjectId),
classroomId: parseInt(classroomId),
typeLesson: lessonType,
lessonFormat: lessonFormat,
day: dayOfWeek,
week: weekType,
time: timeSlot
});
if (modalViewLessons?.classList.contains('open') && currentLessonsTeacherId == userId) {
await loadTeacherLessons(currentLessonsTeacherId, currentLessonsTeacherName);
}
showAlert('add-lesson-alert', 'Занятие добавлено', 'success');
lessonGroupSelect.value = '';
lessonDisciplineSelect.value = '';
lessonClassroomSelect.value = '';
lessonTypeSelect.value = '';
lessonDaySelect.value = '';
lessonTimeSelect.value = '';
lessonTimeSelect.disabled = true;
weekUpper.checked = false;
weekLower.checked = false;
document.querySelector('input[name="lessonFormat"][value="Очно"]').checked = true;
requestAnimationFrame(() => syncAddLessonHeight());
setTimeout(() => {
hideAlert('add-lesson-alert');
syncAddLessonHeight();
}, 3000);
} catch (err) {
showAlert('add-lesson-alert', err.message || 'Ошибка добавления занятия', 'error');
requestAnimationFrame(() => syncAddLessonHeight());
}
});
lessonDaySelect.addEventListener('change', function () {
updateTimeOptions(this.value);
requestAnimationFrame(() => syncAddLessonHeight());
});
if (modalAddLessonClose) {
modalAddLessonClose.addEventListener('click', () => {
modalAddLesson.classList.remove('open');
resetLessonForm();
syncAddLessonHeight();
updateBackdrop();
});
}
if (modalAddLesson) {
modalAddLesson.addEventListener('click', (e) => {
if (e.target === modalAddLesson) {
modalAddLesson.classList.remove('open');
resetLessonForm();
syncAddLessonHeight();
updateBackdrop();
}
});
}
// =========================================================
// Создание пользователя
// =========================================================
createForm.addEventListener('submit', async (e) => {
e.preventDefault();
hideAlert('create-alert');
const username = document.getElementById('new-username').value.trim();
const password = document.getElementById('new-password').value;
const role = document.getElementById('new-role').value;
const fullName = document.getElementById('new-fullname').value.trim();
const jobTitle = document.getElementById('new-jobtitle').value.trim();
const departmentId = document.getElementById('new-department').value;
if (!username || !password || !fullName || !jobTitle || !departmentId) {
showAlert('create-alert', 'Заполните все поля', 'error');
return;
}
try {
const data = await api.post('/api/users', {
username,
password,
role,
fullName,
jobTitle,
departmentId: Number(departmentId)
});
showAlert('create-alert', `Пользователь "${escapeHtml(data.username)}" создан`, 'success');
createForm.reset();
loadUsers();
} catch (err) {
showAlert('create-alert', err.message || 'Ошибка соединения', 'error');
}
});
// =========================================================
// Инициализация
// =========================================================
await Promise.all([loadUsers(), loadGroups(), loadSubjects(), loadClassrooms()]);
// =========================================================
// 2-я модалка: просмотр занятий
// =========================================================
async function loadTeacherLessons(teacherId, teacherName) {
try {
lessonsContainer.innerHTML = '<div class="loading-lessons">Загрузка занятий...</div>';
modalTeacherName.textContent = teacherName
? `Занятия преподавателя: ${teacherName}`
: 'Занятия преподавателя';
const lessons = await api.get(`/api/users/lessons/${teacherId}`);
if (!lessons || lessons.length === 0) {
lessonsContainer.innerHTML = '<div class="no-lessons">У преподавателя пока нет занятий</div>';
return;
}
const daysOrder = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'];
const lessonsByDay = {};
lessons.forEach(lesson => {
if (!lessonsByDay[lesson.day]) lessonsByDay[lesson.day] = [];
lessonsByDay[lesson.day].push(lesson);
});
Object.keys(lessonsByDay).forEach(day => {
lessonsByDay[day].sort((a, b) => a.time.localeCompare(b.time));
});
let html = '';
daysOrder.forEach(day => {
if (!lessonsByDay[day]) return;
html += `<div class="lesson-day-divider">${day}</div>`;
lessonsByDay[day].forEach(lesson => {
html += `
<div class="lesson-card">
<div class="lesson-card-header">
<span class="lesson-group">${escapeHtml(lesson.groupName)}</span>
<span class="lesson-time">${escapeHtml(lesson.time)}</span>
</div>
<div class="lesson-card-body">
<div class="lesson-subject">${escapeHtml(lesson.subjectName)}</div>
<div class="lesson-details">
<span class="lesson-detail-item">${escapeHtml(lesson.typeLesson)}</span>
<span class="lesson-detail-item">${escapeHtml(lesson.lessonFormat)}</span>
<span class="lesson-detail-item">${escapeHtml(lesson.week)}</span>
<span class="lesson-detail-item">${escapeHtml(lesson.classroomName)}</span>
</div>
</div>
</div>
`;
});
});
lessonsContainer.innerHTML = html;
} catch (e) {
lessonsContainer.innerHTML = `<div class="no-lessons">Ошибка загрузки: ${escapeHtml(e.message)}</div>`;
console.error('Ошибка загрузки занятий:', e);
}
}
function openViewLessonsModal(teacherId, teacherName) {
currentLessonsTeacherId = teacherId;
currentLessonsTeacherName = teacherName || '';
loadTeacherLessons(teacherId, teacherName);
requestAnimationFrame(() => syncAddLessonHeight());
modalViewLessons.classList.add('open');
updateBackdrop();
// document.body.style.overflow = 'hidden';
}
function closeViewLessonsModal() {
modalViewLessons.classList.remove('open');
updateBackdrop();
// document.body.style.overflow = '';
currentLessonsTeacherId = null;
currentLessonsTeacherName = '';
}
if (modalViewLessonsClose) {
modalViewLessonsClose.addEventListener('click', closeViewLessonsModal);
}
if (modalViewLessons) {
modalViewLessons.addEventListener('click', (e) => {
if (e.target === modalViewLessons) closeViewLessonsModal();
});
}
document.addEventListener('keydown', (e) => {
if (e.key !== 'Escape') return;
if (modalAddLesson?.classList.contains('open')) {
modalAddLesson.classList.remove('open');
resetLessonForm();
syncAddLessonHeight();
updateBackdrop();
return;
}
if (modalViewLessons?.classList.contains('open')) {
closeViewLessonsModal();
return;
}
});
// =========================================================
// ЕДИНЫЙ обработчик кликов по таблице (ВАЖНО: без дубля)
// =========================================================
usersTbody.addEventListener('click', async (e) => {
const deleteBtn = e.target.closest('.btn-delete');
if (deleteBtn) {
if (!confirm('Удалить пользователя?')) return;
try {
await api.delete('/api/users/' + deleteBtn.dataset.id);
loadUsers();
} catch (err) {
alert(err.message || 'Ошибка удаления');
}
return;
}
const addLessonBtn = e.target.closest('.btn-add-lesson');
if (addLessonBtn) {
e.preventDefault();
const teacherId = addLessonBtn.dataset.id;
const teacherName = addLessonBtn.dataset.name;
openAddLessonModal(teacherId);
openViewLessonsModal(teacherId, teacherName);
return;
}
const viewLessonsBtn = e.target.closest('.btn-view-lessons');
if (viewLessonsBtn) {
e.preventDefault();
const teacherId = viewLessonsBtn.dataset.id;
const teacherName = viewLessonsBtn.dataset.name;
openViewLessonsModal(teacherId, teacherName);
return;
}
});
}