Files
gia-gve-certificates/views/index.ejs
2026-07-01 11:59:35 +05:00

230 lines
9.4 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 id="warning-banner" class="banner-warning" style="display:none"></div>
<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">
<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>
</div>
</div>
<div class="filter-bar">
<label class="filter-label">
<input type="radio" name="filter" value="all" checked onchange="onFilterChange()"> Все
</label>
<label class="filter-label filter-gia">
<input type="radio" name="filter" value="gia" onchange="onFilterChange()"> ОГЭ (ГИА)
</label>
<label class="filter-label filter-gve">
<input type="radio" name="filter" value="gve" onchange="onFilterChange()"> ГВЭ
</label>
<label class="filter-label filter-nodata">
<input type="radio" name="filter" value="nodata" onchange="onFilterChange()"> Нет данных
</label>
<label class="filter-label" style="background:#ffe0e0;color:#900">
<input type="radio" name="filter" value="conflict" onchange="onFilterChange()"> Конфликт
</label>
<label class="filter-label" style="background:#fff0d0;color:#860">
<input type="radio" name="filter" value="secondyear" onchange="onFilterChange()"> Второгодники
</label>
<label class="filter-label" style="background:#fdd;color:#a00">
<input type="radio" name="filter" value="lowgrade" onchange="onFilterChange()"> Отметка &lt; 3
</label>
</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>
<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() {
const search = document.getElementById('search').value;
const filter = document.querySelector('input[name="filter"]:checked').value;
const params = new URLSearchParams();
if (search) params.set('search', search);
if (filter && filter !== 'all') params.set('filter', filter);
const url = '/api/students' + (params.toString() ? '?' + params.toString() : '');
const res = await fetch(url);
allStudents = await res.json();
renderTable(allStudents);
updateBanner(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 updateBanner(students) {
const nodata = students.filter(s => (s.mandatory_grades || 0) < 2);
const banner = document.getElementById('warning-banner');
if (nodata.length > 0) {
banner.style.display = 'block';
banner.innerHTML = `⚠ ${nodata.length} учеников без оценок по русскому/математике`;
} else {
banner.style.display = 'none';
}
}
function formatSubjectsList(list) {
if (!list) return '—';
return list.split(' / ').map(item => {
const m = item.match(/^(.*?)\s*(\d)\s*$/);
if (m && parseInt(m[2]) < 3) return `<span style="color:#c0392b;font-weight:600">${esc(item)}</span>`;
return esc(item);
}).join(' / ');
}
function renderTable(students) {
const tbody = document.querySelector('#students-table tbody');
tbody.innerHTML = students.map((s, i) => {
const isSecondYear = (s.elective_count || 0) === 0 && s.exam_count === 1;
const hasMandatory = isSecondYear ? (s.mandatory_grades || 0) >= 1 : (s.mandatory_grades || 0) >= 2;
const rowClass = !hasMandatory ? 'row-warning' : '';
const isOge = (s.elective_count || 0) > 0 || isSecondYear;
const examType = isOge ? 'ОГЭ' : 'ГВЭ';
const examTypeClass = isOge ? 'badge-gia' : 'badge-gve';
return `
<tr class="${rowClass}" onclick="window.location.href='/student/${s.id}'" style="cursor:pointer">
<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><span class="badge ${examTypeClass}">${examType}</span></td>
<td style="font-size:11px;max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${esc(s.subjects_list || '')}">${formatSubjectsList(s.subjects_list)}</td>
<td onclick="event.stopPropagation()"><a href="/student/${s.id}" class="btn btn-sm">Открыть</a></td>
</tr>`;
}).join('');
}
function esc(s) { return (s || '').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
document.getElementById('search').addEventListener('input', () => loadStudents());
function onFilterChange() { loadStudents(); }
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;
let url = '/batch-print';
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 += `?parallel=${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>