This commit is contained in:
Калугин Олег Александрович
2026-04-13 12:06:31 +00:00
committed by GitVerse
parent ae3227f127
commit 2d007d2359
8 changed files with 318 additions and 99 deletions

View File

@@ -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"
}
}

View File

@@ -14,35 +14,35 @@
<main>
<div class="admin-controls">
<button id="addLessonBtn">+ Добавить урок</button>
<div class="filters">
<div class="filter-group">
<label>Класс</label>
<select id="filterClass">
<option value="">Все классы</option>
</select>
</div>
<div class="filter-group">
<label>Учитель</label>
<select id="filterTeacher">
<option value="">Все учителя</option>
</select>
</div>
<div class="filter-group">
<label>Тема урока</label>
<select id="filterTopic">
<option value="">Все темы</option>
</select>
</div>
<input type="text" id="filterParallel" placeholder="Параллель (цифра)">
<button id="applyFilters">Применить</button>
<button id="resetFilters" class="reset-btn">Сбросить</button>
</div>
<div class="filters">
<div class="filter-group">
<label>Класс</label>
<select id="filterClass">
<option value="">Все классы</option>
</select>
</div>
<div class="filter-group">
<label>Учитель</label>
<select id="filterTeacher">
<option value="">Все учителя</option>
</select>
</div>
<div class="filter-group">
<label>Тема урока</label>
<select id="filterTopic">
<option value="">Все темы</option>
</select>
</div>
<input type="text" id="filterParallel" placeholder="Параллель (цифра)">
<button id="applyFilters">Применить</button>
<button id="resetFilters" class="reset-btn">Сбросить</button>
</div>
</div>
<div id="lessonsList" class="lessons-list"></div>
<!-- Импорт JSON (без даты/времени) -->
<!-- Импорт JSON / Excel -->
<div class="import-section">
<h3>Импорт уроков из JSON</h3>
<h3>Импорт уроков из JSON / Excel</h3>
<input type="file" id="jsonFileInput" accept=".json, .xlsx, .xls">
<div class="import-params">
<label>Макс. мест (сколько родителей может записаться):
@@ -54,7 +54,9 @@
<h4>Предпросмотр (первые 20 записей)</h4>
<div style="overflow-x:auto;">
<table id="previewTable" border="1" cellpadding="5">
<thead><tr><th>Класс</th><th>Предмет</th><th>Учитель</th><th>Тема</th></tr></thead>
<thead>
<tr><th>Класс</th><th>Предмет</th><th>Учитель</th><th>Тема</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
@@ -76,8 +78,8 @@
<label>Учитель: <input type="text" id="teacher" required></label>
<label>Тема урока: <input type="text" id="topic"></label>
<label>Макс. мест: <input type="number" id="maxSlots" required></label>
<label>Дата: <input type="date" id="date" required></label>
<label>Время: <input type="time" id="time" required></label>
<label>Дата: <input type="date" id="date"></label>
<label>Время: <input type="time" id="time"></label>
<button type="submit">Сохранить</button>
</form>
</div>
@@ -92,7 +94,7 @@
</div>
</div>
</main>
<script src="https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js"></script>
<!--<script src="https://cdn.sheetjs.com/xlsx-0.20.2/package/dist/xlsx.full.min.js"></script>-->
<script src="admin.js"></script>
</body>
</html>

View File

@@ -121,13 +121,19 @@ async function loadLessons(filters = {}) {
section.className = 'class-group';
section.innerHTML = `<h2>${className}</h2>`;
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 = `
<div>
<strong>${lesson.subject}</strong> — ${lesson.teacher}<br>
<em>Тема: ${lesson.topic || '—'}</em><br>
${lesson.date} ${lesson.time} | Места: ${lesson.current_slots}/${lesson.max_slots}
${dateTimeStr} | Мест свободно: ${lesson.max_slots - lesson.current_slots}
</div>
<div class="lesson-actions">
<button class="viewRegBtn" data-id="${lesson.id}">Записи</button>
@@ -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();

View File

@@ -66,7 +66,7 @@ async function loadRegistrations() {
document.getElementById('recordsCount').innerText = `Найдено: ${currentRegistrations.length}`;
} catch (err) {
console.error(err);
document.getElementById('tableBody').innerHTML = '<tr><td colspan="9">Ошибка загрузки</td></tr>';
document.getElementById('tableBody').innerHTML = '<td><td colspan="9">Ошибка загрузки</td></tr>';
}
}
@@ -76,19 +76,29 @@ function renderTable(registrations) {
tbody.innerHTML = '<tr><td colspan="9">Нет записей</td></tr>';
return;
}
tbody.innerHTML = registrations.map(reg => `
<tr>
<td>${escapeHtml(reg.parent_name)}</td>
<td>${escapeHtml(reg.parent_phone)}</td>
<td>${escapeHtml(reg.class_name)}</td>
<td>${escapeHtml(reg.subject)}</td>
<td>${escapeHtml(reg.teacher)}</td>
<td>${escapeHtml(reg.topic || '')}</td>
<td>${escapeHtml(reg.date)}</td>
<td>${escapeHtml(reg.time)}</td>
<td>${new Date(reg.created_at).toLocaleString()}</td>
</tr>
`).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 `
<tr>
<td>${escapeHtml(reg.parent_name)}</td>
<td>${escapeHtml(reg.parent_phone)}</td>
<td>${escapeHtml(reg.class_name)}</td>
<td>${escapeHtml(reg.subject)}</td>
<td>${escapeHtml(reg.teacher)}</td>
<td>${escapeHtml(reg.topic || '—')}</td>
<td>${dateStr}</td>
<td>${timeStr}</td>
<td>${new Date(reg.created_at).toLocaleString()}</td>
</tr>
`;
}).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);

77
public/k.html Normal file
View File

@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Управление корпусами учителей</title>
<link rel="stylesheet" href="style.css">
<style>
.teachers-table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 1rem;
overflow: hidden;
}
.teachers-table th, .teachers-table td {
border: 1px solid #e2e8f0;
padding: 0.75rem;
text-align: left;
}
.teachers-table th {
background: #f1f5f9;
}
.campus-select {
padding: 0.4rem;
border-radius: 0.5rem;
border: 1px solid #cbd5e1;
}
.save-btn {
background: #2d6a4f;
margin: 1rem 0;
}
.message {
margin-top: 1rem;
padding: 0.5rem;
border-radius: 0.5rem;
}
.success {
background: #d1fae5;
color: #065f46;
}
.error {
background: #fee2e2;
color: #991b1b;
}
</style>
</head>
<body>
<header>
<h1>Управление корпусами учителей</h1>
<div id="userInfo"></div>
<button id="logoutBtn">Выйти</button>
</header>
<main style="padding: 2rem;">
<div class="header-actions">
<h2>Список учителей</h2>
<button id="saveAllBtn" class="save-btn">💾 Сохранить все изменения</button>
</div>
<div style="overflow-x: auto;">
<table class="teachers-table" id="teachersTable">
<thead>
<tr>
<th>Учитель</th>
<th>Предмет</th>
<th>Корпус</th>
</tr>
</thead>
<tbody id="tableBody">
<tr><td colspan="3">Загрузка...</td></tr>
</tbody>
</table>
</div>
<div id="message" class="message" style="display:none;"></div>
</main>
<script src="k.js"></script>
</body>
</html>

115
public/k.js Normal file
View File

@@ -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 = '<tr><td colspan="3">Ошибка загрузки</td></tr>';
}
}
function renderTable() {
const tbody = document.getElementById('tableBody');
if (!teachersList.length) {
tbody.innerHTML = '<tr><td colspan="3">Нет учителей</td></tr>';
return;
}
tbody.innerHTML = teachersList.map(teacher => `
<tr data-id="${teacher.id}">
<td>${escapeHtml(teacher.name)}</td>
<td>${escapeHtml(teacher.subject || '—')}</td>
<td>
<select class="campus-select" data-id="${teacher.id}">
<option value="" ${teacher.campus === '' ? 'selected' : ''}>Оба корпуса</option>
<option value="Феофанова 10" ${teacher.campus === 'Феофанова 10' ? 'selected' : ''}>Феофанова 10</option>
<option value="Цветоносная 2" ${teacher.campus === 'Цветоносная 2' ? 'selected' : ''}>Цветоносная 2</option>
</select>
</td>
</tr>
`).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 '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}

View File

@@ -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 => `
<div class="lesson-card" data-id="${lesson.id}">
<h3>${escapeHtml(lesson.class_name)} | ${escapeHtml(lesson.subject)}</h3>
<p><strong>Учитель:</strong> ${escapeHtml(lesson.teacher)}</p>
<p><strong>Тема:</strong> ${escapeHtml(lesson.topic || '—')}</p>
<div class="slots">
Свободных мест: ${lesson.max_slots - lesson.current_slots} из ${lesson.max_slots}
container.innerHTML = lessons.map(lesson => {
// Определяем, что показывать в строке времени
let timeHtml = '';
if (lesson.topic === 'Консультация' && lesson.date && lesson.time) {
timeHtml = `<p><strong>Дата/время:</strong> ${escapeHtml(lesson.date)} ${escapeHtml(lesson.time)}</p>`;
} else {
timeHtml = `<p><strong>Время:</strong> Согласно расписания</p>`;
}
return `
<div class="lesson-card" data-id="${lesson.id}">
<h3>${escapeHtml(lesson.class_name)} | ${escapeHtml(lesson.subject)}</h3>
<p><strong>Учитель:</strong> ${escapeHtml(lesson.teacher)}</p>
<p><strong>Тема:</strong> ${escapeHtml(lesson.topic || '—')}</p>
${timeHtml}
<div class="slots">
Свободных мест: ${lesson.max_slots - lesson.current_slots}
</div>
</div>
</div>
`).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 = `
<strong>${escapeHtml(lesson.class_name)}</strong><br>
${escapeHtml(lesson.subject)}${escapeHtml(lesson.teacher)}<br>
<small>${lesson.date} ${lesson.time}</small>
<small>${timeInfo}</small>
`;
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');

View File

@@ -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;
}