From 2d007d23590800d4f1591d2befdfbfbd5ac8c7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B0=D0=BB=D1=83=D0=B3=D0=B8=D0=BD=20=D0=9E=D0=BB?= =?UTF-8?q?=D0=B5=D0=B3=20=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B8=D1=87?= <134477kalugin66@users.no-reply.gitverse.ru> Date: Mon, 13 Apr 2026 12:06:31 +0000 Subject: [PATCH] hfcg --- package.json | 20 ++++---- public/admin.html | 60 ++++++++++++------------ public/admin.js | 14 ++++-- public/info.js | 72 ++++++++++++++++++----------- public/k.html | 77 +++++++++++++++++++++++++++++++ public/k.js | 115 ++++++++++++++++++++++++++++++++++++++++++++++ public/main.js | 57 ++++++++++++----------- public/style.css | 2 +- 8 files changed, 318 insertions(+), 99 deletions(-) create mode 100644 public/k.html create mode 100644 public/k.js diff --git a/package.json b/package.json index afb8725..0d92491 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ -{ - "dependencies": { - "axios": "^1.15.0", - "connect-sqlite3": "^0.9.16", - "dotenv": "^17.4.1", - "express": "^5.2.1", - "express-session": "^1.19.0", - "sqlite3": "^6.0.1" - } -} +{ + "dependencies": { + "axios": "^1.15.0", + "connect-sqlite3": "^0.9.16", + "dotenv": "^17.4.1", + "express": "^5.2.1", + "express-session": "^1.19.0", + "sqlite3": "^6.0.1" + } +} diff --git a/public/admin.html b/public/admin.html index 37ef8d2..cd78106 100644 --- a/public/admin.html +++ b/public/admin.html @@ -14,35 +14,35 @@
-
-
- - -
-
- - -
-
- - -
- - - -
+
+
+ + +
+
+ + +
+
+ + +
+ + + +
- +
-

Импорт уроков из JSON

+

Импорт уроков из JSON / Excel

@@ -92,7 +94,7 @@
- + \ No newline at end of file diff --git a/public/admin.js b/public/admin.js index 87392ed..7d63f85 100644 --- a/public/admin.js +++ b/public/admin.js @@ -121,13 +121,19 @@ async function loadLessons(filters = {}) { section.className = 'class-group'; section.innerHTML = `

${className}

`; classLessons.forEach(lesson => { + let dateTimeStr = ''; + if (lesson.topic === 'Консультация' && lesson.date && lesson.time) { + dateTimeStr = `${lesson.date} ${lesson.time}`; + } else { + dateTimeStr = 'Согласно расписания'; + } const div = document.createElement('div'); div.className = 'lesson-item'; div.innerHTML = `
${lesson.subject} — ${lesson.teacher}
Тема: ${lesson.topic || '—'}
- ${lesson.date} ${lesson.time} | Места: ${lesson.current_slots}/${lesson.max_slots} + ${dateTimeStr} | Мест свободно: ${lesson.max_slots - lesson.current_slots}
@@ -178,8 +184,8 @@ function openLessonModal(id = null) { 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; + document.getElementById('date').value = lesson.date || ''; + document.getElementById('time').value = lesson.time || ''; } }); } @@ -252,7 +258,7 @@ function setupEventListeners() { window.location.href = '/'; }); - // ========== ИМПОРТ JSON / XLSX ========== + // Импорт function parseExcelToRecords(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); diff --git a/public/info.js b/public/info.js index 0a0d310..ba8a47d 100644 --- a/public/info.js +++ b/public/info.js @@ -66,7 +66,7 @@ async function loadRegistrations() { document.getElementById('recordsCount').innerText = `Найдено: ${currentRegistrations.length}`; } catch (err) { console.error(err); - document.getElementById('tableBody').innerHTML = 'Ошибка загрузки'; + document.getElementById('tableBody').innerHTML = 'Ошибка загрузки'; } } @@ -76,19 +76,29 @@ function renderTable(registrations) { tbody.innerHTML = 'Нет записей'; return; } - tbody.innerHTML = registrations.map(reg => ` - - ${escapeHtml(reg.parent_name)} - ${escapeHtml(reg.parent_phone)} - ${escapeHtml(reg.class_name)} - ${escapeHtml(reg.subject)} - ${escapeHtml(reg.teacher)} - ${escapeHtml(reg.topic || '—')} - ${escapeHtml(reg.date)} - ${escapeHtml(reg.time)} - ${new Date(reg.created_at).toLocaleString()} - - `).join(''); + tbody.innerHTML = registrations.map(reg => { + let dateStr = '', timeStr = ''; + if (reg.topic === 'Консультация' && reg.date && reg.time) { + dateStr = escapeHtml(reg.date); + timeStr = escapeHtml(reg.time); + } else { + dateStr = 'Согласно расписания'; + timeStr = ''; + } + return ` + + ${escapeHtml(reg.parent_name)} + ${escapeHtml(reg.parent_phone)} + ${escapeHtml(reg.class_name)} + ${escapeHtml(reg.subject)} + ${escapeHtml(reg.teacher)} + ${escapeHtml(reg.topic || '—')} + ${dateStr} + ${timeStr} + ${new Date(reg.created_at).toLocaleString()} + + `; + }).join(''); } function escapeHtml(str) { @@ -106,23 +116,31 @@ function exportToCSV() { alert('Нет данных для экспорта'); return; } - // Заголовки const headers = ['ФИО родителя', 'Телефон', 'Класс', 'Предмет', 'Учитель', 'Тема урока', 'Дата урока', 'Время', 'Дата регистрации']; - const rows = currentRegistrations.map(reg => [ - reg.parent_name, - reg.parent_phone, - reg.class_name, - reg.subject, - reg.teacher, - reg.topic || '', - reg.date, - reg.time, - new Date(reg.created_at).toLocaleString() - ]); + const rows = currentRegistrations.map(reg => { + let dateVal = '', timeVal = ''; + if (reg.topic === 'Консультация' && reg.date && reg.time) { + dateVal = reg.date; + timeVal = reg.time; + } else { + dateVal = 'Согласно расписания'; + timeVal = ''; + } + return [ + reg.parent_name, + reg.parent_phone, + reg.class_name, + reg.subject, + reg.teacher, + reg.topic || '', + dateVal, + timeVal, + new Date(reg.created_at).toLocaleString() + ]; + }); const csvContent = [headers, ...rows] .map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(';')) .join('\n'); - // Добавляем BOM для корректной поддержки кириллицы в Excel const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); diff --git a/public/k.html b/public/k.html new file mode 100644 index 0000000..58ed6e5 --- /dev/null +++ b/public/k.html @@ -0,0 +1,77 @@ + + + + + + Управление корпусами учителей + + + + +
+

Управление корпусами учителей

+
+ +
+
+
+

Список учителей

+ +
+
+ + + + + + + + + + + +
УчительПредметКорпус
Загрузка...
+
+ +
+ + + \ No newline at end of file diff --git a/public/k.js b/public/k.js new file mode 100644 index 0000000..4d7d30f --- /dev/null +++ b/public/k.js @@ -0,0 +1,115 @@ +// public/k.js – управление привязкой учителей к корпусам + +let currentUser = null; +let teachersList = []; // [{ id, name, subject, campus }] + +document.addEventListener('DOMContentLoaded', async () => { + await checkAuth(); + await loadTeachers(); + 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 loadTeachers() { + try { + const res = await fetch('/api/teachers'); + teachersList = await res.json(); + renderTable(); + } catch (err) { + console.error(err); + document.getElementById('tableBody').innerHTML = 'Ошибка загрузки'; + } +} + +function renderTable() { + const tbody = document.getElementById('tableBody'); + if (!teachersList.length) { + tbody.innerHTML = 'Нет учителей'; + return; + } + tbody.innerHTML = teachersList.map(teacher => ` + + ${escapeHtml(teacher.name)} + ${escapeHtml(teacher.subject || '—')} + + + + + `).join(''); +} + +async function saveAllChanges() { + const updates = []; + document.querySelectorAll('.campus-select').forEach(select => { + const teacherId = parseInt(select.dataset.id); + const newCampus = select.value; + updates.push({ id: teacherId, campus: newCampus }); + }); + + const messageDiv = document.getElementById('message'); + messageDiv.style.display = 'block'; + messageDiv.className = 'message'; + messageDiv.innerHTML = 'Сохранение...'; + + try { + const res = await fetch('/api/teachers/campus/batch', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ updates }) + }); + const data = await res.json(); + if (res.ok) { + messageDiv.className = 'message success'; + messageDiv.innerHTML = '✅ Изменения сохранены'; + setTimeout(() => messageDiv.style.display = 'none', 3000); + // обновляем локальные данные + teachersList = teachersList.map(t => { + const update = updates.find(u => u.id === t.id); + if (update) t.campus = update.campus; + return t; + }); + } else { + throw new Error(data.error || 'Ошибка сохранения'); + } + } catch (err) { + messageDiv.className = 'message error'; + messageDiv.innerHTML = `❌ Ошибка: ${err.message}`; + setTimeout(() => messageDiv.style.display = 'none', 3000); + } +} + +function setupEventListeners() { + document.getElementById('saveAllBtn')?.addEventListener('click', saveAllChanges); + document.getElementById('logoutBtn')?.addEventListener('click', async () => { + await fetch('/api/logout', { method: 'POST' }); + window.location.href = '/'; + }); +} + +function escapeHtml(str) { + if (!str) return ''; + return str.replace(/[&<>]/g, function(m) { + if (m === '&') return '&'; + if (m === '<') return '<'; + if (m === '>') return '>'; + return m; + }); +} \ No newline at end of file diff --git a/public/main.js b/public/main.js index 768d6f7..0999012 100644 --- a/public/main.js +++ b/public/main.js @@ -2,7 +2,6 @@ let allLessons = []; -// Загрузка опций для выпадающих списков async function loadFilterOptions() { try { const [classes, teachers, topics] = await Promise.all([ @@ -10,7 +9,6 @@ async function loadFilterOptions() { 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; @@ -34,7 +32,6 @@ function populateSelect(selectId, options, defaultLabel) { option.textContent = opt; select.appendChild(option); }); - // Восстанавливаем выбранное значение, если оно ещё допустимо if (currentValue && options.includes(currentValue)) { select.value = currentValue; } else { @@ -42,12 +39,11 @@ function populateSelect(selectId, options, defaultLabel) { } } -// Загрузка всех уроков с сервера async function loadLessons() { try { const res = await fetch('/api/lessons'); allLessons = await res.json(); - updateDependentFilters(); // первоначальное построение зависимых списков + updateDependentFilters(); applyFilters(); } catch (err) { console.error('Ошибка загрузки уроков', err); @@ -55,13 +51,11 @@ async function loadLessons() { } } -// Обновление зависимых выпадающих списков на основе текущих фильтров function updateDependentFilters() { const selectedClass = document.getElementById('filterClass').value; const selectedTeacher = document.getElementById('filterTeacher').value; const selectedTopic = document.getElementById('filterTopic').value; - // Фильтруем уроки по выбранным значениям (если они не пустые) let filteredLessons = allLessons; if (selectedClass) { filteredLessons = filteredLessons.filter(l => l.class_name === selectedClass); @@ -73,12 +67,10 @@ function updateDependentFilters() { 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(); - // Обновляем select, сохраняя текущие значения, если они допустимы const classSelect = document.getElementById('filterClass'); const teacherSelect = document.getElementById('filterTeacher'); const topicSelect = document.getElementById('filterTopic'); @@ -91,7 +83,6 @@ function updateDependentFilters() { populateSelect('filterTeacher', availableTeachers, 'Все учителя'); populateSelect('filterTopic', availableTopics, 'Все темы'); - // Если старое значение не было сброшено populateSelect, восстанавливаем if (oldClass && availableClasses.includes(oldClass)) classSelect.value = oldClass; else classSelect.value = ''; if (oldTeacher && availableTeachers.includes(oldTeacher)) teacherSelect.value = oldTeacher; @@ -100,16 +91,12 @@ function updateDependentFilters() { else topicSelect.value = ''; } -// Применение фильтров + скрытие уроков без свободных мест function applyFilters() { const classFilter = document.getElementById('filterClass').value; const teacherFilter = document.getElementById('filterTeacher').value; const topicFilter = document.getElementById('filterTopic').value; - // Сначала отбираем только доступные уроки (есть свободные места) let filtered = allLessons.filter(lesson => lesson.available === true); - - // Точное совпадение для select (не частичное) if (classFilter) filtered = filtered.filter(lesson => lesson.class_name === classFilter); if (teacherFilter) filtered = filtered.filter(lesson => lesson.teacher === teacherFilter); if (topicFilter) filtered = filtered.filter(lesson => lesson.topic === topicFilter); @@ -117,7 +104,6 @@ function applyFilters() { renderLessons(filtered); } -// Отрисовка карточек уроков + обновление счётчика function renderLessons(lessons) { const container = document.getElementById('lessonsContainer'); const counterSpan = document.getElementById('availableCount'); @@ -129,16 +115,27 @@ function renderLessons(lessons) { return; } - container.innerHTML = lessons.map(lesson => ` -
-

${escapeHtml(lesson.class_name)} | ${escapeHtml(lesson.subject)}

-

Учитель: ${escapeHtml(lesson.teacher)}

-

Тема: ${escapeHtml(lesson.topic || '—')}

-
- Свободных мест: ${lesson.max_slots - lesson.current_slots} из ${lesson.max_slots} + container.innerHTML = lessons.map(lesson => { + // Определяем, что показывать в строке времени + let timeHtml = ''; + if (lesson.topic === 'Консультация' && lesson.date && lesson.time) { + timeHtml = `

Дата/время: ${escapeHtml(lesson.date)} ${escapeHtml(lesson.time)}

`; + } else { + timeHtml = `

Время: Согласно расписания

`; + } + + return ` +
+

${escapeHtml(lesson.class_name)} | ${escapeHtml(lesson.subject)}

+

Учитель: ${escapeHtml(lesson.teacher)}

+

Тема: ${escapeHtml(lesson.topic || '—')}

+ ${timeHtml} +
+ Свободных мест: ${lesson.max_slots - lesson.current_slots} +
-
- `).join(''); + `; + }).join(''); if (counterSpan) counterSpan.textContent = `${lessons.length}`; @@ -147,7 +144,6 @@ function renderLessons(lessons) { }); } -// Модальное окно записи (без изменений) function setupModal() { const modal = document.getElementById('modal'); const closeSpan = modal.querySelector('.close'); @@ -195,11 +191,18 @@ function openModal(lessonId) { const lesson = allLessons.find(l => l.id == lessonId); if (!lesson) return; + let timeInfo = ''; + if (lesson.topic === 'Консультация' && lesson.date && lesson.time) { + timeInfo = `${lesson.date} ${lesson.time}`; + } else { + timeInfo = 'Согласно расписания'; + } + document.getElementById('lessonId').value = lessonId; document.getElementById('modalLessonInfo').innerHTML = ` ${escapeHtml(lesson.class_name)}
${escapeHtml(lesson.subject)} — ${escapeHtml(lesson.teacher)}
- ${lesson.date} ${lesson.time} + ${timeInfo} `; document.getElementById('modalMessage').innerHTML = ''; document.getElementById('registrationForm').reset(); @@ -216,13 +219,11 @@ function escapeHtml(str) { }); } -// Инициализация document.addEventListener('DOMContentLoaded', async () => { await loadFilterOptions(); await loadLessons(); setupModal(); - // Обработчики изменений фильтров const classSelect = document.getElementById('filterClass'); const teacherSelect = document.getElementById('filterTeacher'); const topicSelect = document.getElementById('filterTopic'); diff --git a/public/style.css b/public/style.css index 226edfa..5d2ed42 100644 --- a/public/style.css +++ b/public/style.css @@ -174,7 +174,7 @@ form input, form textarea { display: flex; flex-direction: column; gap: 0.3rem; - min-width: 150px; + min-width: 100px; max-width: 450px; }