hfcg
This commit is contained in:
committed by
GitVerse
parent
ae3227f127
commit
2d007d2359
20
package.json
20
package.json
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.15.0",
|
"axios": "^1.15.0",
|
||||||
"connect-sqlite3": "^0.9.16",
|
"connect-sqlite3": "^0.9.16",
|
||||||
"dotenv": "^17.4.1",
|
"dotenv": "^17.4.1",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"express-session": "^1.19.0",
|
"express-session": "^1.19.0",
|
||||||
"sqlite3": "^6.0.1"
|
"sqlite3": "^6.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,35 +14,35 @@
|
|||||||
<main>
|
<main>
|
||||||
<div class="admin-controls">
|
<div class="admin-controls">
|
||||||
<button id="addLessonBtn">+ Добавить урок</button>
|
<button id="addLessonBtn">+ Добавить урок</button>
|
||||||
<div class="filters">
|
<div class="filters">
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>Класс</label>
|
<label>Класс</label>
|
||||||
<select id="filterClass">
|
<select id="filterClass">
|
||||||
<option value="">Все классы</option>
|
<option value="">Все классы</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>Учитель</label>
|
<label>Учитель</label>
|
||||||
<select id="filterTeacher">
|
<select id="filterTeacher">
|
||||||
<option value="">Все учителя</option>
|
<option value="">Все учителя</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group">
|
<div class="filter-group">
|
||||||
<label>Тема урока</label>
|
<label>Тема урока</label>
|
||||||
<select id="filterTopic">
|
<select id="filterTopic">
|
||||||
<option value="">Все темы</option>
|
<option value="">Все темы</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" id="filterParallel" placeholder="Параллель (цифра)">
|
<input type="text" id="filterParallel" placeholder="Параллель (цифра)">
|
||||||
<button id="applyFilters">Применить</button>
|
<button id="applyFilters">Применить</button>
|
||||||
<button id="resetFilters" class="reset-btn">Сбросить</button>
|
<button id="resetFilters" class="reset-btn">Сбросить</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="lessonsList" class="lessons-list"></div>
|
<div id="lessonsList" class="lessons-list"></div>
|
||||||
|
|
||||||
<!-- Импорт JSON (без даты/времени) -->
|
<!-- Импорт JSON / Excel -->
|
||||||
<div class="import-section">
|
<div class="import-section">
|
||||||
<h3>Импорт уроков из JSON</h3>
|
<h3>Импорт уроков из JSON / Excel</h3>
|
||||||
<input type="file" id="jsonFileInput" accept=".json, .xlsx, .xls">
|
<input type="file" id="jsonFileInput" accept=".json, .xlsx, .xls">
|
||||||
<div class="import-params">
|
<div class="import-params">
|
||||||
<label>Макс. мест (сколько родителей может записаться):
|
<label>Макс. мест (сколько родителей может записаться):
|
||||||
@@ -54,7 +54,9 @@
|
|||||||
<h4>Предпросмотр (первые 20 записей)</h4>
|
<h4>Предпросмотр (первые 20 записей)</h4>
|
||||||
<div style="overflow-x:auto;">
|
<div style="overflow-x:auto;">
|
||||||
<table id="previewTable" border="1" cellpadding="5">
|
<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>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@@ -76,8 +78,8 @@
|
|||||||
<label>Учитель: <input type="text" id="teacher" required></label>
|
<label>Учитель: <input type="text" id="teacher" required></label>
|
||||||
<label>Тема урока: <input type="text" id="topic"></label>
|
<label>Тема урока: <input type="text" id="topic"></label>
|
||||||
<label>Макс. мест: <input type="number" id="maxSlots" required></label>
|
<label>Макс. мест: <input type="number" id="maxSlots" required></label>
|
||||||
<label>Дата: <input type="date" id="date" required></label>
|
<label>Дата: <input type="date" id="date"></label>
|
||||||
<label>Время: <input type="time" id="time" required></label>
|
<label>Время: <input type="time" id="time"></label>
|
||||||
<button type="submit">Сохранить</button>
|
<button type="submit">Сохранить</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,7 +94,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</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>
|
<script src="admin.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -121,13 +121,19 @@ async function loadLessons(filters = {}) {
|
|||||||
section.className = 'class-group';
|
section.className = 'class-group';
|
||||||
section.innerHTML = `<h2>${className}</h2>`;
|
section.innerHTML = `<h2>${className}</h2>`;
|
||||||
classLessons.forEach(lesson => {
|
classLessons.forEach(lesson => {
|
||||||
|
let dateTimeStr = '';
|
||||||
|
if (lesson.topic === 'Консультация' && lesson.date && lesson.time) {
|
||||||
|
dateTimeStr = `${lesson.date} ${lesson.time}`;
|
||||||
|
} else {
|
||||||
|
dateTimeStr = 'Согласно расписания';
|
||||||
|
}
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.className = 'lesson-item';
|
div.className = 'lesson-item';
|
||||||
div.innerHTML = `
|
div.innerHTML = `
|
||||||
<div>
|
<div>
|
||||||
<strong>${lesson.subject}</strong> — ${lesson.teacher}<br>
|
<strong>${lesson.subject}</strong> — ${lesson.teacher}<br>
|
||||||
<em>Тема: ${lesson.topic || '—'}</em><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>
|
||||||
<div class="lesson-actions">
|
<div class="lesson-actions">
|
||||||
<button class="viewRegBtn" data-id="${lesson.id}">Записи</button>
|
<button class="viewRegBtn" data-id="${lesson.id}">Записи</button>
|
||||||
@@ -178,8 +184,8 @@ function openLessonModal(id = null) {
|
|||||||
document.getElementById('teacher').value = lesson.teacher;
|
document.getElementById('teacher').value = lesson.teacher;
|
||||||
document.getElementById('topic').value = lesson.topic || '';
|
document.getElementById('topic').value = lesson.topic || '';
|
||||||
document.getElementById('maxSlots').value = lesson.max_slots;
|
document.getElementById('maxSlots').value = lesson.max_slots;
|
||||||
document.getElementById('date').value = lesson.date;
|
document.getElementById('date').value = lesson.date || '';
|
||||||
document.getElementById('time').value = lesson.time;
|
document.getElementById('time').value = lesson.time || '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -252,7 +258,7 @@ function setupEventListeners() {
|
|||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========== ИМПОРТ JSON / XLSX ==========
|
// Импорт
|
||||||
function parseExcelToRecords(file) {
|
function parseExcelToRecords(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ async function loadRegistrations() {
|
|||||||
document.getElementById('recordsCount').innerText = `Найдено: ${currentRegistrations.length}`;
|
document.getElementById('recordsCount').innerText = `Найдено: ${currentRegistrations.length}`;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(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>';
|
tbody.innerHTML = '<tr><td colspan="9">Нет записей</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tbody.innerHTML = registrations.map(reg => `
|
tbody.innerHTML = registrations.map(reg => {
|
||||||
<tr>
|
let dateStr = '', timeStr = '';
|
||||||
<td>${escapeHtml(reg.parent_name)}</td>
|
if (reg.topic === 'Консультация' && reg.date && reg.time) {
|
||||||
<td>${escapeHtml(reg.parent_phone)}</td>
|
dateStr = escapeHtml(reg.date);
|
||||||
<td>${escapeHtml(reg.class_name)}</td>
|
timeStr = escapeHtml(reg.time);
|
||||||
<td>${escapeHtml(reg.subject)}</td>
|
} else {
|
||||||
<td>${escapeHtml(reg.teacher)}</td>
|
dateStr = 'Согласно расписания';
|
||||||
<td>${escapeHtml(reg.topic || '—')}</td>
|
timeStr = '';
|
||||||
<td>${escapeHtml(reg.date)}</td>
|
}
|
||||||
<td>${escapeHtml(reg.time)}</td>
|
return `
|
||||||
<td>${new Date(reg.created_at).toLocaleString()}</td>
|
<tr>
|
||||||
</tr>
|
<td>${escapeHtml(reg.parent_name)}</td>
|
||||||
`).join('');
|
<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) {
|
function escapeHtml(str) {
|
||||||
@@ -106,23 +116,31 @@ function exportToCSV() {
|
|||||||
alert('Нет данных для экспорта');
|
alert('Нет данных для экспорта');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Заголовки
|
|
||||||
const headers = ['ФИО родителя', 'Телефон', 'Класс', 'Предмет', 'Учитель', 'Тема урока', 'Дата урока', 'Время', 'Дата регистрации'];
|
const headers = ['ФИО родителя', 'Телефон', 'Класс', 'Предмет', 'Учитель', 'Тема урока', 'Дата урока', 'Время', 'Дата регистрации'];
|
||||||
const rows = currentRegistrations.map(reg => [
|
const rows = currentRegistrations.map(reg => {
|
||||||
reg.parent_name,
|
let dateVal = '', timeVal = '';
|
||||||
reg.parent_phone,
|
if (reg.topic === 'Консультация' && reg.date && reg.time) {
|
||||||
reg.class_name,
|
dateVal = reg.date;
|
||||||
reg.subject,
|
timeVal = reg.time;
|
||||||
reg.teacher,
|
} else {
|
||||||
reg.topic || '',
|
dateVal = 'Согласно расписания';
|
||||||
reg.date,
|
timeVal = '';
|
||||||
reg.time,
|
}
|
||||||
new Date(reg.created_at).toLocaleString()
|
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]
|
const csvContent = [headers, ...rows]
|
||||||
.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(';'))
|
.map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(';'))
|
||||||
.join('\n');
|
.join('\n');
|
||||||
// Добавляем BOM для корректной поддержки кириллицы в Excel
|
|
||||||
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
const blob = new Blob(['\uFEFF' + csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
|
|||||||
77
public/k.html
Normal file
77
public/k.html
Normal 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
115
public/k.js
Normal 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 '&';
|
||||||
|
if (m === '<') return '<';
|
||||||
|
if (m === '>') return '>';
|
||||||
|
return m;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
let allLessons = [];
|
let allLessons = [];
|
||||||
|
|
||||||
// Загрузка опций для выпадающих списков
|
|
||||||
async function loadFilterOptions() {
|
async function loadFilterOptions() {
|
||||||
try {
|
try {
|
||||||
const [classes, teachers, topics] = await Promise.all([
|
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/teachers').then(r => r.json()),
|
||||||
fetch('/api/filter-options/topics').then(r => r.json())
|
fetch('/api/filter-options/topics').then(r => r.json())
|
||||||
]);
|
]);
|
||||||
// Сохраняем полные списки для дальнейшей фильтрации
|
|
||||||
window.allClassNames = classes;
|
window.allClassNames = classes;
|
||||||
window.allTeachers = teachers;
|
window.allTeachers = teachers;
|
||||||
window.allTopics = topics;
|
window.allTopics = topics;
|
||||||
@@ -34,7 +32,6 @@ function populateSelect(selectId, options, defaultLabel) {
|
|||||||
option.textContent = opt;
|
option.textContent = opt;
|
||||||
select.appendChild(option);
|
select.appendChild(option);
|
||||||
});
|
});
|
||||||
// Восстанавливаем выбранное значение, если оно ещё допустимо
|
|
||||||
if (currentValue && options.includes(currentValue)) {
|
if (currentValue && options.includes(currentValue)) {
|
||||||
select.value = currentValue;
|
select.value = currentValue;
|
||||||
} else {
|
} else {
|
||||||
@@ -42,12 +39,11 @@ function populateSelect(selectId, options, defaultLabel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка всех уроков с сервера
|
|
||||||
async function loadLessons() {
|
async function loadLessons() {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/lessons');
|
const res = await fetch('/api/lessons');
|
||||||
allLessons = await res.json();
|
allLessons = await res.json();
|
||||||
updateDependentFilters(); // первоначальное построение зависимых списков
|
updateDependentFilters();
|
||||||
applyFilters();
|
applyFilters();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка загрузки уроков', err);
|
console.error('Ошибка загрузки уроков', err);
|
||||||
@@ -55,13 +51,11 @@ async function loadLessons() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление зависимых выпадающих списков на основе текущих фильтров
|
|
||||||
function updateDependentFilters() {
|
function updateDependentFilters() {
|
||||||
const selectedClass = document.getElementById('filterClass').value;
|
const selectedClass = document.getElementById('filterClass').value;
|
||||||
const selectedTeacher = document.getElementById('filterTeacher').value;
|
const selectedTeacher = document.getElementById('filterTeacher').value;
|
||||||
const selectedTopic = document.getElementById('filterTopic').value;
|
const selectedTopic = document.getElementById('filterTopic').value;
|
||||||
|
|
||||||
// Фильтруем уроки по выбранным значениям (если они не пустые)
|
|
||||||
let filteredLessons = allLessons;
|
let filteredLessons = allLessons;
|
||||||
if (selectedClass) {
|
if (selectedClass) {
|
||||||
filteredLessons = filteredLessons.filter(l => l.class_name === selectedClass);
|
filteredLessons = filteredLessons.filter(l => l.class_name === selectedClass);
|
||||||
@@ -73,12 +67,10 @@ function updateDependentFilters() {
|
|||||||
filteredLessons = filteredLessons.filter(l => l.topic === selectedTopic);
|
filteredLessons = filteredLessons.filter(l => l.topic === selectedTopic);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Извлекаем уникальные значения для каждого поля
|
|
||||||
const availableClasses = [...new Set(filteredLessons.map(l => l.class_name))].sort();
|
const availableClasses = [...new Set(filteredLessons.map(l => l.class_name))].sort();
|
||||||
const availableTeachers = [...new Set(filteredLessons.map(l => l.teacher))].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 availableTopics = [...new Set(filteredLessons.map(l => l.topic).filter(t => t))].sort();
|
||||||
|
|
||||||
// Обновляем select, сохраняя текущие значения, если они допустимы
|
|
||||||
const classSelect = document.getElementById('filterClass');
|
const classSelect = document.getElementById('filterClass');
|
||||||
const teacherSelect = document.getElementById('filterTeacher');
|
const teacherSelect = document.getElementById('filterTeacher');
|
||||||
const topicSelect = document.getElementById('filterTopic');
|
const topicSelect = document.getElementById('filterTopic');
|
||||||
@@ -91,7 +83,6 @@ function updateDependentFilters() {
|
|||||||
populateSelect('filterTeacher', availableTeachers, 'Все учителя');
|
populateSelect('filterTeacher', availableTeachers, 'Все учителя');
|
||||||
populateSelect('filterTopic', availableTopics, 'Все темы');
|
populateSelect('filterTopic', availableTopics, 'Все темы');
|
||||||
|
|
||||||
// Если старое значение не было сброшено populateSelect, восстанавливаем
|
|
||||||
if (oldClass && availableClasses.includes(oldClass)) classSelect.value = oldClass;
|
if (oldClass && availableClasses.includes(oldClass)) classSelect.value = oldClass;
|
||||||
else classSelect.value = '';
|
else classSelect.value = '';
|
||||||
if (oldTeacher && availableTeachers.includes(oldTeacher)) teacherSelect.value = oldTeacher;
|
if (oldTeacher && availableTeachers.includes(oldTeacher)) teacherSelect.value = oldTeacher;
|
||||||
@@ -100,16 +91,12 @@ function updateDependentFilters() {
|
|||||||
else topicSelect.value = '';
|
else topicSelect.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Применение фильтров + скрытие уроков без свободных мест
|
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
const classFilter = document.getElementById('filterClass').value;
|
const classFilter = document.getElementById('filterClass').value;
|
||||||
const teacherFilter = document.getElementById('filterTeacher').value;
|
const teacherFilter = document.getElementById('filterTeacher').value;
|
||||||
const topicFilter = document.getElementById('filterTopic').value;
|
const topicFilter = document.getElementById('filterTopic').value;
|
||||||
|
|
||||||
// Сначала отбираем только доступные уроки (есть свободные места)
|
|
||||||
let filtered = allLessons.filter(lesson => lesson.available === true);
|
let filtered = allLessons.filter(lesson => lesson.available === true);
|
||||||
|
|
||||||
// Точное совпадение для select (не частичное)
|
|
||||||
if (classFilter) filtered = filtered.filter(lesson => lesson.class_name === classFilter);
|
if (classFilter) filtered = filtered.filter(lesson => lesson.class_name === classFilter);
|
||||||
if (teacherFilter) filtered = filtered.filter(lesson => lesson.teacher === teacherFilter);
|
if (teacherFilter) filtered = filtered.filter(lesson => lesson.teacher === teacherFilter);
|
||||||
if (topicFilter) filtered = filtered.filter(lesson => lesson.topic === topicFilter);
|
if (topicFilter) filtered = filtered.filter(lesson => lesson.topic === topicFilter);
|
||||||
@@ -117,7 +104,6 @@ function applyFilters() {
|
|||||||
renderLessons(filtered);
|
renderLessons(filtered);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Отрисовка карточек уроков + обновление счётчика
|
|
||||||
function renderLessons(lessons) {
|
function renderLessons(lessons) {
|
||||||
const container = document.getElementById('lessonsContainer');
|
const container = document.getElementById('lessonsContainer');
|
||||||
const counterSpan = document.getElementById('availableCount');
|
const counterSpan = document.getElementById('availableCount');
|
||||||
@@ -129,16 +115,27 @@ function renderLessons(lessons) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.innerHTML = lessons.map(lesson => `
|
container.innerHTML = lessons.map(lesson => {
|
||||||
<div class="lesson-card" data-id="${lesson.id}">
|
// Определяем, что показывать в строке времени
|
||||||
<h3>${escapeHtml(lesson.class_name)} | ${escapeHtml(lesson.subject)}</h3>
|
let timeHtml = '';
|
||||||
<p><strong>Учитель:</strong> ${escapeHtml(lesson.teacher)}</p>
|
if (lesson.topic === 'Консультация' && lesson.date && lesson.time) {
|
||||||
<p><strong>Тема:</strong> ${escapeHtml(lesson.topic || '—')}</p>
|
timeHtml = `<p><strong>Дата/время:</strong> ${escapeHtml(lesson.date)} ${escapeHtml(lesson.time)}</p>`;
|
||||||
<div class="slots">
|
} else {
|
||||||
Свободных мест: ${lesson.max_slots - lesson.current_slots} из ${lesson.max_slots}
|
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>
|
||||||
</div>
|
`;
|
||||||
`).join('');
|
}).join('');
|
||||||
|
|
||||||
if (counterSpan) counterSpan.textContent = `${lessons.length}`;
|
if (counterSpan) counterSpan.textContent = `${lessons.length}`;
|
||||||
|
|
||||||
@@ -147,7 +144,6 @@ function renderLessons(lessons) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Модальное окно записи (без изменений)
|
|
||||||
function setupModal() {
|
function setupModal() {
|
||||||
const modal = document.getElementById('modal');
|
const modal = document.getElementById('modal');
|
||||||
const closeSpan = modal.querySelector('.close');
|
const closeSpan = modal.querySelector('.close');
|
||||||
@@ -195,11 +191,18 @@ function openModal(lessonId) {
|
|||||||
const lesson = allLessons.find(l => l.id == lessonId);
|
const lesson = allLessons.find(l => l.id == lessonId);
|
||||||
if (!lesson) return;
|
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('lessonId').value = lessonId;
|
||||||
document.getElementById('modalLessonInfo').innerHTML = `
|
document.getElementById('modalLessonInfo').innerHTML = `
|
||||||
<strong>${escapeHtml(lesson.class_name)}</strong><br>
|
<strong>${escapeHtml(lesson.class_name)}</strong><br>
|
||||||
${escapeHtml(lesson.subject)} — ${escapeHtml(lesson.teacher)}<br>
|
${escapeHtml(lesson.subject)} — ${escapeHtml(lesson.teacher)}<br>
|
||||||
<small>${lesson.date} ${lesson.time}</small>
|
<small>${timeInfo}</small>
|
||||||
`;
|
`;
|
||||||
document.getElementById('modalMessage').innerHTML = '';
|
document.getElementById('modalMessage').innerHTML = '';
|
||||||
document.getElementById('registrationForm').reset();
|
document.getElementById('registrationForm').reset();
|
||||||
@@ -216,13 +219,11 @@ function escapeHtml(str) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Инициализация
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await loadFilterOptions();
|
await loadFilterOptions();
|
||||||
await loadLessons();
|
await loadLessons();
|
||||||
setupModal();
|
setupModal();
|
||||||
|
|
||||||
// Обработчики изменений фильтров
|
|
||||||
const classSelect = document.getElementById('filterClass');
|
const classSelect = document.getElementById('filterClass');
|
||||||
const teacherSelect = document.getElementById('filterTeacher');
|
const teacherSelect = document.getElementById('filterTeacher');
|
||||||
const topicSelect = document.getElementById('filterTopic');
|
const topicSelect = document.getElementById('filterTopic');
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ form input, form textarea {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.3rem;
|
gap: 0.3rem;
|
||||||
min-width: 150px;
|
min-width: 100px;
|
||||||
max-width: 450px;
|
max-width: 450px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user