174 lines
6.3 KiB
Plaintext
174 lines
6.3 KiB
Plaintext
<!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">
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>Ученики</h1>
|
||
|
||
<nav class="nav">
|
||
<a href="/" class="btn">Список учеников</a>
|
||
<a href="/import" class="btn btn-green">Импорт XLS</a>
|
||
<span class="user-bar"><%= user ? user.name : '' %></span>
|
||
<button id="logout-btn" class="btn btn-logout">Выйти</button>
|
||
</nav>
|
||
|
||
<div class="card batch-panel">
|
||
<h2>Пакетная печать</h2>
|
||
<div class="form-row">
|
||
<label>
|
||
<input type="radio" name="batch-scope" value="class" checked onchange="toggleBatchMode()"> Класс
|
||
</label>
|
||
<select id="class-select" class="input">
|
||
<option value="">— загрузка... —</option>
|
||
</select>
|
||
<label style="margin-left:15px">
|
||
<input type="radio" name="batch-scope" value="parallel" onchange="toggleBatchMode()"> Параллель
|
||
</label>
|
||
<select id="parallel-select" class="input" disabled>
|
||
<option value="">— загрузка... —</option>
|
||
</select>
|
||
<label style="margin-left:15px">
|
||
<input type="radio" name="batch-scope" value="all" onchange="toggleBatchMode()"> Все ученики
|
||
</label>
|
||
</div>
|
||
<div class="form-row" style="margin-top:8px">
|
||
<label>
|
||
<input type="radio" name="batch-type" value="gia" checked> ГИА (ОГЭ)
|
||
</label>
|
||
<label style="margin-left:15px">
|
||
<input type="radio" name="batch-type" value="gve"> ГВЭ
|
||
</label>
|
||
<button id="batch-print-btn" class="btn btn-blue" style="margin-left:15px">Печать справок</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="search-bar">
|
||
<input type="text" id="search" class="input" placeholder="Поиск по ФИО или классу...">
|
||
</div>
|
||
|
||
<table id="students-table">
|
||
<thead>
|
||
<tr>
|
||
<th>№</th>
|
||
<th>Фамилия</th>
|
||
<th>Имя</th>
|
||
<th>Отчество</th>
|
||
<th>Класс</th>
|
||
<th>Предметов</th>
|
||
<th>Действия</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
|
||
<div class="toolbar">
|
||
<button id="clear-data" class="btn btn-danger">Очистить все данные</button>
|
||
</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);
|
||
};
|
||
|
||
async function checkAuth() {
|
||
const r = await fetch('/api/user');
|
||
if (!r.ok) window.location.href = '/login';
|
||
}
|
||
checkAuth();
|
||
</script>
|
||
|
||
<script>
|
||
let allStudents = [];
|
||
|
||
async function loadStudents(search = '') {
|
||
const url = search ? `/api/students?search=${encodeURIComponent(search)}` : '/api/students';
|
||
const res = await fetch(url);
|
||
allStudents = await res.json();
|
||
renderTable(allStudents);
|
||
}
|
||
|
||
async function loadClasses() {
|
||
const res = await fetch('/api/classes');
|
||
const classes = await res.json();
|
||
const clsSel = document.getElementById('class-select');
|
||
clsSel.innerHTML = classes.map(c => `<option value="${esc(c)}">${esc(c)}</option>`).join('');
|
||
const parallels = [...new Set(classes.map(c => c.charAt(0)).filter(Boolean))].sort();
|
||
const parSel = document.getElementById('parallel-select');
|
||
parSel.innerHTML = parallels.map(p => `<option value="${esc(p)}">${esc(p)}-е классы</option>`).join('');
|
||
}
|
||
|
||
function toggleBatchMode() {
|
||
const scope = document.querySelector('input[name="batch-scope"]:checked').value;
|
||
document.getElementById('class-select').disabled = scope !== 'class';
|
||
document.getElementById('parallel-select').disabled = scope !== 'parallel';
|
||
}
|
||
|
||
function renderTable(students) {
|
||
const tbody = document.querySelector('#students-table tbody');
|
||
tbody.innerHTML = students.map((s, i) => `
|
||
<tr>
|
||
<td>${i + 1}</td>
|
||
<td>${esc(s.last_name)}</td>
|
||
<td>${esc(s.first_name)}</td>
|
||
<td>${esc(s.patronymic)}</td>
|
||
<td>${esc(s.class)}</td>
|
||
<td>${s.exam_count}</td>
|
||
<td><a href="/student/${s.id}" class="btn btn-sm">Открыть</a></td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function esc(s) { return (s || '').replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); }
|
||
|
||
document.getElementById('search').addEventListener('input', (e) => {
|
||
loadStudents(e.target.value);
|
||
});
|
||
|
||
document.getElementById('clear-data').addEventListener('click', async () => {
|
||
if (!confirm('Удалить всех учеников и результаты?')) return;
|
||
await fetch('/api/data', { method: 'DELETE' });
|
||
loadStudents();
|
||
});
|
||
|
||
document.getElementById('batch-print-btn').addEventListener('click', () => {
|
||
const scope = document.querySelector('input[name="batch-scope"]:checked').value;
|
||
const type = document.querySelector('input[name="batch-type"]:checked').value;
|
||
let url = `/batch-print?type=${type}`;
|
||
if (scope === 'class') {
|
||
const cls = document.getElementById('class-select').value;
|
||
if (!cls) return alert('Выберите класс');
|
||
url += `&class=${encodeURIComponent(cls)}`;
|
||
} else if (scope === 'parallel') {
|
||
const par = document.getElementById('parallel-select').value;
|
||
if (!par) return alert('Выберите параллель');
|
||
url += `¶llel=${encodeURIComponent(par)}`;
|
||
}
|
||
window.open(url, '_blank');
|
||
});
|
||
|
||
document.getElementById('logout-btn').addEventListener('click', async () => {
|
||
await fetch('/api/logout', { method: 'POST' });
|
||
window.location.href = '/login';
|
||
});
|
||
|
||
loadStudents();
|
||
loadClasses();
|
||
</script>
|
||
</body>
|
||
</html>
|