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

358 lines
13 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 } 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 = `<tr><td colspan="8" class="loading-row">Ошибка загрузки: ${escapeHtml(e.message)}</td></tr>`;
}
}
function renderSchedule(lessons) {
if (!lessons || !lessons.length) {
tbody.innerHTML = '<tr><td colspan="8" class="loading-row">Нет занятий</td></tr>';
return;
}
// Сначала фильтруем, потом сортируем
const filtered = applyFilters(lessons);
if (!filtered.length) {
tbody.innerHTML = '<tr><td colspan="8" 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 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 `<tr>
<td>${escapeHtml(lesson.id)}</td>
<td>${escapeHtml(teacherName)}</td>
<td>${escapeHtml(groupName)}</td>
<td>${escapeHtml(educationForm)}</td>
<td>${escapeHtml(subjectName)}</td>
<td>${escapeHtml(day)}</td>
<td>${escapeHtml(week)}</td>
<td>${escapeHtml(time)}</td>
</tr>`;
}).join('');
}
await loadSchedule();
}