Files
gia-gve-certificates/views/declension.ejs
2026-07-02 15:34:46 +05:00

247 lines
11 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">&larr; К списку</a>
<a href="/import" class="btn btn-green">Импорт XLS</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 class="card">
<p style="font-size:13px;color:#666;margin-bottom:10px">
Настройте склонение фамилии, имени и отчества для справок.
Если отмечено «Не склоняется» — ФИО будет использоваться в именительном падеже.
Если введено своё склонение — оно будет использовано вместо автоматического.
</p>
<div class="form-row">
<label>Фамилия:</label>
<input type="text" id="filter-last" class="input input-sm" placeholder="Фильтр по фамилии" oninput="loadDeclensions()">
</div>
<div class="form-row">
<label>Имя:</label>
<input type="text" id="filter-first" class="input input-sm" placeholder="Фильтр по имени" oninput="loadDeclensions()">
</div>
<div class="form-row">
<label>Отчество:</label>
<input type="text" id="filter-patr" class="input input-sm" placeholder="Фильтр по отчеству" oninput="loadDeclensions()">
</div>
</div>
<div class="card" style="overflow-x:auto">
<table id="declension-table">
<thead>
<tr>
<th>Фамилия</th>
<th>Имя</th>
<th>Отчество</th>
<th>Склонённая фамилия</th>
<th style="text-align:center">Не скл.</th>
<th>Склонённое имя</th>
<th style="text-align:center">Не скл.</th>
<th>Склонённое отчество</th>
<th style="text-align:center">Не скл.</th>
<th>Действия</th>
</tr>
</thead>
<tbody></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';
});
function esc(s) { return (s || '').replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
function autoDeclineLast(name, gender) {
if (!name) return '';
if (gender === 'f') {
if (name.endsWith('ая')) return name.slice(0, -2) + 'ой';
if (name.endsWith('яя')) return name.slice(0, -2) + 'ей';
if (name.endsWith('а')) return name.slice(0, -1) + 'ой';
if (name.endsWith('я')) return name.slice(0, -1) + 'е';
if (name.endsWith('ова') || name.endsWith('ева') || name.endsWith('ина')) return name.slice(0, -1) + 'ой';
return name + 'ой';
}
if (name.endsWith('ов') || name.endsWith('ев') || name.endsWith('ин') || name.endsWith('ёв')) return name + 'у';
if (name.endsWith('ий') || name.endsWith('ый')) return name.slice(0, -2) + 'ому';
if (name.endsWith('ой')) return name.slice(0, -2) + 'ому';
return name + 'у';
}
function autoDeclineFirst(name, gender) {
if (!name) return '';
if (gender === 'f') {
if (name.endsWith('а') || name.endsWith('я')) return name.slice(0, -1) + 'е';
return name + 'е';
}
if (name.endsWith('й') || name.endsWith('ь')) return name.slice(0, -1) + 'ю';
if (name.endsWith('а') || name.endsWith('я')) return name.slice(0, -1) + 'е';
return name + 'у';
}
function autoDeclinePatr(name, gender) {
if (!name) return '';
if (gender === 'f') {
if (name.endsWith('на')) return name.slice(0, -1) + 'е';
return name + 'е';
}
if (name.endsWith('ич')) return name + 'у';
if (name.endsWith('на')) return name.slice(0, -1) + 'е';
return name + 'у';
}
function detectGender(patronymic) {
if (!patronymic) return 'm';
const last = patronymic.toLowerCase();
if (/вна|чна|нична$/.test(last)) return 'f';
return 'm';
}
async function loadDeclensions() {
const last_name = document.getElementById('filter-last').value;
const first_name = document.getElementById('filter-first').value;
const patronymic = document.getElementById('filter-patr').value;
const params = new URLSearchParams();
if (last_name) params.set('last_name', last_name);
if (first_name) params.set('first_name', first_name);
if (patronymic) params.set('patronymic', patronymic);
const url = '/api/declensions' + (params.toString() ? '?' + params.toString() : '');
const res = await fetch(url);
const rows = await res.json();
renderTable(rows);
}
function renderTable(rows) {
const tbody = document.querySelector('#declension-table tbody');
tbody.innerHTML = rows.map((r, i) => {
const gender = detectGender(r.patronymic);
const autoLast = autoDeclineLast(r.last_name, gender);
const autoFirst = autoDeclineFirst(r.first_name, gender);
const autoPatr = autoDeclinePatr(r.patronymic, gender);
const currentLast = r.no_decline_last ? r.last_name : (r.last_name_declined || autoLast);
const currentFirst = r.no_decline_first ? r.first_name : (r.first_name_declined || autoFirst);
const currentPatr = r.no_decline_patr ? r.patronymic : (r.patronymic_declined || autoPatr);
return `
<tr data-idx="${i}">
<td>${esc(r.last_name)}</td>
<td>${esc(r.first_name)}</td>
<td>${esc(r.patronymic)}</td>
<td><input type="text" class="input input-sm decl-input" data-field="last_name_declined" data-idx="${i}" value="${esc(currentLast)}" placeholder="${esc(autoLast)}"></td>
<td style="text-align:center"><input type="checkbox" class="decl-cb" data-field="no_decline_last" data-idx="${i}" ${r.no_decline_last ? 'checked' : ''}></td>
<td><input type="text" class="input input-sm decl-input" data-field="first_name_declined" data-idx="${i}" value="${esc(currentFirst)}" placeholder="${esc(autoFirst)}"></td>
<td style="text-align:center"><input type="checkbox" class="decl-cb" data-field="no_decline_first" data-idx="${i}" ${r.no_decline_first ? 'checked' : ''}></td>
<td><input type="text" class="input input-sm decl-input" data-field="patronymic_declined" data-idx="${i}" value="${esc(currentPatr)}" placeholder="${esc(autoPatr)}"></td>
<td style="text-align:center"><input type="checkbox" class="decl-cb" data-field="no_decline_patr" data-idx="${i}" ${r.no_decline_patr ? 'checked' : ''}></td>
<td><button class="btn btn-sm btn-green" onclick="saveRow(${i})">Сохранить</button><span id="msg-${i}" class="msg"></span></td>
</tr>`;
}).join('');
window.__declensionRows = rows;
document.querySelectorAll('.decl-cb').forEach(cb => {
cb.addEventListener('change', function() {
const idx = this.dataset.idx;
const field = this.dataset.field;
const row = window.__declensionRows[idx];
const inputField = field.replace('no_decline_', '') + '_declined';
const inputEl = document.querySelector(`.decl-input[data-idx="${idx}"][data-field="${inputField}"]`);
if (this.checked) {
inputEl.value = field === 'no_decline_last' ? row.last_name : (field === 'no_decline_first' ? row.first_name : row.patronymic);
inputEl.disabled = true;
} else {
inputEl.disabled = false;
}
});
});
document.querySelectorAll('.decl-cb:checked').forEach(cb => {
const idx = cb.dataset.idx;
const field = cb.dataset.field;
const inputField = field.replace('no_decline_', '') + '_declined';
const inputEl = document.querySelector(`.decl-input[data-idx="${idx}"][data-field="${inputField}"]`);
if (inputEl) inputEl.disabled = true;
});
}
async function saveRow(idx) {
const row = window.__declensionRows[idx];
const lastInput = document.querySelector(`.decl-input[data-idx="${idx}"][data-field="last_name_declined"]`);
const firstInput = document.querySelector(`.decl-input[data-idx="${idx}"][data-field="first_name_declined"]`);
const patrInput = document.querySelector(`.decl-input[data-idx="${idx}"][data-field="patronymic_declined"]`);
const lastCb = document.querySelector(`.decl-cb[data-idx="${idx}"][data-field="no_decline_last"]`);
const firstCb = document.querySelector(`.decl-cb[data-idx="${idx}"][data-field="no_decline_first"]`);
const patrCb = document.querySelector(`.decl-cb[data-idx="${idx}"][data-field="no_decline_patr"]`);
const msgEl = document.getElementById(`msg-${idx}`);
const body = {
last_name: row.last_name,
first_name: row.first_name,
patronymic: row.patronymic,
last_name_declined: lastCb.checked ? '' : lastInput.value,
first_name_declined: firstCb.checked ? '' : firstInput.value,
patronymic_declined: patrCb.checked ? '' : patrInput.value,
no_decline_last: lastCb.checked,
no_decline_first: firstCb.checked,
no_decline_patr: patrCb.checked
};
try {
const res = await fetch('/api/declensions', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await res.json();
if (res.ok) {
msgEl.textContent = 'Сохранено';
msgEl.className = 'msg msg-ok';
loadDeclensions();
} else {
msgEl.textContent = data.error || 'Ошибка';
msgEl.className = 'msg msg-err';
}
} catch (e) {
msgEl.textContent = 'Ошибка сети';
msgEl.className = 'msg msg-err';
}
}
loadDeclensions();
</script>
</body>
</html>