// public/admin.js – панель администратора (локальная XLSX) 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 = ``; 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 = ''; } } 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(); 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 = `
Нет записей
'; 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 class_name = document.getElementById('className').value.trim(); const parallel = parseInt(document.getElementById('parallel').value); const subject = document.getElementById('subject').value.trim(); const teacher = document.getElementById('teacher').value.trim(); const topic = document.getElementById('topic').value; const max_slots = parseInt(document.getElementById('maxSlots').value); const date = document.getElementById('date').value; const time = document.getElementById('time').value; // Проверяем обязательные поля (дата и время теперь обязательны) if (!class_name || isNaN(parallel) || !subject || !teacher || isNaN(max_slots) || !date || !time) { alert('Пожалуйста, заполните все поля: класс, параллель, предмет, учитель, макс. мест, дата и время.'); return; } const payload = { id: id || undefined, class_name, parallel, subject, teacher, topic, max_slots, date, time }; try { const response = await fetch('/api/admin/lessons', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); const data = await response.json(); if (!response.ok) { alert('Ошибка сохранения: ' + (data.error || 'Неизвестная ошибка')); return; } document.getElementById('lessonModal').style.display = 'none'; // Сбрасываем фильтры, чтобы обновлённый урок точно отобразился resetFilters(); loadLessons(getCurrentFilters()); } catch (err) { alert('Ошибка соединения: ' + err.message); } }); 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 resetFilters() { document.getElementById('filterClass').value = ''; document.getElementById('filterParallel').value = ''; document.getElementById('filterTeacher').value = ''; document.getElementById('filterTopic').value = ''; updateDependentFilters(); } 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', () => { resetFilters(); loadLessons({}); }); document.getElementById('addLessonBtn')?.addEventListener('click', () => openLessonModal()); document.getElementById('logoutBtn')?.addEventListener('click', async () => { await fetch('/api/logout', { method: 'POST' }); window.location.href = '/'; }); // Кнопка очистки всей базы document.getElementById('clearDbBtn')?.addEventListener('click', async () => { const confirmed = confirm('⚠️ ВНИМАНИЕ! Вы собираетесь УДАЛИТЬ ВСЕ уроки и все записи родителей. Это действие необратимо. Продолжить?'); if (!confirmed) return; try { const res = await fetch('/api/admin/clear-db', { method: 'POST' }); const data = await res.json(); if (res.ok && data.success) { alert('База данных успешно очищена.'); loadLessons(getCurrentFilters()); } else { alert('Ошибка при очистке: ' + (data.error || 'неизвестная ошибка')); } } catch (err) { alert('Ошибка соединения: ' + err.message); } }); // Импорт (XLSX) function parseExcelToRecords(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function(e) { const data = new Uint8Array(e.target.result); let workbook; try { workbook = XLSX.read(data, { type: 'array' }); } catch (err) { reject(new Error('Ошибка чтения Excel: ' + err.message)); return; } const firstSheet = workbook.Sheets[workbook.SheetNames[0]]; const rows = XLSX.utils.sheet_to_json(firstSheet, { header: 1, defval: "" }); if (!rows || rows.length < 2) { reject(new Error("Файл не содержит данных")); return; } const headers = rows[0].map(h => String(h || "").trim()); const expectedHeaders = [ "Фамилия Учителя", "Имя Учителя", "Отчество Учителя", "Предмет", "Тема Урока", "Класс (параллель)", "Класс (буква)" ]; const missing = expectedHeaders.filter(h => !headers.includes(h)); if (missing.length) { reject(new Error(`В файле отсутствуют столбцы: ${missing.join(", ")}`)); return; } const idx = {}; expectedHeaders.forEach(h => { idx[h] = headers.indexOf(h); }); const records = []; for (let i = 1; i < rows.length; i++) { const row = rows[i]; if (!row || row.length === 0) continue; const teacherLast = row[idx["Фамилия Учителя"]] || ""; const teacherFirst = row[idx["Имя Учителя"]] || ""; const teacherPatr = row[idx["Отчество Учителя"]] || ""; const subject = row[idx["Предмет"]] || ""; const topic = row[idx["Тема Урока"]] || ""; const parallelRaw = row[idx["Класс (параллель)"]]?.toString() || ""; const classLetter = row[idx["Класс (буква)"]]?.toString() || ""; if (!teacherLast && !teacherFirst && !subject) continue; records.push({ "Фамилия Учителя": teacherLast, "Имя Учителя": teacherFirst, "Отчество Учителя": teacherPatr, "Предмет": subject, "Тема Урока": topic, "Класс (параллель)": parallelRaw, "Класс (буква)": classLetter }); } resolve(records); }; reader.onerror = reject; reader.readAsArrayBuffer(file); }); } document.getElementById('previewImportBtn')?.addEventListener('click', async () => { const file = document.getElementById('jsonFileInput').files[0]; if (!file) return alert('Выберите JSON или Excel файл'); const importMaxSlots = document.getElementById('importMaxSlots').value; if (!importMaxSlots) return alert('Укажите максимальное количество мест'); let records = null; const fileExt = file.name.split('.').pop().toLowerCase(); try { if (fileExt === 'json') { const text = await file.text(); records = JSON.parse(text); } else if (fileExt === 'xlsx' || fileExt === 'xls') { records = await parseExcelToRecords(file); } else { alert('Неподдерживаемый формат. Используйте .json, .xlsx или .xls'); return; } const response = await fetch('/api/admin/import/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonData: records }) }); 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('Ошибка при разборе файла: ' + 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 = `Импорт завершён: создано ${created}, пропущено (дубликаты) ${skipped}`; loadLessons(getCurrentFilters()); document.getElementById('importPreview').style.display = 'none'; } else { alert('Ошибка импорта: ' + (result.error || '')); } }); }