x
This commit is contained in:
3100
package-lock.json
generated
Normal file
3100
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,7 +43,7 @@
|
|||||||
<!-- Импорт JSON (без даты/времени) -->
|
<!-- Импорт JSON (без даты/времени) -->
|
||||||
<div class="import-section">
|
<div class="import-section">
|
||||||
<h3>Импорт уроков из JSON</h3>
|
<h3>Импорт уроков из JSON</h3>
|
||||||
<input type="file" id="jsonFileInput" accept=".json">
|
<input type="file" id="jsonFileInput" accept=".json, .xlsx, .xls">
|
||||||
<div class="import-params">
|
<div class="import-params">
|
||||||
<label>Макс. мест (сколько родителей может записаться):
|
<label>Макс. мест (сколько родителей может записаться):
|
||||||
<input type="number" id="importMaxSlots" value="4" required>
|
<input type="number" id="importMaxSlots" value="4" required>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
let currentUser = null;
|
let currentUser = null;
|
||||||
let currentPreviewLessons = [];
|
let currentPreviewLessons = [];
|
||||||
let allLessonsForFilters = []; // храним все уроки для построения зависимых фильтров
|
let allLessonsForFilters = [];
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
await checkAuth();
|
await checkAuth();
|
||||||
@@ -26,7 +26,6 @@ async function checkAuth() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Загрузка опций для выпадающих списков фильтров (начальные)
|
|
||||||
async function loadFilterOptions() {
|
async function loadFilterOptions() {
|
||||||
try {
|
try {
|
||||||
const [classes, teachers, topics] = await Promise.all([
|
const [classes, teachers, topics] = await Promise.all([
|
||||||
@@ -64,7 +63,6 @@ function populateSelect(selectId, options, defaultLabel) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обновление зависимых фильтров на основе текущих значений и allLessonsForFilters
|
|
||||||
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;
|
||||||
@@ -105,9 +103,8 @@ async function loadLessons(filters = {}) {
|
|||||||
const params = new URLSearchParams(filters);
|
const params = new URLSearchParams(filters);
|
||||||
const res = await fetch(`/api/admin/lessons?${params}`);
|
const res = await fetch(`/api/admin/lessons?${params}`);
|
||||||
const lessons = await res.json();
|
const lessons = await res.json();
|
||||||
// Сохраняем полный список для фильтрации (без учёта фильтров)
|
|
||||||
allLessonsForFilters = lessons;
|
allLessonsForFilters = lessons;
|
||||||
updateDependentFilters(); // обновить списки в select на основе всех уроков
|
updateDependentFilters();
|
||||||
|
|
||||||
const container = document.getElementById('lessonsList');
|
const container = document.getElementById('lessonsList');
|
||||||
if (!container) return;
|
if (!container) return;
|
||||||
@@ -228,7 +225,6 @@ function setupEventListeners() {
|
|||||||
const topicSelect = document.getElementById('filterTopic');
|
const topicSelect = document.getElementById('filterTopic');
|
||||||
|
|
||||||
function handleFilterChange() {
|
function handleFilterChange() {
|
||||||
// Обновить зависимые списки, затем применить фильтрацию списка уроков
|
|
||||||
updateDependentFilters();
|
updateDependentFilters();
|
||||||
loadLessons(getCurrentFilters());
|
loadLessons(getCurrentFilters());
|
||||||
}
|
}
|
||||||
@@ -256,19 +252,83 @@ function setupEventListeners() {
|
|||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Импорт JSON (без изменений)
|
// ========== ИМПОРТ JSON / XLSX ==========
|
||||||
|
function parseExcelToRecords(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
const data = new Uint8Array(e.target.result);
|
||||||
|
const workbook = XLSX.read(data, { type: 'array' });
|
||||||
|
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 () => {
|
document.getElementById('previewImportBtn')?.addEventListener('click', async () => {
|
||||||
const file = document.getElementById('jsonFileInput').files[0];
|
const file = document.getElementById('jsonFileInput').files[0];
|
||||||
if (!file) return alert('Выберите JSON-файл');
|
if (!file) return alert('Выберите JSON или Excel файл');
|
||||||
const importMaxSlots = document.getElementById('importMaxSlots').value;
|
const importMaxSlots = document.getElementById('importMaxSlots').value;
|
||||||
if (!importMaxSlots) return alert('Укажите максимальное количество мест');
|
if (!importMaxSlots) return alert('Укажите максимальное количество мест');
|
||||||
|
|
||||||
const text = await file.text();
|
let records = null;
|
||||||
|
const fileExt = file.name.split('.').pop().toLowerCase();
|
||||||
try {
|
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', {
|
const response = await fetch('/api/admin/import/preview', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ jsonData: JSON.parse(text) })
|
body: JSON.stringify({ jsonData: records })
|
||||||
});
|
});
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
@@ -293,7 +353,7 @@ function setupEventListeners() {
|
|||||||
alert('Ошибка предпросмотра: ' + (data.error || ''));
|
alert('Ошибка предпросмотра: ' + (data.error || ''));
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert('Ошибка при разборе JSON: ' + err.message);
|
alert('Ошибка при разборе файла: ' + err.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
32
server.js
32
server.js
@@ -237,12 +237,25 @@ app.get('/api/filter-options/subjects', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// --------------------- Импорт уроков из JSON (без даты/времени) ---------------------
|
// --------------------- Универсальный парсер для импорта (JSON / XLSX) ---------------------
|
||||||
function parseLessonFromJsonRecord(record) {
|
function parseLessonFromJsonRecord(record) {
|
||||||
const fields = {};
|
let fields = {};
|
||||||
for (const [key, value] of record) {
|
|
||||||
fields[key.trim()] = (value || '').trim();
|
if (Array.isArray(record)) {
|
||||||
|
// Массив пар [["key","value"], ...]
|
||||||
|
for (const [key, value] of record) {
|
||||||
|
fields[key.trim()] = (value || '').toString().trim();
|
||||||
|
}
|
||||||
|
} else if (typeof record === 'object' && record !== null) {
|
||||||
|
// Обычный объект
|
||||||
|
fields = { ...record };
|
||||||
|
Object.keys(fields).forEach(k => {
|
||||||
|
fields[k.trim()] = (fields[k] || '').toString().trim();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error('Неверный формат записи');
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastName = fields["Фамилия Учителя"] || "";
|
const lastName = fields["Фамилия Учителя"] || "";
|
||||||
const firstName = fields["Имя Учителя"] || "";
|
const firstName = fields["Имя Учителя"] || "";
|
||||||
const patronymic = fields["Отчество Учителя"] || "";
|
const patronymic = fields["Отчество Учителя"] || "";
|
||||||
@@ -253,6 +266,7 @@ function parseLessonFromJsonRecord(record) {
|
|||||||
const classLetter = fields["Класс (буква)"] || "";
|
const classLetter = fields["Класс (буква)"] || "";
|
||||||
const parallel = parseInt(parallelRaw, 10);
|
const parallel = parseInt(parallelRaw, 10);
|
||||||
const className = `${parallelRaw}${classLetter}`.trim();
|
const className = `${parallelRaw}${classLetter}`.trim();
|
||||||
|
|
||||||
return { teacher, subject, topic, parallel, className };
|
return { teacher, subject, topic, parallel, className };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,8 +278,12 @@ app.post('/api/admin/import/preview', isAuthenticated, isAdmin, async (req, res)
|
|||||||
|
|
||||||
const preview = [];
|
const preview = [];
|
||||||
for (const record of records) {
|
for (const record of records) {
|
||||||
if (!Array.isArray(record)) continue;
|
let lesson;
|
||||||
const lesson = parseLessonFromJsonRecord(record);
|
try {
|
||||||
|
lesson = parseLessonFromJsonRecord(record);
|
||||||
|
} catch(e) {
|
||||||
|
continue; // пропускаем битые записи
|
||||||
|
}
|
||||||
if (lesson.teacher && lesson.subject && !isNaN(lesson.parallel)) {
|
if (lesson.teacher && lesson.subject && !isNaN(lesson.parallel)) {
|
||||||
preview.push(lesson);
|
preview.push(lesson);
|
||||||
}
|
}
|
||||||
@@ -273,7 +291,7 @@ app.post('/api/admin/import/preview', isAuthenticated, isAdmin, async (req, res)
|
|||||||
res.json({ success: true, preview });
|
res.json({ success: true, preview });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
res.status(500).json({ error: 'Ошибка разбора JSON' });
|
res.status(500).json({ error: 'Ошибка разбора JSON/Excel' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user