Files
OpenLesson/public/admin.js

322 lines
15 KiB
JavaScript
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.
// public/admin.js панель администратора
let currentUser = null;
let currentPreviewLessons = [];
let allLessonsForFilters = []; // храним все уроки для построения зависимых фильтров
document.addEventListener('DOMContentLoaded', async () => {
await checkAuth();
await loadFilterOptions();
loadLessons();
setupEventListeners();
});
async function checkAuth() {
try {
const res = await fetch('/api/me');
const data = await res.json();
if (!data.authenticated || data.user.role !== 'admin') {
window.location.href = '/login.html';
return;
}
currentUser = data.user;
document.getElementById('userInfo').innerHTML = `👋 ${currentUser.full_name} (${currentUser.role})`;
} catch (err) {
window.location.href = '/login.html';
}
}
// Загрузка опций для выпадающих списков фильтров (начальные)
async function loadFilterOptions() {
try {
const [classes, teachers, topics] = await Promise.all([
fetch('/api/filter-options/class-names').then(r => r.json()),
fetch('/api/filter-options/teachers').then(r => r.json()),
fetch('/api/filter-options/topics').then(r => r.json())
]);
window.allClassNames = classes;
window.allTeachers = teachers;
window.allTopics = topics;
populateSelect('filterClass', window.allClassNames, 'Все классы');
populateSelect('filterTeacher', window.allTeachers, 'Все учителя');
populateSelect('filterTopic', window.allTopics, 'Все темы');
} catch (err) {
console.error('Ошибка загрузки опций фильтров', err);
}
}
function populateSelect(selectId, options, defaultLabel) {
const select = document.getElementById(selectId);
if (!select) return;
const currentValue = select.value;
select.innerHTML = `<option value="">${defaultLabel}</option>`;
options.forEach(opt => {
const option = document.createElement('option');
option.value = opt;
option.textContent = opt;
select.appendChild(option);
});
if (currentValue && options.includes(currentValue)) {
select.value = currentValue;
} else {
select.value = '';
}
}
// Обновление зависимых фильтров на основе текущих значений и allLessonsForFilters
function updateDependentFilters() {
const selectedClass = document.getElementById('filterClass').value;
const selectedTeacher = document.getElementById('filterTeacher').value;
const selectedTopic = document.getElementById('filterTopic').value;
let filteredLessons = allLessonsForFilters;
if (selectedClass) {
filteredLessons = filteredLessons.filter(l => l.class_name === selectedClass);
}
if (selectedTeacher) {
filteredLessons = filteredLessons.filter(l => l.teacher === selectedTeacher);
}
if (selectedTopic) {
filteredLessons = filteredLessons.filter(l => l.topic === selectedTopic);
}
const availableClasses = [...new Set(filteredLessons.map(l => l.class_name))].sort();
const availableTeachers = [...new Set(filteredLessons.map(l => l.teacher))].sort();
const availableTopics = [...new Set(filteredLessons.map(l => l.topic).filter(t => t))].sort();
const oldClass = document.getElementById('filterClass').value;
const oldTeacher = document.getElementById('filterTeacher').value;
const oldTopic = document.getElementById('filterTopic').value;
populateSelect('filterClass', availableClasses, 'Все классы');
populateSelect('filterTeacher', availableTeachers, 'Все учителя');
populateSelect('filterTopic', availableTopics, 'Все темы');
if (oldClass && availableClasses.includes(oldClass)) document.getElementById('filterClass').value = oldClass;
else document.getElementById('filterClass').value = '';
if (oldTeacher && availableTeachers.includes(oldTeacher)) document.getElementById('filterTeacher').value = oldTeacher;
else document.getElementById('filterTeacher').value = '';
if (oldTopic && availableTopics.includes(oldTopic)) document.getElementById('filterTopic').value = oldTopic;
else document.getElementById('filterTopic').value = '';
}
async function loadLessons(filters = {}) {
const params = new URLSearchParams(filters);
const res = await fetch(`/api/admin/lessons?${params}`);
const lessons = await res.json();
// Сохраняем полный список для фильтрации (без учёта фильтров)
allLessonsForFilters = lessons;
updateDependentFilters(); // обновить списки в select на основе всех уроков
const container = document.getElementById('lessonsList');
if (!container) return;
const grouped = {};
lessons.forEach(lesson => {
if (!grouped[lesson.class_name]) grouped[lesson.class_name] = [];
grouped[lesson.class_name].push(lesson);
});
container.innerHTML = '';
for (const [className, classLessons] of Object.entries(grouped)) {
const section = document.createElement('div');
section.className = 'class-group';
section.innerHTML = `<h2>${className}</h2>`;
classLessons.forEach(lesson => {
const div = document.createElement('div');
div.className = 'lesson-item';
div.innerHTML = `
<div>
<strong>${lesson.subject}</strong> — ${lesson.teacher}<br>
<em>Тема: ${lesson.topic || '—'}</em><br>
${lesson.date} ${lesson.time} | Места: ${lesson.current_slots}/${lesson.max_slots}
</div>
<div class="lesson-actions">
<button class="viewRegBtn" data-id="${lesson.id}">Записи</button>
<button class="editBtn" data-id="${lesson.id}">✏️</button>
<button class="deleteBtn danger" data-id="${lesson.id}">🗑️</button>
</div>
`;
section.appendChild(div);
});
container.appendChild(section);
}
document.querySelectorAll('.viewRegBtn').forEach(btn => btn.addEventListener('click', () => showRegistrations(btn.dataset.id)));
document.querySelectorAll('.editBtn').forEach(btn => btn.addEventListener('click', () => openLessonModal(btn.dataset.id)));
document.querySelectorAll('.deleteBtn').forEach(btn => btn.addEventListener('click', () => deleteLesson(btn.dataset.id)));
}
async function showRegistrations(lessonId) {
const res = await fetch(`/api/admin/registrations?lesson_id=${lessonId}`);
const registrations = await res.json();
const modal = document.getElementById('registrationsModal');
const listDiv = document.getElementById('registrationsList');
listDiv.innerHTML = registrations.length ? '<ul>' + registrations.map(r => `<li><strong>${r.parent_name}</strong> — ${r.parent_phone} (${new Date(r.created_at).toLocaleString()})</li>`).join('') + '</ul>' : '<p>Нет записей</p>';
modal.style.display = 'flex';
modal.querySelector('.close').onclick = () => modal.style.display = 'none';
}
async function deleteLesson(id) {
if (!confirm('Удалить урок? Все записи будут удалены.')) return;
await fetch(`/api/admin/lessons/${id}`, { method: 'DELETE' });
loadLessons(getCurrentFilters());
}
function openLessonModal(id = null) {
const modal = document.getElementById('lessonModal');
const form = document.getElementById('lessonForm');
form.reset();
document.getElementById('lessonId').value = '';
document.getElementById('modalTitle').innerText = id ? 'Редактирование урока' : 'Новый урок';
if (id) {
fetch(`/api/admin/lessons`).then(res => res.json()).then(lessons => {
const lesson = lessons.find(l => l.id == id);
if (lesson) {
document.getElementById('lessonId').value = lesson.id;
document.getElementById('className').value = lesson.class_name;
document.getElementById('parallel').value = lesson.parallel;
document.getElementById('subject').value = lesson.subject;
document.getElementById('teacher').value = lesson.teacher;
document.getElementById('topic').value = lesson.topic || '';
document.getElementById('maxSlots').value = lesson.max_slots;
document.getElementById('date').value = lesson.date;
document.getElementById('time').value = lesson.time;
}
});
}
modal.style.display = 'flex';
modal.querySelector('.close').onclick = () => modal.style.display = 'none';
}
document.getElementById('lessonForm')?.addEventListener('submit', async (e) => {
e.preventDefault();
const id = document.getElementById('lessonId').value;
const payload = {
id: id || undefined,
class_name: document.getElementById('className').value,
parallel: parseInt(document.getElementById('parallel').value),
subject: document.getElementById('subject').value,
teacher: document.getElementById('teacher').value,
topic: document.getElementById('topic').value,
max_slots: parseInt(document.getElementById('maxSlots').value),
date: document.getElementById('date').value,
time: document.getElementById('time').value
};
await fetch('/api/admin/lessons', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
document.getElementById('lessonModal').style.display = 'none';
loadLessons(getCurrentFilters());
});
function getCurrentFilters() {
return {
class_name: document.getElementById('filterClass').value,
parallel: document.getElementById('filterParallel').value,
teacher: document.getElementById('filterTeacher').value,
topic: document.getElementById('filterTopic').value
};
}
function setupEventListeners() {
const classSelect = document.getElementById('filterClass');
const teacherSelect = document.getElementById('filterTeacher');
const topicSelect = document.getElementById('filterTopic');
function handleFilterChange() {
// Обновить зависимые списки, затем применить фильтрацию списка уроков
updateDependentFilters();
loadLessons(getCurrentFilters());
}
classSelect?.addEventListener('change', handleFilterChange);
teacherSelect?.addEventListener('change', handleFilterChange);
topicSelect?.addEventListener('change', handleFilterChange);
document.getElementById('applyFilters')?.addEventListener('click', () => {
loadLessons(getCurrentFilters());
});
document.getElementById('resetFilters')?.addEventListener('click', () => {
document.getElementById('filterClass').value = '';
document.getElementById('filterParallel').value = '';
document.getElementById('filterTeacher').value = '';
document.getElementById('filterTopic').value = '';
updateDependentFilters();
loadLessons({});
});
document.getElementById('addLessonBtn')?.addEventListener('click', () => openLessonModal());
document.getElementById('logoutBtn')?.addEventListener('click', async () => {
await fetch('/api/logout', { method: 'POST' });
window.location.href = '/';
});
// Импорт JSON (без изменений)
document.getElementById('previewImportBtn')?.addEventListener('click', async () => {
const file = document.getElementById('jsonFileInput').files[0];
if (!file) return alert('Выберите JSON-файл');
const importMaxSlots = document.getElementById('importMaxSlots').value;
if (!importMaxSlots) return alert('Укажите максимальное количество мест');
const text = await file.text();
try {
const response = await fetch('/api/admin/import/preview', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonData: JSON.parse(text) })
});
const data = await response.json();
if (data.success) {
currentPreviewLessons = data.preview;
const tbody = document.querySelector('#previewTable tbody');
tbody.innerHTML = '';
data.preview.slice(0,20).forEach(lesson => {
const row = tbody.insertRow();
row.insertCell(0).innerText = lesson.className;
row.insertCell(1).innerText = lesson.subject;
row.insertCell(2).innerText = lesson.teacher;
row.insertCell(3).innerText = lesson.topic || '';
});
if (data.preview.length > 20) {
const row = tbody.insertRow();
const cell = row.insertCell(0);
cell.colSpan = 4;
cell.innerText = `... и ещё ${data.preview.length - 20} записей`;
}
document.getElementById('importPreview').style.display = 'block';
} else {
alert('Ошибка предпросмотра: ' + (data.error || ''));
}
} catch (err) {
alert('Ошибка при разборе JSON: ' + err.message);
}
});
document.getElementById('confirmImportBtn')?.addEventListener('click', async () => {
if (!confirm(`Импортировать ${currentPreviewLessons.length} уроков?`)) return;
const importMaxSlots = document.getElementById('importMaxSlots').value;
const response = await fetch('/api/admin/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
lessons: currentPreviewLessons,
defaultMaxSlots: importMaxSlots
})
});
const result = await response.json();
if (result.success) {
const created = result.results.filter(r => r.status === 'created').length;
const skipped = result.results.filter(r => r.status === 'skipped').length;
document.getElementById('importResult').innerHTML = `<span style="color:green">Импорт завершён: создано ${created}, пропущено (дубликаты) ${skipped}</span>`;
loadLessons(getCurrentFilters());
document.getElementById('importPreview').style.display = 'none';
} else {
alert('Ошибка импорта: ' + (result.error || ''));
}
});
}