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:
Калугин Олег Александрович
2026-04-12 19:05:12 +00:00
committed by GitVerse
parent 458b1fa927
commit 3fbf7311d8
13 changed files with 2018 additions and 0 deletions

322
public/admin.js Normal file
View 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 || ''));
}
});
}