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 = `Ошибка загрузки: ${escapeHtml(e.message)}`; } } function renderSchedule(lessons) { if (!lessons || !lessons.length) { tbody.innerHTML = 'Нет занятий'; return; } const filtered = applyFilters(lessons); if (!filtered.length) { tbody.innerHTML = 'Нет занятий по выбранным фильтрам'; 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 ` ${escapeHtml(lesson.id)} ${escapeHtml(teacherName)} ${escapeHtml(groupName)} ${escapeHtml(classroomName)} ${escapeHtml(educationForm)} ${escapeHtml(subjectName)} ${escapeHtml(lessonFormat)} ${escapeHtml(typeLesson)} ${escapeHtml(day)} ${escapeHtml(week)} ${escapeHtml(time)} `; }).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 = '' + groups.map(g => { let text = escapeHtml(g.name); if (g.groupSize) text += ` (числ: ${g.groupSize} чел.)`; return ``; }).join(''); } catch (e) { console.error('Ошибка загрузки групп:', e); } } async function loadSubjects() { try { subjects = await api.get('/api/subjects'); schDisciplineSelect.innerHTML = '' + subjects.map(s => ``).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 = '' + teachers.map(t => ``).join(''); } catch (e) { console.error('Ошибка загрузки преподавателей:', e); } } function renderClassroomOptions() { if (!classrooms || classrooms.length === 0) { schClassroomSelect.innerHTML = ''; return; } const selectedGroupId = schGroupSelect.value; const selectedGroup = groups?.find(g => g.id == selectedGroupId); const groupSize = selectedGroup?.groupSize || 0; schClassroomSelect.innerHTML = '' + 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 ``; }).join(''); } schGroupSelect.addEventListener('change', () => renderClassroomOptions()); function updateTimeOptions(dayValue) { let times = []; if (dayValue === "Суббота") { times = saturdayTimes; } else if (dayValue && dayValue !== '') { times = weekdaysTimes; } else { schTimeSelect.innerHTML = ''; schTimeSelect.disabled = true; return; } schTimeSelect.innerHTML = '' + times.map(t => ``).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 = '
Загрузка занятий...
'; try { const lessons = await api.get(`/api/users/lessons/${teacherId}`); if (!lessons || !Array.isArray(lessons) || lessons.length === 0) { schLessonsContainer.innerHTML = '
У преподавателя пока нет занятий
'; 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 += `
${day}
`; lessonsByDay[day].forEach(lesson => { html += `
${escapeHtml(lesson.groupName)} ${escapeHtml(lesson.time)}
${escapeHtml(lesson.subjectName)}
${escapeHtml(lesson.typeLesson)} ${escapeHtml(lesson.lessonFormat)} ${escapeHtml(lesson.week)} ${escapeHtml(lesson.classroomName)}
`; }); }); schLessonsContainer.innerHTML = html; } catch (e) { schLessonsContainer.innerHTML = `
Ошибка загрузки: ${escapeHtml(e.message)}
`; } } // ===== При смене преподавателя — подгрузить его занятия ===== schTeacherSelect.addEventListener('change', function () { const teacherId = this.value; if (teacherId) { loadTeacherLessons(teacherId); } else { modalLessons.style.display = 'none'; schLessonsContainer.innerHTML = '
Выберите преподавателя для просмотра занятий
'; } }); // ===== Открытие / закрытие оверлея ===== 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 = ''; schTimeSelect.disabled = true; if (schWeekUpper) schWeekUpper.checked = false; if (schWeekLower) schWeekLower.checked = false; if (schFormatOffline) schFormatOffline.checked = true; modalLessons.style.display = 'none'; schLessonsContainer.innerHTML = '
Выберите преподавателя для просмотра занятий
'; 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 = ''; 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() ]); }