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

684 lines
27 KiB
JavaScript
Executable File
Raw Permalink 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';
export async function initSchedule() {
const tbody = document.getElementById('schedule-tbody');
const table = document.getElementById('schedule-table');
let lessonsData = [];
let sortKey = null;
let sortDir = 'asc';
// Активные фильтры: { teacher: Set, group: Set, subject: Set, day: Set }
const activeFilters = {};
// Маппинг дней недели для корректной сортировки
const dayOrder = {
'понедельник': 1, 'вторник': 2, 'среда': 3,
'четверг': 4, 'пятница': 5, 'суббота': 6, 'воскресенье': 7
};
// ===================== Фильтрация =====================
function getDisplayValue(lesson, key) {
switch (key) {
case 'teacher':
return lesson.teacher?.username || lesson.teacherName || '—';
case 'group':
return lesson.group?.name || lesson.groupName || '—';
case 'subject':
return lesson.subject?.name || lesson.subjectName || '—';
case 'day':
return lesson.day || '—';
case 'educationForm':
return lesson.educationForm?.name || lesson.educationFormName || '—';
default:
return '';
}
}
function getUniqueValues(key) {
const vals = new Set();
lessonsData.forEach(lesson => {
vals.add(getDisplayValue(lesson, key));
});
if (key === 'day') {
return [...vals].sort((a, b) => (dayOrder[a.toLowerCase()] ?? 99) - (dayOrder[b.toLowerCase()] ?? 99));
}
return [...vals].sort((a, b) => a.localeCompare(b, 'ru'));
}
function applyFilters(lessons) {
return lessons.filter(lesson => {
for (const key of Object.keys(activeFilters)) {
const filterSet = activeFilters[key];
if (filterSet && filterSet.size > 0) {
const val = getDisplayValue(lesson, key);
if (!filterSet.has(val)) return false;
}
}
return true;
});
}
// ===================== Попап фильтра =====================
let currentPopup = null;
function closePopup() {
if (currentPopup) {
currentPopup.remove();
currentPopup = null;
}
document.removeEventListener('click', onDocumentClick, true);
}
function onDocumentClick(e) {
if (currentPopup && !currentPopup.contains(e.target)) {
if (!e.target.closest('.filter-icon')) {
closePopup();
}
}
}
function openFilterPopup(th, filterKey) {
if (currentPopup && currentPopup.dataset.filterKey === filterKey) {
closePopup();
return;
}
closePopup();
const uniqueValues = getUniqueValues(filterKey);
const currentFilter = activeFilters[filterKey];
const popup = document.createElement('div');
popup.className = 'filter-popup';
popup.dataset.filterKey = filterKey;
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.className = 'filter-search';
searchInput.placeholder = 'Поиск...';
popup.appendChild(searchInput);
const btnRow = document.createElement('div');
btnRow.className = 'filter-btn-row';
const btnAll = document.createElement('button');
btnAll.className = 'filter-btn-action';
btnAll.textContent = 'Все';
btnAll.addEventListener('click', (e) => {
e.stopPropagation();
checkboxes.forEach(cb => { cb.checked = true; });
});
const btnNone = document.createElement('button');
btnNone.className = 'filter-btn-action';
btnNone.textContent = 'Сбросить';
btnNone.addEventListener('click', (e) => {
e.stopPropagation();
checkboxes.forEach(cb => { cb.checked = false; });
});
btnRow.appendChild(btnAll);
btnRow.appendChild(btnNone);
popup.appendChild(btnRow);
const listWrap = document.createElement('div');
listWrap.className = 'filter-list';
const checkboxes = [];
uniqueValues.forEach(val => {
const label = document.createElement('label');
label.className = 'filter-item';
const cb = document.createElement('input');
cb.type = 'checkbox';
cb.value = val;
cb.checked = currentFilter ? currentFilter.has(val) : true;
const span = document.createElement('span');
span.textContent = val;
label.appendChild(cb);
label.appendChild(span);
listWrap.appendChild(label);
checkboxes.push(cb);
});
popup.appendChild(listWrap);
const btnApply = document.createElement('button');
btnApply.className = 'filter-btn-apply';
btnApply.textContent = 'Применить';
btnApply.addEventListener('click', (e) => {
e.stopPropagation();
const selected = new Set();
checkboxes.forEach(cb => {
if (cb.checked) selected.add(cb.value);
});
if (selected.size === uniqueValues.length) {
delete activeFilters[filterKey];
th.classList.remove('filter-active');
} else {
activeFilters[filterKey] = selected;
th.classList.add('filter-active');
}
closePopup();
renderSchedule(lessonsData);
});
popup.appendChild(btnApply);
searchInput.addEventListener('input', () => {
const query = searchInput.value.toLowerCase();
listWrap.querySelectorAll('.filter-item').forEach(item => {
const text = item.querySelector('span').textContent.toLowerCase();
item.style.display = text.includes(query) ? '' : 'none';
});
});
popup.addEventListener('click', (e) => e.stopPropagation());
searchInput.addEventListener('click', (e) => e.stopPropagation());
th.style.position = 'relative';
th.appendChild(popup);
currentPopup = popup;
setTimeout(() => searchInput.focus(), 50);
setTimeout(() => {
document.addEventListener('click', onDocumentClick, true);
}, 10);
}
table.querySelectorAll('thead th.filterable').forEach(th => {
th.addEventListener('click', (e) => {
if (e.target.closest('.filter-popup')) return;
const filterKey = th.dataset.filterKey;
openFilterPopup(th, filterKey);
});
});
// ===================== Сортировка =====================
function getSortValue(lesson, key) {
switch (key) {
case 'id':
return lesson.id ?? 0;
case 'teacher':
return (lesson.teacher?.username || lesson.teacherName || '').toLowerCase();
case 'group':
return (lesson.group?.name || lesson.groupName || '').toLowerCase();
case 'classroomName':
return (lesson.classroomName?.name || lesson.classroomName || '').toLowerCase();
case 'educationForm':
return (lesson.educationForm?.name || lesson.educationFormName || '').toLowerCase();
case 'subject':
return (lesson.subject?.name || lesson.subjectName || '').toLowerCase();
case 'lessonFormat':
return (lesson.lessonFormat?.name || lesson.lessonFormat || '').toLowerCase();
case 'typeLesson':
return (lesson.typeLesson?.name || lesson.typeLesson || '').toLowerCase();
case 'day': {
const d = (lesson.day || '').toLowerCase();
return dayOrder[d] ?? 99;
}
case 'week':
return (lesson.week || '').toLowerCase();
case 'time': {
const d = (lesson.day || '').toLowerCase();
const dayNum = dayOrder[d] ?? 99;
const t = lesson.time || '99:99';
return String(dayNum).padStart(2, '0') + '_' + t;
}
default:
return '';
}
}
function sortLessons(lessons) {
if (!sortKey) return lessons;
return [...lessons].sort((a, b) => {
let va = getSortValue(a, sortKey);
let vb = getSortValue(b, sortKey);
if (typeof va === 'number' && typeof vb === 'number') {
return sortDir === 'asc' ? va - vb : vb - va;
}
va = String(va);
vb = String(vb);
const cmp = va.localeCompare(vb, 'ru');
return sortDir === 'asc' ? cmp : -cmp;
});
}
function updateSortHeaders() {
table.querySelectorAll('thead th.sortable').forEach(th => {
th.classList.remove('sort-asc', 'sort-desc', 'sort-active');
if (th.dataset.sortKey === sortKey) {
th.classList.add('sort-active', sortDir === 'asc' ? 'sort-asc' : 'sort-desc');
}
});
}
table.querySelectorAll('thead th.sortable').forEach(th => {
th.addEventListener('click', (e) => {
if (e.target.closest('.filter-icon') || e.target.closest('.filter-popup')) return;
const key = th.dataset.sortKey;
if (sortKey === key) {
if (sortDir === 'asc') {
sortDir = 'desc';
} else {
sortKey = null;
sortDir = 'asc';
}
} else {
sortKey = key;
sortDir = 'asc';
}
updateSortHeaders();
renderSchedule(lessonsData);
});
});
// ===================== Загрузка и рендер таблицы =====================
async function loadSchedule() {
try {
const lessons = await api.get('/api/users/lessons');
lessonsData = lessons;
renderSchedule(lessons);
} catch (e) {
tbody.innerHTML = `<tr><td colspan="11" class="loading-row">Ошибка загрузки: ${escapeHtml(e.message)}</td></tr>`;
}
}
function renderSchedule(lessons) {
if (!lessons || !lessons.length) {
tbody.innerHTML = '<tr><td colspan="11" class="loading-row">Нет занятий</td></tr>';
return;
}
const filtered = applyFilters(lessons);
if (!filtered.length) {
tbody.innerHTML = '<tr><td colspan="11" class="loading-row">Нет занятий по выбранным фильтрам</td></tr>';
return;
}
const sorted = sortLessons(filtered);
tbody.innerHTML = sorted.map(lesson => {
const teacherName = lesson.teacher?.username || lesson.teacherName || '—';
const groupName = lesson.group?.name || lesson.groupName || '—';
const classroomName = lesson.classroom?.name || lesson.classroomName || '—';
const educationForm = lesson.educationForm?.name || lesson.educationFormName || '-';
const subjectName = lesson.subject?.name || lesson.subjectName || '—';
const lessonFormat = lesson.lessonFormat?.name || lesson.lessonFormat || '—';
const typeLesson = lesson.typeLesson?.name || lesson.typeLesson || '—';
const day = lesson.day || '—';
const week = lesson.week || '—';
const time = lesson.time || '—';
return `<tr>
<td>${escapeHtml(lesson.id)}</td>
<td>${escapeHtml(teacherName)}</td>
<td>${escapeHtml(groupName)}</td>
<td>${escapeHtml(classroomName)}</td>
<td>${escapeHtml(educationForm)}</td>
<td>${escapeHtml(subjectName)}</td>
<td>${escapeHtml(lessonFormat)}</td>
<td>${escapeHtml(typeLesson)}</td>
<td>${escapeHtml(day)}</td>
<td>${escapeHtml(week)}</td>
<td>${escapeHtml(time)}</td>
</tr>`;
}).join('');
}
// ===================== Модалки добавления занятия =====================
const overlay = document.getElementById('sch-overlay');
const modalForm = document.getElementById('sch-modal-form');
const modalLessons = document.getElementById('sch-modal-lessons');
const btnAddLesson = document.getElementById('sch-btn-add-lesson');
const btnClose = document.getElementById('sch-modal-close');
const addForm = document.getElementById('sch-add-lesson-form');
const schTeacherSelect = document.getElementById('sch-teacher');
const schGroupSelect = document.getElementById('sch-group');
const schDisciplineSelect = document.getElementById('sch-discipline');
const schClassroomSelect = document.getElementById('sch-classroom');
const schDaySelect = document.getElementById('sch-day');
const schTimeSelect = document.getElementById('sch-time');
const schTypeSelect = document.getElementById('sch-type');
const schWeekUpper = document.getElementById('sch-week-upper');
const schWeekLower = document.getElementById('sch-week-lower');
const schFormatOffline = document.getElementById('sch-format-offline');
const schTeacherName = document.getElementById('sch-teacher-name');
const schLessonsContainer = document.getElementById('sch-lessons-container');
let groups = [];
let subjects = [];
let classrooms = [];
let teachers = [];
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"
];
// ===== Загрузка справочников =====
async function loadGroups() {
try {
groups = await api.get('/api/groups');
schGroupSelect.innerHTML = '<option value="">Выберите группу</option>' +
groups.map(g => {
let text = escapeHtml(g.name);
if (g.groupSize) text += ` (числ: ${g.groupSize} чел.)`;
return `<option value="${g.id}">${text}</option>`;
}).join('');
} catch (e) { console.error('Ошибка загрузки групп:', e); }
}
async function loadSubjects() {
try {
subjects = await api.get('/api/subjects');
schDisciplineSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
subjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
} catch (e) { console.error('Ошибка загрузки дисциплин:', e); }
}
async function loadClassrooms() {
try {
classrooms = await api.get('/api/classrooms');
renderClassroomOptions();
} catch (e) { console.error('Ошибка загрузки аудиторий:', e); }
}
async function loadTeachers() {
try {
teachers = await api.get('/api/users/teachers');
schTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' +
teachers.map(t => `<option value="${t.id}">${escapeHtml(t.fullName || t.username)}</option>`).join('');
} catch (e) { console.error('Ошибка загрузки преподавателей:', e); }
}
function renderClassroomOptions() {
if (!classrooms || classrooms.length === 0) {
schClassroomSelect.innerHTML = '<option value="">Нет доступных аудиторий</option>';
return;
}
const selectedGroupId = schGroupSelect.value;
const selectedGroup = groups?.find(g => g.id == selectedGroupId);
const groupSize = selectedGroup?.groupSize || 0;
schClassroomSelect.innerHTML = '<option value="">Выберите аудиторию</option>' +
classrooms.map(c => {
let text = escapeHtml(c.name);
if (c.capacity) text += ` (вместимость: ${c.capacity} чел.)`;
if (c.isAvailable === false) {
text += ` ❌ Занята`;
} else if (selectedGroupId && groupSize > 0 && c.capacity && groupSize > c.capacity) {
text += ` ⚠️ Недостаточно места`;
}
return `<option value="${c.id}">${text}</option>`;
}).join('');
}
schGroupSelect.addEventListener('change', () => renderClassroomOptions());
function updateTimeOptions(dayValue) {
let times = [];
if (dayValue === "Суббота") {
times = saturdayTimes;
} else if (dayValue && dayValue !== '') {
times = weekdaysTimes;
} else {
schTimeSelect.innerHTML = '<option value="">Сначала выберите день</option>';
schTimeSelect.disabled = true;
return;
}
schTimeSelect.innerHTML = '<option value="">Выберите время</option>' +
times.map(t => `<option value="${t}">${t}</option>`).join('');
schTimeSelect.disabled = false;
}
schDaySelect.addEventListener('change', function () {
updateTimeOptions(this.value);
});
// ===== Автозаполнение преподавателя из фильтра =====
function getFilteredTeacherId() {
const teacherFilter = activeFilters['teacher'];
if (teacherFilter && teacherFilter.size === 1) {
const teacherName = [...teacherFilter][0];
// Сопоставляем по username, fullName и их комбинациям
const match = teachers.find(t =>
t.username === teacherName ||
t.fullName === teacherName ||
(t.fullName || t.username) === teacherName
);
return match ? String(match.id) : '';
}
return '';
}
// ===== Загрузка занятий преподавателя =====
async function loadTeacherLessons(teacherId) {
const teacher = teachers.find(t => t.id == teacherId);
const name = teacher ? (teacher.fullName || teacher.username) : '';
schTeacherName.textContent = name
? `Занятия преподавателя: ${name}`
: 'Занятия преподавателя';
modalLessons.style.display = '';
schLessonsContainer.innerHTML = '<div class="loading-lessons">Загрузка занятий...</div>';
try {
const lessons = await api.get(`/api/users/lessons/${teacherId}`);
if (!lessons || !Array.isArray(lessons) || lessons.length === 0) {
schLessonsContainer.innerHTML = '<div class="no-lessons">У преподавателя пока нет занятий</div>';
return;
}
const daysOrder = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'];
const lessonsByDay = {};
lessons.forEach(l => {
if (!lessonsByDay[l.day]) lessonsByDay[l.day] = [];
lessonsByDay[l.day].push(l);
});
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>`;
});
});
schLessonsContainer.innerHTML = html;
} catch (e) {
schLessonsContainer.innerHTML = `<div class="no-lessons">Ошибка загрузки: ${escapeHtml(e.message)}</div>`;
}
}
// ===== При смене преподавателя — подгрузить его занятия =====
schTeacherSelect.addEventListener('change', function () {
const teacherId = this.value;
if (teacherId) {
loadTeacherLessons(teacherId);
} else {
modalLessons.style.display = 'none';
schLessonsContainer.innerHTML = '<div class="no-lessons">Выберите преподавателя для просмотра занятий</div>';
}
});
// ===== Открытие / закрытие оверлея =====
function openOverlay() {
// Автозаполнение преподавателя из фильтра таблицы
const autoTeacherId = getFilteredTeacherId();
if (autoTeacherId) {
schTeacherSelect.value = autoTeacherId;
loadTeacherLessons(autoTeacherId);
}
overlay.classList.add('open');
}
function closeOverlay() {
overlay.classList.remove('open');
resetForm();
}
function resetForm() {
addForm.reset();
schTeacherSelect.value = '';
schGroupSelect.value = '';
schDisciplineSelect.value = '';
schClassroomSelect.value = '';
schDaySelect.value = '';
schTypeSelect.value = '';
schTimeSelect.innerHTML = '<option value="">Сначала выберите день</option>';
schTimeSelect.disabled = true;
if (schWeekUpper) schWeekUpper.checked = false;
if (schWeekLower) schWeekLower.checked = false;
if (schFormatOffline) schFormatOffline.checked = true;
modalLessons.style.display = 'none';
schLessonsContainer.innerHTML = '<div class="no-lessons">Выберите преподавателя для просмотра занятий</div>';
hideAlert('sch-add-alert');
}
btnAddLesson.addEventListener('click', openOverlay);
btnClose.addEventListener('click', closeOverlay);
// Закрытие по клику на оверлей (мимо модалок)
overlay.addEventListener('click', (e) => {
if (e.target === overlay || e.target.classList.contains('cs-overlay-scroll')) {
closeOverlay();
}
});
// Закрытие по Escape
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && overlay.classList.contains('open')) {
closeOverlay();
}
});
// ===== Отправка формы =====
addForm.addEventListener('submit', async (e) => {
e.preventDefault();
hideAlert('sch-add-alert');
const teacherId = schTeacherSelect.value;
const groupId = schGroupSelect.value;
const subjectId = schDisciplineSelect.value;
const classroomId = schClassroomSelect.value;
const lessonType = schTypeSelect.value;
const dayOfWeek = schDaySelect.value;
const timeSlot = schTimeSelect.value;
const lessonFormat = document.querySelector('input[name="schLessonFormat"]:checked')?.value;
if (!teacherId) { showAlert('sch-add-alert', 'Выберите преподавателя', 'error'); return; }
if (!groupId) { showAlert('sch-add-alert', 'Выберите группу', 'error'); return; }
if (!subjectId) { showAlert('sch-add-alert', 'Выберите дисциплину', 'error'); return; }
if (!classroomId) { showAlert('sch-add-alert', 'Выберите аудиторию', 'error'); return; }
if (!dayOfWeek) { showAlert('sch-add-alert', 'Выберите день недели', 'error'); return; }
if (!timeSlot) { showAlert('sch-add-alert', 'Выберите время', 'error'); return; }
const weekUpperChecked = schWeekUpper?.checked || false;
const weekLowerChecked = schWeekLower?.checked || false;
if (!weekUpperChecked && !weekLowerChecked) {
showAlert('sch-add-alert', 'Не выбран тип недели', 'error');
return;
}
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(teacherId),
groupId: parseInt(groupId),
subjectId: parseInt(subjectId),
classroomId: parseInt(classroomId),
typeLesson: lessonType,
lessonFormat: lessonFormat,
day: dayOfWeek,
week: weekType,
time: timeSlot
});
showAlert('sch-add-alert', 'Занятие добавлено ✓', 'success');
// Очистить все поля кроме преподавателя (для массового добавления)
schGroupSelect.selectedIndex = 0;
schDisciplineSelect.selectedIndex = 0;
schClassroomSelect.selectedIndex = 0;
schTypeSelect.selectedIndex = 0;
schDaySelect.selectedIndex = 0;
schTimeSelect.innerHTML = '<option value="">Сначала выберите день</option>';
schTimeSelect.disabled = true;
schWeekUpper.checked = false;
schWeekLower.checked = false;
document.querySelector('input[name="schLessonFormat"][value="Очно"]').checked = true;
// Обновить занятия преподавателя в модалке 2
if (teacherId) {
await loadTeacherLessons(teacherId);
}
// Обновить основную таблицу
await loadSchedule();
setTimeout(() => {
hideAlert('sch-add-alert');
}, 4000);
} catch (err) {
showAlert('sch-add-alert', err.message || 'Ошибка добавления занятия', 'error');
}
});
// ===================== Инициализация =====================
await Promise.all([
loadSchedule(),
loadGroups(),
loadSubjects(),
loadClassrooms(),
loadTeachers()
]);
}