import { api } from '../api.js'; import { escapeHtml } 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'; }); }); // Предотвращаем всплытие кликов внутри попапа (чтобы не срабатывала сортировка th) popup.addEventListener('click', (e) => e.stopPropagation()); searchInput.addEventListener('click', (e) => e.stopPropagation()); // Позиционируем попап под th 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 'educationForm': return (lesson.educationForm?.name || lesson.educationFormName || '').toLowerCase(); case 'subject': return (lesson.subject?.name || lesson.subjectName || '').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 educationForm = lesson.educationForm?.name || lesson.educationFormName || '-'; const subjectName = lesson.subject?.name || lesson.subjectName || '—'; const day = lesson.day || '—'; const week = lesson.week || '—'; const time = lesson.time || '—'; return ` ${escapeHtml(lesson.id)} ${escapeHtml(teacherName)} ${escapeHtml(groupName)} ${escapeHtml(educationForm)} ${escapeHtml(subjectName)} ${escapeHtml(day)} ${escapeHtml(week)} ${escapeHtml(time)} `; }).join(''); } await loadSchedule(); }