v5
This commit is contained in:
12
src/db.js
12
src/db.js
@@ -92,6 +92,18 @@ async function initDb() {
|
|||||||
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)', ['01', 'Русский язык']);
|
||||||
db.run('INSERT OR IGNORE INTO subjects (code, name, is_mandatory) VALUES (?, ?, 1)', ['02', 'Математика']);
|
db.run('INSERT OR IGNORE INTO subjects (code, name, is_mandatory) VALUES (?, ?, 1)', ['02', 'Математика']);
|
||||||
|
|
||||||
|
// Миграция: добавить cert_number
|
||||||
|
try { db.run('ALTER TABLE students ADD COLUMN cert_number TEXT'); } catch (e) {}
|
||||||
|
try { db.run('CREATE UNIQUE INDEX IF NOT EXISTS idx_students_cert_number ON students(cert_number)'); } catch (e) {}
|
||||||
|
// Заполнить пустые номера
|
||||||
|
const year = new Date().getFullYear();
|
||||||
|
const rows = dbAll("SELECT id, cert_number FROM students WHERE cert_number IS NULL OR cert_number = ''");
|
||||||
|
for (const r of rows) {
|
||||||
|
const num = String(year) + String(r.id).padStart(4, '0');
|
||||||
|
dbRun("UPDATE students SET cert_number = ? WHERE id = ?", [num, r.id]);
|
||||||
|
}
|
||||||
|
if (rows.length > 0) console.log(`Миграция cert_number: заполнено ${rows.length} учеников`);
|
||||||
|
|
||||||
saveDb();
|
saveDb();
|
||||||
|
|
||||||
// Миграция: перенос Русского/Математики в обязательные слоты 01/02, если они пусты
|
// Миграция: перенос Русского/Математики в обязательные слоты 01/02, если они пусты
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ router.post('/import', upload.single('xlsfile'), (req, res) => {
|
|||||||
const key = `${st.last_name}|${st.first_name}|${st.patronymic}|${st.class}`.toLowerCase();
|
const key = `${st.last_name}|${st.first_name}|${st.patronymic}|${st.class}`.toLowerCase();
|
||||||
const before = dbRun('INSERT OR IGNORE INTO students (last_name, first_name, patronymic, class, school_code, passport_series, passport_number) VALUES (?, ?, ?, ?, ?, ?, ?)', [st.last_name, st.first_name, st.patronymic, st.class, st.school_code, st.passport_series, st.passport_number]);
|
const before = dbRun('INSERT OR IGNORE INTO students (last_name, first_name, patronymic, class, school_code, passport_series, passport_number) VALUES (?, ?, ?, ?, ?, ?, ?)', [st.last_name, st.first_name, st.patronymic, st.class, st.school_code, st.passport_series, st.passport_number]);
|
||||||
if (before.changes > 0) {
|
if (before.changes > 0) {
|
||||||
studentIdByKey[key] = dbLastInsertId();
|
const newId = dbLastInsertId();
|
||||||
|
studentIdByKey[key] = newId;
|
||||||
|
// Авто-присвоить номер справки
|
||||||
|
const certNumber = String(new Date().getFullYear()) + String(newId).padStart(4, '0');
|
||||||
|
dbRun('UPDATE students SET cert_number = ? WHERE id = ? AND cert_number IS NULL', [certNumber, newId]);
|
||||||
} else {
|
} else {
|
||||||
const row = dbGet('SELECT id FROM students WHERE last_name=? AND first_name=? AND patronymic=? AND class=?', [st.last_name, st.first_name, st.patronymic, st.class]);
|
const row = dbGet('SELECT id FROM students WHERE last_name=? AND first_name=? AND patronymic=? AND class=?', [st.last_name, st.first_name, st.patronymic, st.class]);
|
||||||
if (row) {
|
if (row) {
|
||||||
@@ -212,6 +216,31 @@ router.put('/students/:id/mandatory', (req, res) => {
|
|||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.put('/students/:id/cert-number', (req, res) => {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { cert_number } = req.body;
|
||||||
|
if (!cert_number) return res.status(400).json({ error: 'Номер справки обязателен' });
|
||||||
|
|
||||||
|
// Проверить уникальность
|
||||||
|
const existing = dbGet('SELECT id, last_name, first_name, class FROM students WHERE cert_number = ? AND id != ?', [cert_number, id]);
|
||||||
|
if (existing) {
|
||||||
|
return res.status(409).json({ error: `Номер ${cert_number} уже занят: ${existing.last_name} ${existing.first_name}, ${existing.class}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldRow = dbGet('SELECT cert_number FROM students WHERE id = ?', [id]);
|
||||||
|
const oldValue = oldRow ? oldRow.cert_number : null;
|
||||||
|
dbRun('UPDATE students SET cert_number = ? WHERE id = ?', [cert_number, id]);
|
||||||
|
|
||||||
|
// История
|
||||||
|
const changedBy = req.session.user ? req.session.user.login : 'unknown';
|
||||||
|
if (String(oldValue) !== String(cert_number)) {
|
||||||
|
logHistory(id, 'CERT', 'cert_number', oldValue, cert_number, changedBy, 'manual');
|
||||||
|
}
|
||||||
|
|
||||||
|
saveDb();
|
||||||
|
res.json({ ok: true });
|
||||||
|
});
|
||||||
|
|
||||||
router.get('/classes', (req, res) => {
|
router.get('/classes', (req, res) => {
|
||||||
const rows = dbAll('SELECT DISTINCT class FROM students ORDER BY class');
|
const rows = dbAll('SELECT DISTINCT class FROM students ORDER BY class');
|
||||||
res.json(rows.map(r => r.class));
|
res.json(rows.map(r => r.class));
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ router.get('/student/:id', requireAuth, (req, res) => {
|
|||||||
groupedHistory.push(currentGroup);
|
groupedHistory.push(currentGroup);
|
||||||
}
|
}
|
||||||
currentGroup.changes.push({
|
currentGroup.changes.push({
|
||||||
subject_name: row.subject_name || row.subject_code,
|
subject_name: row.subject_code === 'CERT' ? 'Номер справки' : (row.subject_name || row.subject_code),
|
||||||
field_name: row.field_name === 'primary_score' ? 'первичный балл' : 'оценка',
|
field_name: row.field_name === 'primary_score' ? 'первичный балл' : row.field_name === 'grade' ? 'оценка' : 'номер',
|
||||||
old_value: row.old_value !== null ? row.old_value : '—',
|
old_value: row.old_value !== null ? row.old_value : '—',
|
||||||
new_value: row.new_value !== null ? row.new_value : '—'
|
new_value: row.new_value !== null ? row.new_value : '—'
|
||||||
});
|
});
|
||||||
@@ -104,10 +104,17 @@ router.get('/certificate/:id', requireAuth, (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
const certNumber = `${today.getFullYear()}${String(id).padStart(4, '0')}`;
|
const certNumber = student.cert_number || `${today.getFullYear()}${String(id).padStart(4, '0')}`;
|
||||||
const months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
|
||||||
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'];
|
const reqDate = req.query.date;
|
||||||
const dateStr = `«${String(today.getDate()).padStart(2, '0')}» ${months[today.getMonth()]} ${today.getFullYear()} года`;
|
let dateStr;
|
||||||
|
if (reqDate) {
|
||||||
|
const [y, m, d] = reqDate.split('-');
|
||||||
|
dateStr = `«${d.replace(/^0/, '')}» ${months[parseInt(m) - 1]} ${y} года`;
|
||||||
|
} else {
|
||||||
|
const today = new Date();
|
||||||
|
dateStr = `«${String(today.getDate()).padStart(2, '0')}» ${months[today.getMonth()]} ${today.getFullYear()} года`;
|
||||||
|
}
|
||||||
|
|
||||||
const gender = detectGender(student.patronymic);
|
const gender = detectGender(student.patronymic);
|
||||||
const fioDative = formatNameDative(student, gender);
|
const fioDative = formatNameDative(student, gender);
|
||||||
@@ -209,7 +216,15 @@ router.get('/batch-print', requireAuth, (req, res) => {
|
|||||||
const today = new Date();
|
const today = new Date();
|
||||||
const months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
const months = ['января', 'февраля', 'марта', 'апреля', 'мая', 'июня',
|
||||||
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'];
|
'июля', 'августа', 'сентября', 'октября', 'ноября', 'декабря'];
|
||||||
const dateStr = `«${String(today.getDate()).padStart(2, '0')}» ${months[today.getMonth()]} ${today.getFullYear()} года`;
|
|
||||||
|
const reqDate = req.query.date;
|
||||||
|
let dateStr;
|
||||||
|
if (reqDate) {
|
||||||
|
const [y, m, d] = reqDate.split('-');
|
||||||
|
dateStr = `«${d.replace(/^0/, '')}» ${months[parseInt(m) - 1]} ${y} года`;
|
||||||
|
} else {
|
||||||
|
dateStr = `«${String(today.getDate()).padStart(2, '0')}» ${months[today.getMonth()]} ${today.getFullYear()} года`;
|
||||||
|
}
|
||||||
|
|
||||||
const studentData = students.map(st => {
|
const studentData = students.map(st => {
|
||||||
const results = dbAll(`
|
const results = dbAll(`
|
||||||
@@ -246,7 +261,7 @@ router.get('/batch-print', requireAuth, (req, res) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
student: { ...st, fioDative, gender },
|
student: { ...st, fioDative, gender },
|
||||||
certNumber: `${today.getFullYear()}${String(st.id).padStart(4, '0')}`,
|
certNumber: st.cert_number || `${today.getFullYear()}${String(st.id).padStart(4, '0')}`,
|
||||||
dateStr,
|
dateStr,
|
||||||
verbEnding: gender === 'm' ? 'обучался' : 'обучалась',
|
verbEnding: gender === 'm' ? 'обучался' : 'обучалась',
|
||||||
verbEnding2: gender === 'm' ? 'получил' : 'получила',
|
verbEnding2: gender === 'm' ? 'получил' : 'получила',
|
||||||
|
|||||||
@@ -21,14 +21,15 @@
|
|||||||
}
|
}
|
||||||
.cert-page:last-child { page-break-after: auto; }
|
.cert-page:last-child { page-break-after: auto; }
|
||||||
.header { text-align: center; margin-bottom: 6mm; }
|
.header { text-align: center; margin-bottom: 6mm; }
|
||||||
.header .dept { font-size: 10pt; font-weight: bold; }
|
.header .dept { font-size: 9pt; font-weight: bold; }
|
||||||
.header .school {
|
.header .school {
|
||||||
font-size: 11pt;
|
font-size: 10pt;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-top: 3mm;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.header .address { font-size: 10pt; margin-top: 1mm; }
|
.header .school-first { white-space: nowrap; }
|
||||||
|
.header .address { font-size: 9pt; margin-top: 1mm; }
|
||||||
.title {
|
.title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -54,11 +55,13 @@
|
|||||||
table td:first-child { width: 8mm; }
|
table td:first-child { width: 8mm; }
|
||||||
table td:nth-child(2) { text-align: left; }
|
table td:nth-child(2) { text-align: left; }
|
||||||
.signature {
|
.signature {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
margin-top: 10mm;
|
margin-top: 10mm;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
.signature .dir { margin-bottom: 6mm; }
|
.signature .dir { white-space: nowrap; }
|
||||||
.signature .name { font-weight: bold; }
|
.signature .line { flex: 1; border-bottom: 1px solid #000; margin: 0 12px; }
|
||||||
|
.signature .name { font-weight: bold; white-space: nowrap; }
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||||
@@ -94,8 +97,10 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="dept">ДЕПАРТАМЕНТ ОБРАЗОВАНИЯ АДМИНИСТРАЦИИ ГОРОДА ЕКАТЕРИНБУРГА</div>
|
<div class="dept">ДЕПАРТАМЕНТ ОБРАЗОВАНИЯ АДМИНИСТРАЦИИ ГОРОДА ЕКАТЕРИНБУРГА</div>
|
||||||
<div class="dept">УПРАВЛЕНИЕ ОБРАЗОВАНИЯ АКАДЕМИЧЕСКОГО РАЙОНА</div>
|
<div class="dept">УПРАВЛЕНИЕ ОБРАЗОВАНИЯ АКАДЕМИЧЕСКОГО РАЙОНА</div>
|
||||||
|
<div class="school school-first">
|
||||||
|
МУНИЦИПАЛЬНОЕ АВТОНОМНОЕ ОБЩЕОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ
|
||||||
|
</div>
|
||||||
<div class="school">
|
<div class="school">
|
||||||
МУНИЦИПАЛЬНОЕ АВТОНОМНОЕ ОБЩЕОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ —<br>
|
|
||||||
СРЕДНЯЯ ОБЩЕОБРАЗОВАТЕЛЬНАЯ ШКОЛА № 25 им. В.Г. ФЕОФАНОВА
|
СРЕДНЯЯ ОБЩЕОБРАЗОВАТЕЛЬНАЯ ШКОЛА № 25 им. В.Г. ФЕОФАНОВА
|
||||||
</div>
|
</div>
|
||||||
<div class="address">ул. Цветоносная, 2, тел. 334-32-26</div>
|
<div class="address">ул. Цветоносная, 2, тел. 334-32-26</div>
|
||||||
@@ -152,8 +157,9 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="signature">
|
<div class="signature">
|
||||||
<div class="dir">Директор</div>
|
<span class="dir">Директор</span>
|
||||||
<div class="name">О.А. Добычина</div>
|
<span class="line"></span>
|
||||||
|
<span class="name">О.А. Добычина</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
|
|||||||
@@ -15,14 +15,15 @@
|
|||||||
padding: 15mm 20mm;
|
padding: 15mm 20mm;
|
||||||
}
|
}
|
||||||
.header { text-align: center; margin-bottom: 10mm; }
|
.header { text-align: center; margin-bottom: 10mm; }
|
||||||
.header .dept { font-size: 11pt; font-weight: bold; }
|
.header .dept { font-size: 10pt; font-weight: bold; }
|
||||||
.header .school {
|
.header .school {
|
||||||
font-size: 12pt;
|
font-size: 11pt;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-top: 4mm;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.header .address { font-size: 11pt; margin-top: 2mm; }
|
.header .school-first { white-space: nowrap; }
|
||||||
|
.header .address { font-size: 10pt; margin-top: 2mm; }
|
||||||
.title {
|
.title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@@ -48,11 +49,13 @@
|
|||||||
table td:first-child { width: 10mm; }
|
table td:first-child { width: 10mm; }
|
||||||
table td:nth-child(2) { text-align: left; }
|
table td:nth-child(2) { text-align: left; }
|
||||||
.signature {
|
.signature {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
margin-top: 15mm;
|
margin-top: 15mm;
|
||||||
text-align: right;
|
|
||||||
}
|
}
|
||||||
.signature .dir { margin-bottom: 8mm; }
|
.signature .dir { white-space: nowrap; }
|
||||||
.signature .name { font-weight: bold; }
|
.signature .line { flex: 1; border-bottom: 1px solid #000; margin: 0 15px; }
|
||||||
|
.signature .name { font-weight: bold; white-space: nowrap; }
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
body { padding: 10mm 15mm; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
body { padding: 10mm 15mm; -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||||
@@ -84,8 +87,10 @@
|
|||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="dept">ДЕПАРТАМЕНТ ОБРАЗОВАНИЯ АДМИНИСТРАЦИИ ГОРОДА ЕКАТЕРИНБУРГА</div>
|
<div class="dept">ДЕПАРТАМЕНТ ОБРАЗОВАНИЯ АДМИНИСТРАЦИИ ГОРОДА ЕКАТЕРИНБУРГА</div>
|
||||||
<div class="dept">УПРАВЛЕНИЕ ОБРАЗОВАНИЯ АКАДЕМИЧЕСКОГО РАЙОНА</div>
|
<div class="dept">УПРАВЛЕНИЕ ОБРАЗОВАНИЯ АКАДЕМИЧЕСКОГО РАЙОНА</div>
|
||||||
|
<div class="school school-first">
|
||||||
|
МУНИЦИПАЛЬНОЕ АВТОНОМНОЕ ОБЩЕОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ
|
||||||
|
</div>
|
||||||
<div class="school">
|
<div class="school">
|
||||||
МУНИЦИПАЛЬНОЕ АВТОНОМНОЕ ОБЩЕОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ —<br>
|
|
||||||
СРЕДНЯЯ ОБЩЕОБРАЗОВАТЕЛЬНАЯ ШКОЛА № 25 им. В.Г. ФЕОФАНОВА
|
СРЕДНЯЯ ОБЩЕОБРАЗОВАТЕЛЬНАЯ ШКОЛА № 25 им. В.Г. ФЕОФАНОВА
|
||||||
</div>
|
</div>
|
||||||
<div class="address">ул. Цветоносная, 2, тел. 334-32-26</div>
|
<div class="address">ул. Цветоносная, 2, тел. 334-32-26</div>
|
||||||
@@ -142,8 +147,9 @@
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="signature">
|
<div class="signature">
|
||||||
<div class="dir">Директор</div>
|
<span class="dir">Директор</span>
|
||||||
<div class="name">О.А. Добычина</div>
|
<span class="line"></span>
|
||||||
|
<span class="name">О.А. Добычина</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<%
|
<%
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
<td style="font-size:12px;white-space:nowrap"><%= r.changed_at %></td>
|
<td style="font-size:12px;white-space:nowrap"><%= r.changed_at %></td>
|
||||||
<td><a href="/student/<%= r.student_id %>"><%= r.last_name %> <%= r.first_name %> <%= r.patronymic %></a></td>
|
<td><a href="/student/<%= r.student_id %>"><%= r.last_name %> <%= r.first_name %> <%= r.patronymic %></a></td>
|
||||||
<td><%= r.class %></td>
|
<td><%= r.class %></td>
|
||||||
<td><%= r.subject_name || r.subject_code %></td>
|
<td><%= r.subject_code === 'CERT' ? 'Номер справки' : (r.subject_name || r.subject_code) %></td>
|
||||||
<td><%= r.field_name === 'primary_score' ? 'балл' : 'оценка' %></td>
|
<td><%= r.field_name === 'primary_score' ? 'балл' : r.field_name === 'grade' ? 'оценка' : r.field_name === 'cert_number' ? 'номер' : r.field_name %></td>
|
||||||
<td style="color:#999"><%= r.old_value !== null ? r.old_value : '—' %></td>
|
<td style="color:#999"><%= r.old_value !== null ? r.old_value : '—' %></td>
|
||||||
<td><strong><%= r.new_value !== null ? r.new_value : '—' %></strong></td>
|
<td><strong><%= r.new_value !== null ? r.new_value : '—' %></strong></td>
|
||||||
<td style="font-size:12px"><%= r.changed_by || '—' %></td>
|
<td style="font-size:12px"><%= r.changed_by || '—' %></td>
|
||||||
|
|||||||
@@ -40,6 +40,8 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row" style="margin-top:8px">
|
<div class="form-row" style="margin-top:8px">
|
||||||
|
<label>Дата выдачи:</label>
|
||||||
|
<input type="date" id="batch-date" class="input input-sm">
|
||||||
<button id="batch-print-btn" class="btn btn-blue" style="margin-left:15px">Печать справок</button>
|
<button id="batch-print-btn" class="btn btn-blue" style="margin-left:15px">Печать справок</button>
|
||||||
<span style="font-size:12px; color:#888; margin-left:10px">тип определяется автоматически</span>
|
<span style="font-size:12px; color:#888; margin-left:10px">тип определяется автоматически</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -206,14 +208,20 @@
|
|||||||
document.getElementById('batch-print-btn').addEventListener('click', () => {
|
document.getElementById('batch-print-btn').addEventListener('click', () => {
|
||||||
const scope = document.querySelector('input[name="batch-scope"]:checked').value;
|
const scope = document.querySelector('input[name="batch-scope"]:checked').value;
|
||||||
let url = '/batch-print';
|
let url = '/batch-print';
|
||||||
|
const sep = url.includes('?') ? '&' : '?';
|
||||||
if (scope === 'class') {
|
if (scope === 'class') {
|
||||||
const cls = document.getElementById('class-select').value;
|
const cls = document.getElementById('class-select').value;
|
||||||
if (!cls) return alert('Выберите класс');
|
if (!cls) return alert('Выберите класс');
|
||||||
url += `?class=${encodeURIComponent(cls)}`;
|
url += `${sep}class=${encodeURIComponent(cls)}`;
|
||||||
} else if (scope === 'parallel') {
|
} else if (scope === 'parallel') {
|
||||||
const par = document.getElementById('parallel-select').value;
|
const par = document.getElementById('parallel-select').value;
|
||||||
if (!par) return alert('Выберите параллель');
|
if (!par) return alert('Выберите параллель');
|
||||||
url += `?parallel=${encodeURIComponent(par)}`;
|
url += `${sep}parallel=${encodeURIComponent(par)}`;
|
||||||
|
}
|
||||||
|
const date = document.getElementById('batch-date').value;
|
||||||
|
if (date) {
|
||||||
|
const sep2 = url.includes('?') ? '&' : '?';
|
||||||
|
url += `${sep2}date=${date}`;
|
||||||
}
|
}
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
});
|
});
|
||||||
@@ -223,6 +231,8 @@
|
|||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('batch-date').value = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
loadStudents();
|
loadStudents();
|
||||||
loadClasses();
|
loadClasses();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,6 +18,15 @@
|
|||||||
<h1><%= student.last_name %> <%= student.first_name %> <%= student.patronymic %></h1>
|
<h1><%= student.last_name %> <%= student.first_name %> <%= student.patronymic %></h1>
|
||||||
<p>Класс: <strong><%= student.class %></strong></p>
|
<p>Класс: <strong><%= student.class %></strong></p>
|
||||||
|
|
||||||
|
<div class="card" style="padding:10px 20px">
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Номер справки:</label>
|
||||||
|
<input type="text" id="cert-number" class="input input-sm" value="<%= student.cert_number || '' %>">
|
||||||
|
<button id="save-cert-number" class="btn btn-sm">Сохранить номер</button>
|
||||||
|
<span id="cert-msg" class="msg"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% const hasRus = rusResult && rusResult.grade !== null; const hasMath = mathResult && mathResult.grade !== null; %>
|
<% const hasRus = rusResult && rusResult.grade !== null; const hasMath = mathResult && mathResult.grade !== null; %>
|
||||||
<% if (!hasRus || !hasMath) { %>
|
<% if (!hasRus || !hasMath) { %>
|
||||||
<div class="banner-warning">
|
<div class="banner-warning">
|
||||||
@@ -98,6 +107,11 @@
|
|||||||
<span id="elective-msg" class="msg"></span>
|
<span id="elective-msg" class="msg"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row" style="margin-top:10px">
|
||||||
|
<label>Дата выдачи:</label>
|
||||||
|
<input type="date" id="cert-date" class="input input-sm">
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn btn-blue" style="margin-top:15px">Сформировать и распечатать</button>
|
<button type="submit" class="btn btn-blue" style="margin-top:15px">Сформировать и распечатать</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@@ -126,7 +140,9 @@
|
|||||||
</td>
|
</td>
|
||||||
<td style="font-size:12px">
|
<td style="font-size:12px">
|
||||||
<% g.changes.forEach((c, ci) => { %>
|
<% g.changes.forEach((c, ci) => { %>
|
||||||
<div><%= c.subject_name %>: <%= c.field_name %> <span style="color:#999"><%= c.old_value %></span> → <strong><%= c.new_value %></strong></div>
|
<div>
|
||||||
|
<% const subjName = c.subject_name === 'CERT' ? 'Номер справки' : c.subject_name; %>
|
||||||
|
<%= subjName %>: <%= c.field_name %> <span style="color:#999"><%= c.old_value %></span> → <strong><%= c.new_value %></strong></div>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -156,6 +172,24 @@
|
|||||||
window.location.href = '/login';
|
window.location.href = '/login';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set default date to today
|
||||||
|
document.getElementById('cert-date').value = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
// Save cert number
|
||||||
|
document.getElementById('save-cert-number').addEventListener('click', async () => {
|
||||||
|
const num = document.getElementById('cert-number').value.trim();
|
||||||
|
const msg = document.getElementById('cert-msg');
|
||||||
|
if (!num) { msg.textContent = 'Введите номер'; msg.className = 'msg msg-err'; return; }
|
||||||
|
const res = await fetch('/api/students/<%= student.id %>/cert-number', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ cert_number: num })
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) { msg.textContent = 'Сохранено'; msg.className = 'msg msg-ok'; }
|
||||||
|
else { msg.textContent = data.error || 'Ошибка'; msg.className = 'msg msg-err'; }
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('mandatory-form').addEventListener('submit', async (e) => {
|
document.getElementById('mandatory-form').addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const score = v => { const n = parseInt(v,10); return isNaN(n)?null:n; };
|
const score = v => { const n = parseInt(v,10); return isNaN(n)?null:n; };
|
||||||
@@ -198,7 +232,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
msg.textContent = '';
|
msg.textContent = '';
|
||||||
const url = `/certificate/<%= student.id %>?type=${type}&subjects=${electives.join(',')}`;
|
const date = document.getElementById('cert-date').value;
|
||||||
|
const url = `/certificate/<%= student.id %>?type=${type}&subjects=${electives.join(',')}&date=${date}`;
|
||||||
window.open(url, '_blank');
|
window.open(url, '_blank');
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user