create: admin.html, admin.js, help.html, index.html, info.html, info.js, login.html, main.js, style.css, auth.js, package.json, server.js, sqllite.js
This commit is contained in:
committed by
GitVerse
parent
458b1fa927
commit
3fbf7311d8
322
public/admin.js
Normal file
322
public/admin.js
Normal file
@@ -0,0 +1,322 @@
|
||||
// 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 || ''));
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user