Files
gia-gve-certificates/views/index.ejs
2026-07-03 12:02:46 +05:00

276 lines
12 KiB
Plaintext
Raw Permalink 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>
<a href="/declension" class="btn" style="background:#2a7d5a">Склонение ФИО</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>
<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">
<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>
<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="filter-extras">
<div class="form-row">
<label>Предмет:</label>
<select id="subject-select" class="input" onchange="loadStudents()">
<option value="">Все предметы</option>
</select>
<label>Балл:</label>
<input type="number" id="score-min" class="input input-xs" placeholder="от" oninput="loadStudents()">
<input type="number" id="score-max" class="input input-xs" placeholder="до" oninput="loadStudents()">
<label>Оценка:</label>
<input type="number" id="grade-min" class="input input-xs" placeholder="от" min="2" max="5" oninput="loadStudents()">
<input type="number" id="grade-max" class="input input-xs" placeholder="до" min="2" max="5" oninput="loadStudents()">
</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>
<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 subjectCode = document.getElementById('subject-select').value;
const scoreMin = document.getElementById('score-min').value;
const scoreMax = document.getElementById('score-max').value;
const gradeMin = document.getElementById('grade-min').value;
const gradeMax = document.getElementById('grade-max').value;
const params = new URLSearchParams();
if (search) params.set('search', search);
if (filter && filter !== 'all') params.set('filter', filter);
if (subjectCode) params.set('subject_code', subjectCode);
if (scoreMin) params.set('score_min', scoreMin);
if (scoreMax) params.set('score_max', scoreMax);
if (gradeMin) params.set('grade_min', gradeMin);
if (gradeMax) params.set('grade_max', gradeMax);
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('');
}
async function loadSubjects() {
const res = await fetch('/api/subjects');
const subjects = await res.json();
const sel = document.getElementById('subject-select');
sel.innerHTML = '<option value="">Все предметы</option>' +
subjects.map(s => `<option value="${esc(s.code)}">${esc(s.name)}</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';
const sep = url.includes('?') ? '&' : '?';
if (scope === 'class') {
const cls = document.getElementById('class-select').value;
if (!cls) return alert('Выберите класс');
url += `${sep}class=${encodeURIComponent(cls)}`;
} else if (scope === 'parallel') {
const par = document.getElementById('parallel-select').value;
if (!par) return alert('Выберите параллель');
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');
});
document.getElementById('logout-btn').addEventListener('click', async () => {
await fetch('/api/logout', { method: 'POST' });
window.location.href = '/login';
});
document.getElementById('batch-date').value = new Date().toISOString().split('T')[0];
loadStudents();
loadClasses();
loadSubjects();
</script>
</body>
</html>