From 96230759b7de93f3b65d293fbb5954b06350a8c4 Mon Sep 17 00:00:00 2001 From: kalugin66 Date: Wed, 1 Jul 2026 13:16:07 +0500 Subject: [PATCH] v4 --- src/db.js | 14 +++++++++ src/routes/api.js | 52 +++++++++++++++++++++++++++++-- src/routes/pages.js | 42 ++++++++++++++++++++++++- views/history.ejs | 76 +++++++++++++++++++++++++++++++++++++++++++++ views/import.ejs | 1 + views/index.ejs | 1 + views/student.ejs | 35 +++++++++++++++++++++ 7 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 views/history.ejs diff --git a/src/db.js b/src/db.js index 55cfaf2..a8f3336 100644 --- a/src/db.js +++ b/src/db.js @@ -75,6 +75,20 @@ async function initDb() { ) `); + db.run(` + CREATE TABLE IF NOT EXISTS exam_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + student_id INTEGER NOT NULL, + subject_code TEXT NOT NULL, + field_name TEXT NOT NULL, + old_value TEXT, + new_value TEXT, + changed_by TEXT, + changed_at DATETIME DEFAULT CURRENT_TIMESTAMP, + source TEXT DEFAULT 'manual' + ) + `); + db.run('INSERT OR IGNORE INTO subjects (code, name, is_mandatory) VALUES (?, ?, 1)', ['01', 'Русский язык']); db.run('INSERT OR IGNORE INTO subjects (code, name, is_mandatory) VALUES (?, ?, 1)', ['02', 'Математика']); diff --git a/src/routes/api.js b/src/routes/api.js index d1d3cf8..8483728 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -60,6 +60,17 @@ router.post('/import', upload.single('xlsfile'), (req, res) => { if (!existing || existing.grade === null) code = '02'; } + const oldRow = dbGet("SELECT primary_score, grade FROM exam_results WHERE student_id = ? AND subject_code = ?", [sid, code]); + const changedBy = (req.session && req.session.user) ? req.session.user.login : 'import'; + + if (!oldRow) { + if (r.primary_score !== null) logHistory(sid, code, 'primary_score', null, String(r.primary_score), changedBy, 'import'); + if (r.grade !== null) logHistory(sid, code, 'grade', null, String(r.grade), changedBy, 'import'); + } else { + if (String(oldRow.primary_score) !== String(r.primary_score)) logHistory(sid, code, 'primary_score', oldRow.primary_score, r.primary_score, changedBy, 'import'); + if (String(oldRow.grade) !== String(r.grade)) logHistory(sid, code, 'grade', oldRow.grade, r.grade, changedBy, 'import'); + } + dbRun('INSERT OR REPLACE INTO exam_results (student_id, subject_code, primary_score, grade) VALUES (?, ?, ?, ?)', [sid, code, r.primary_score, r.grade]); } } @@ -154,19 +165,46 @@ router.get('/students/:id', (req, res) => { ORDER BY s.is_mandatory DESC, s.name `, [req.params.id]); - res.json({ student, results }); + const history = dbAll(` + SELECT h.*, s.name as subject_name + FROM exam_history h + LEFT JOIN subjects s ON s.code = h.subject_code + WHERE h.student_id = ? + ORDER BY h.changed_at DESC + LIMIT 50 + `, [req.params.id]); + + res.json({ student, results, history }); }); router.put('/students/:id/mandatory', (req, res) => { const { id } = req.params; const { rus_score, rus_grade, math_score, math_grade } = req.body; + const changedBy = (req.session && req.session.user) ? req.session.user.login : 'unknown'; dbBegin(); + + const updateSubject = (code, score, grade) => { + const oldRow = dbGet("SELECT primary_score, grade FROM exam_results WHERE student_id = ? AND subject_code = ?", [id, code]); + + if (!oldRow) { + if (score !== undefined && score !== null && score !== '') logHistory(id, code, 'primary_score', null, String(score), changedBy, 'manual'); + if (grade !== undefined && grade !== null && grade !== '') logHistory(id, code, 'grade', null, String(grade), changedBy, 'manual'); + } else { + const newScore = (score !== undefined && score !== '' && score !== null) ? score : null; + const newGrade = (grade !== undefined && grade !== '' && grade !== null) ? grade : null; + if (String(oldRow.primary_score) !== String(newScore)) logHistory(id, code, 'primary_score', oldRow.primary_score, newScore, changedBy, 'manual'); + if (String(oldRow.grade) !== String(newGrade)) logHistory(id, code, 'grade', oldRow.grade, newGrade, changedBy, 'manual'); + } + + dbRun('INSERT OR REPLACE INTO exam_results (student_id, subject_code, primary_score, grade) VALUES (?, ?, ?, ?)', [id, code, score || null, grade || null]); + }; + if (rus_score !== undefined || rus_grade !== undefined) { - dbRun('INSERT OR REPLACE INTO exam_results (student_id, subject_code, primary_score, grade) VALUES (?, ?, ?, ?)', [id, '01', rus_score || null, rus_grade || null]); + updateSubject('01', rus_score, rus_grade); } if (math_score !== undefined || math_grade !== undefined) { - dbRun('INSERT OR REPLACE INTO exam_results (student_id, subject_code, primary_score, grade) VALUES (?, ?, ?, ?)', [id, '02', math_score || null, math_grade || null]); + updateSubject('02', math_score, math_grade); } dbCommit(); saveDb(); @@ -187,4 +225,12 @@ router.delete('/data', (req, res) => { res.json({ ok: true }); }); +function logHistory(studentId, subjectCode, fieldName, oldValue, newValue, changedBy, source) { + if (String(oldValue) === String(newValue)) return; + dbRun( + 'INSERT INTO exam_history (student_id, subject_code, field_name, old_value, new_value, changed_by, source, changed_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime(\'now\'))', + [studentId, subjectCode, fieldName, oldValue !== null && oldValue !== undefined ? String(oldValue) : null, newValue !== null && newValue !== undefined ? String(newValue) : null, changedBy || 'unknown', source || 'manual'] + ); +} + module.exports = router; diff --git a/src/routes/pages.js b/src/routes/pages.js index f20f186..489567d 100644 --- a/src/routes/pages.js +++ b/src/routes/pages.js @@ -1,6 +1,5 @@ const express = require('express'); const { dbGet, dbAll } = require('../db'); - const router = express.Router(); const requireAuth = (req, res, next) => { @@ -34,11 +33,38 @@ router.get('/student/:id', requireAuth, (req, res) => { const rusResult = mandatoryResults.find(r => r.subject_code === '01') || null; const mathResult = mandatoryResults.find(r => r.subject_code === '02') || null; + const history = dbAll(` + SELECT h.*, s.name as subject_name + FROM exam_history h + LEFT JOIN subjects s ON s.code = h.subject_code + WHERE h.student_id = ? + ORDER BY h.changed_at DESC + LIMIT 50 + `, [req.params.id]); + + // Group history by timestamp+user+source + const groupedHistory = []; + let currentGroup = null; + for (const row of history) { + const key = `${row.changed_at}|${row.changed_by}|${row.source}`; + if (!currentGroup || currentGroup.key !== key) { + currentGroup = { key, changed_at: row.changed_at, changed_by: row.changed_by, source: row.source, changes: [] }; + groupedHistory.push(currentGroup); + } + currentGroup.changes.push({ + subject_name: row.subject_name || row.subject_code, + field_name: row.field_name === 'primary_score' ? 'первичный балл' : 'оценка', + old_value: row.old_value !== null ? row.old_value : '—', + new_value: row.new_value !== null ? row.new_value : '—' + }); + } + res.render('student', { student, electiveResults, rusResult, mathResult, + groupedHistory, user: req.session.user }); }); @@ -232,4 +258,18 @@ router.get('/batch-print', requireAuth, (req, res) => { res.render('batch-certificate', { students: studentData }); }); +router.get('/history', requireAuth, (req, res) => { + const rows = dbAll(` + SELECT h.*, s.name as subject_name, + st.last_name, st.first_name, st.patronymic, st.class, st.id as student_id + FROM exam_history h + LEFT JOIN subjects s ON s.code = h.subject_code + LEFT JOIN students st ON st.id = h.student_id + ORDER BY h.changed_at DESC + LIMIT 200 + `); + + res.render('history', { rows, user: req.session.user }); +}); + module.exports = router; diff --git a/views/history.ejs b/views/history.ejs new file mode 100644 index 0000000..e89346a --- /dev/null +++ b/views/history.ejs @@ -0,0 +1,76 @@ + + + + + + История изменений — Справки + + + +
+ + +

История изменений

+ + + + + + + + + + + + + + + + + <% rows.forEach(r => { %> + + + + + + + + + + + + <% }) %> + <% if (rows.length === 0) { %> + + <% } %> + +
ДатаУченикКлассПредметПолеБылоСталоПользовательИсточник
<%= r.changed_at %><%= r.last_name %> <%= r.first_name %> <%= r.patronymic %><%= r.class %><%= r.subject_name || r.subject_code %><%= r.field_name === 'primary_score' ? 'балл' : 'оценка' %><%= r.old_value !== null ? r.old_value : '—' %><%= r.new_value !== null ? r.new_value : '—' %><%= r.changed_by || '—' %> + <% if (r.source === 'import') { %>импорт + <% } else if (r.source === 'manual') { %>вручную + <% } else { %><%= r.source %><% } %> +
Нет записей
+
+ + + + diff --git a/views/import.ejs b/views/import.ejs index 5fb560d..ed29bb9 100644 --- a/views/import.ejs +++ b/views/import.ejs @@ -10,6 +10,7 @@
diff --git a/views/index.ejs b/views/index.ejs index ad2ede1..8d53fa8 100644 --- a/views/index.ejs +++ b/views/index.ejs @@ -13,6 +13,7 @@ diff --git a/views/student.ejs b/views/student.ejs index 28c8ac2..38336a7 100644 --- a/views/student.ejs +++ b/views/student.ejs @@ -10,6 +10,7 @@
@@ -100,6 +101,40 @@
+ + <% if (groupedHistory && groupedHistory.length > 0) { %> +
+

История изменений

+ + + + + + + + + + + <% groupedHistory.forEach(g => { %> + + + + + + + <% }) %> + +
ДатаПользовательИсточникИзменения
<%= g.changed_at %><%= g.changed_by %> + <% if (g.source === 'import') { %>XLS импорт + <% } else if (g.source === 'manual') { %>вручную + <% } else { %><%= g.source %><% } %> + + <% g.changes.forEach((c, ci) => { %> +
<%= c.subject_name %>: <%= c.field_name %> <%= c.old_value %><%= c.new_value %>
+ <% }) %> +
+
+ <% } %>