242 lines
10 KiB
Plaintext
242 lines
10 KiB
Plaintext
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title><%= student.last_name %> <%= student.first_name %> — Справки</title>
|
||
<link rel="stylesheet" href="/style.css">
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<nav class="nav">
|
||
<a href="/" class="btn">← К списку</a>
|
||
<a href="/history" class="btn" style="background:#5a3e85">История</a>
|
||
<span class="user-bar"><%= user ? user.name : '' %></span>
|
||
<button id="logout-btn" class="btn btn-logout">Выйти</button>
|
||
</nav>
|
||
|
||
<h1><%= student.last_name %> <%= student.first_name %> <%= student.patronymic %></h1>
|
||
<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; %>
|
||
<% if (!hasRus || !hasMath) { %>
|
||
<div class="banner-warning">
|
||
⚠ Не заполнены оценки:
|
||
<%= !hasRus ? 'русский язык' : '' %>
|
||
<%= !hasRus && !hasMath ? 'и' : '' %>
|
||
<%= !hasMath ? 'математика' : '' %>.
|
||
Заполните обязательные предметы перед печатью.
|
||
</div>
|
||
<% } %>
|
||
|
||
<div class="card">
|
||
<h2>Обязательные предметы</h2>
|
||
<form id="mandatory-form" class="mandatory-form">
|
||
<div class="form-row">
|
||
<label>Русский язык</label>
|
||
<input type="number" id="rus-score" class="input input-sm" placeholder="Первичный балл" value="<%= rusResult ? rusResult.primary_score || '' : '' %>">
|
||
<input type="number" id="rus-grade" class="input input-sm" placeholder="Оценка (2-5)" min="2" max="5" value="<%= rusResult ? rusResult.grade || '' : '' %>">
|
||
</div>
|
||
<div class="form-row">
|
||
<label>Математика</label>
|
||
<input type="number" id="math-score" class="input input-sm" placeholder="Первичный балл" value="<%= mathResult ? mathResult.primary_score || '' : '' %>">
|
||
<input type="number" id="math-grade" class="input input-sm" placeholder="Оценка (2-5)" min="2" max="5" value="<%= mathResult ? mathResult.grade || '' : '' %>">
|
||
</div>
|
||
<button type="submit" class="btn btn-green">Сохранить</button>
|
||
<span id="mandatory-msg" class="msg"></span>
|
||
</form>
|
||
</div>
|
||
|
||
<% if (electiveResults.length > 0) { %>
|
||
<div class="card">
|
||
<h2>Предметы по выбору (из протокола)</h2>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Предмет</th>
|
||
<th>Первичный балл</th>
|
||
<th>Оценка</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% electiveResults.forEach(r => { %>
|
||
<tr>
|
||
<td><%= r.subject_name %></td>
|
||
<td><%= r.primary_score !== null ? r.primary_score : '—' %></td>
|
||
<td><strong><%= r.grade !== null ? r.grade : '—' %></strong></td>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% } %>
|
||
|
||
<div class="card">
|
||
<h2>Сформировать справку</h2>
|
||
<form id="cert-form">
|
||
<div class="form-row">
|
||
<label>
|
||
<input type="radio" name="cert-type" value="gia" checked onchange="toggleElectives()">
|
||
ГИА (ОГЭ) — 4 предмета, с первичными баллами
|
||
</label>
|
||
</div>
|
||
<div class="form-row">
|
||
<label>
|
||
<input type="radio" name="cert-type" value="gve" onchange="toggleElectives()">
|
||
ГВЭ — 2 предмета, только отметки
|
||
</label>
|
||
</div>
|
||
|
||
<div id="electives-block" style="margin-top:10px">
|
||
<p>Выберите 1–2 предмета для справки ГИА:</p>
|
||
<% electiveResults.forEach(r => { %>
|
||
<label class="checkbox-label">
|
||
<input type="checkbox" name="elective" value="<%= r.subject_code %>" class="elective-cb">
|
||
<%= r.subject_name %>
|
||
</label>
|
||
<% }) %>
|
||
<span id="elective-msg" class="msg"></span>
|
||
</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>
|
||
</form>
|
||
</div>
|
||
|
||
<% if (groupedHistory && groupedHistory.length > 0) { %>
|
||
<div class="card">
|
||
<h2>История изменений</h2>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Дата</th>
|
||
<th>Пользователь</th>
|
||
<th>Источник</th>
|
||
<th>Изменения</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<% groupedHistory.forEach(g => { %>
|
||
<tr>
|
||
<td style="white-space:nowrap;font-size:12px"><%= g.changed_at %></td>
|
||
<td style="font-size:12px"><%= g.changed_by %></td>
|
||
<td style="font-size:12px">
|
||
<% if (g.source === 'import') { %>XLS импорт
|
||
<% } else if (g.source === 'manual') { %>вручную
|
||
<% } else { %><%= g.source %><% } %>
|
||
</td>
|
||
<td style="font-size:12px">
|
||
<% g.changes.forEach((c, ci) => { %>
|
||
<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>
|
||
</tr>
|
||
<% }) %>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<% } %>
|
||
</div>
|
||
|
||
<script>
|
||
window.__CSRF_TOKEN = '<%= csrfToken %>';
|
||
|
||
const _origFetch = window.fetch.bind(window);
|
||
window.fetch = function(input, init = {}) {
|
||
init.credentials = 'include';
|
||
const method = (init.method || 'GET').toUpperCase();
|
||
if (['POST','PUT','PATCH','DELETE'].includes(method)) {
|
||
if (!init.headers) init.headers = {};
|
||
init.headers['X-CSRF-Token'] = window.__CSRF_TOKEN || '';
|
||
}
|
||
return _origFetch(input, init);
|
||
};
|
||
|
||
document.getElementById('logout-btn').addEventListener('click', async () => {
|
||
await fetch('/api/logout', { method: 'POST' });
|
||
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) => {
|
||
e.preventDefault();
|
||
const score = v => { const n = parseInt(v,10); return isNaN(n)?null:n; };
|
||
const res = await fetch('/api/students/<%= student.id %>/mandatory', {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
rus_score: score(document.getElementById('rus-score').value),
|
||
rus_grade: score(document.getElementById('rus-grade').value),
|
||
math_score: score(document.getElementById('math-score').value),
|
||
math_grade: score(document.getElementById('math-grade').value)
|
||
})
|
||
});
|
||
const msg = document.getElementById('mandatory-msg');
|
||
if (res.ok) { msg.textContent = 'Сохранено'; msg.className = 'msg msg-ok'; }
|
||
else { msg.textContent = 'Ошибка'; msg.className = 'msg msg-err'; }
|
||
});
|
||
|
||
document.querySelectorAll('.elective-cb').forEach(cb => {
|
||
cb.addEventListener('change', () => {
|
||
const checked = document.querySelectorAll('.elective-cb:checked');
|
||
if (checked.length > 2) cb.checked = false;
|
||
});
|
||
});
|
||
|
||
function toggleElectives() {
|
||
const type = document.querySelector('input[name="cert-type"]:checked').value;
|
||
document.getElementById('electives-block').style.display = type === 'gia' ? 'block' : 'none';
|
||
}
|
||
|
||
document.getElementById('cert-form').addEventListener('submit', (e) => {
|
||
e.preventDefault();
|
||
const type = document.querySelector('input[name="cert-type"]:checked').value;
|
||
const electives = [...document.querySelectorAll('.elective-cb:checked')].map(cb => cb.value);
|
||
const msg = document.getElementById('elective-msg');
|
||
|
||
if (type === 'gia' && electives.length < 1) {
|
||
msg.textContent = 'Выберите хотя бы 1 предмет для ГИА';
|
||
msg.className = 'msg msg-err';
|
||
return;
|
||
}
|
||
msg.textContent = '';
|
||
const date = document.getElementById('cert-date').value;
|
||
const url = `/certificate/<%= student.id %>?type=${type}&subjects=${electives.join(',')}&date=${date}`;
|
||
window.open(url, '_blank');
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|