- db: modules table (name, lessons_count) + entries.module_id (ON DELETE SET NULL), migration + idempotent startup ensure - api: GET /api/modules (public, search + limit/offset, entries_count), POST/PUT/DELETE (admin, audit-logged) - entries: accept/validate module_id on create/update, return module_name, module_id filter - backup/restore: include modules and entries.module_id - student form: required module select, hidden while no modules exist - admin: modules.html + js/modules.js list with pagination, search, create/edit/delete modal - journal: module filter, module select in edit modal, module badge, CSV column
137 lines
5.5 KiB
JavaScript
137 lines
5.5 KiB
JavaScript
let modulesList = [];
|
||
let mPage = 1;
|
||
const M_PAGE_SIZE = 25;
|
||
let mSearchTimer = null;
|
||
|
||
function mPlural(n, one, few, many) {
|
||
n = Math.abs(n) % 100; const n1 = n % 10;
|
||
if (n > 10 && n < 20) return many;
|
||
if (n1 > 1 && n1 < 5) return few;
|
||
if (n1 === 1) return one;
|
||
return many;
|
||
}
|
||
|
||
async function loadModules() {
|
||
const p = new URLSearchParams();
|
||
const q = document.getElementById('moduleSearch').value.trim();
|
||
if (q) p.set('search', q);
|
||
p.set('limit', M_PAGE_SIZE);
|
||
p.set('offset', (mPage - 1) * M_PAGE_SIZE);
|
||
const res = await fetch(`${API}/api/modules?${p}`, { headers: hdr() });
|
||
if (!res.ok) return;
|
||
const data = await res.json();
|
||
modulesList = data.modules || [];
|
||
const totalPages = Math.max(1, Math.ceil(data.total / M_PAGE_SIZE));
|
||
if (mPage > totalPages) { mPage = totalPages; return loadModules(); }
|
||
renderModules(data.total);
|
||
}
|
||
|
||
function renderModules(total) {
|
||
const list = document.getElementById('moduleList');
|
||
if (!modulesList.length) {
|
||
list.innerHTML = '<div class="empty">Модулей пока нет. Нажмите «Добавить модуль».</div>';
|
||
} else {
|
||
list.innerHTML = modulesList.map(m => `
|
||
<div class="item module-item" data-id="${m.id}">
|
||
<div class="module-info">
|
||
<div class="module-name">${esc(m.name)}</div>
|
||
<div class="module-meta">${m.lessons_count} ${mPlural(m.lessons_count, 'занятие', 'занятия', 'занятий')} · ${m.entries_count} ${mPlural(m.entries_count, 'запись', 'записи', 'записей')}</div>
|
||
</div>
|
||
<div class="student-actions">
|
||
<button class="btn-link" type="button" data-edit-module="${m.id}">Изменить</button>
|
||
<button class="btn-del" type="button" data-del-module="${m.id}" title="Удалить">×</button>
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
renderPager(document.getElementById('pager'), mPage, Math.max(1, Math.ceil(total / M_PAGE_SIZE)), p => { mPage = p; loadModules(); });
|
||
}
|
||
|
||
function openModuleModal(id = null) {
|
||
const modal = document.getElementById('moduleModal');
|
||
const form = document.getElementById('moduleForm');
|
||
const title = document.getElementById('moduleModalTitle');
|
||
const submitBtn = document.getElementById('moduleSubmitBtn');
|
||
form.reset();
|
||
document.getElementById('moduleId').value = '';
|
||
document.getElementById('moduleLessons').value = '0';
|
||
if (id) {
|
||
const m = modulesList.find(x => x.id === id);
|
||
if (!m) return;
|
||
title.textContent = 'Редактировать модуль';
|
||
submitBtn.textContent = 'Сохранить';
|
||
form.name.value = m.name;
|
||
document.getElementById('moduleLessons').value = String(m.lessons_count);
|
||
document.getElementById('moduleId').value = m.id;
|
||
} else {
|
||
title.textContent = 'Создать модуль';
|
||
submitBtn.textContent = 'Создать';
|
||
}
|
||
modal.classList.add('open');
|
||
setTimeout(() => form.name.focus(), 100);
|
||
}
|
||
|
||
function closeModuleModal() {
|
||
document.getElementById('moduleModal').classList.remove('open');
|
||
}
|
||
|
||
async function deleteModule(id) {
|
||
const m = modulesList.find(x => x.id === id);
|
||
const msg = m && m.entries_count > 0
|
||
? `Удалить модуль «${m.name}»? У ${m.entries_count} связанных записей тема будет очищена.`
|
||
: 'Удалить этот модуль?';
|
||
if (!confirm(msg)) return;
|
||
const res = await fetch(`${API}/api/modules/${id}`, { method: 'DELETE', headers: hdr() });
|
||
if (res.ok) {
|
||
showToast('Модуль удалён');
|
||
loadModules();
|
||
} else {
|
||
const err = await res.json().catch(() => ({}));
|
||
alert(err.error || 'Ошибка');
|
||
}
|
||
}
|
||
|
||
document.getElementById('moduleForm').addEventListener('submit', async (e) => {
|
||
e.preventDefault();
|
||
const form = e.target;
|
||
const id = document.getElementById('moduleId').value;
|
||
const name = form.name.value.trim();
|
||
const lessons_count = parseInt(form.lessons_count.value, 10);
|
||
if (!name) { alert('Название обязательно'); return; }
|
||
if (!Number.isInteger(lessons_count) || lessons_count < 0) { alert('Количество занятий — целое неотрицательное число'); return; }
|
||
const res = await fetch(id ? `${API}/api/modules/${id}` : `${API}/api/modules`, {
|
||
method: id ? 'PUT' : 'POST',
|
||
headers: hdrJson(),
|
||
body: JSON.stringify({ name, lessons_count })
|
||
});
|
||
if (res.ok) {
|
||
showToast(id ? 'Модуль обновлён' : 'Модуль создан');
|
||
closeModuleModal();
|
||
loadModules();
|
||
} else {
|
||
const err = await res.json().catch(() => ({}));
|
||
alert(err.error || 'Ошибка');
|
||
}
|
||
});
|
||
|
||
document.getElementById('addModuleBtn').addEventListener('click', () => openModuleModal());
|
||
document.getElementById('moduleCancelBtn').addEventListener('click', closeModuleModal);
|
||
document.getElementById('moduleModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeModuleModal(); });
|
||
document.getElementById('moduleSearch').addEventListener('input', () => {
|
||
clearTimeout(mSearchTimer);
|
||
mSearchTimer = setTimeout(() => { mPage = 1; loadModules(); }, 300);
|
||
});
|
||
|
||
document.getElementById('moduleList').addEventListener('click', e => {
|
||
const ed = e.target.closest('[data-edit-module]');
|
||
if (ed) { openModuleModal(parseInt(ed.dataset.editModule, 10)); return; }
|
||
const dl = e.target.closest('[data-del-module]');
|
||
if (dl) deleteModule(parseInt(dl.dataset.delModule, 10));
|
||
});
|
||
|
||
(async () => {
|
||
if (await requireAdminPage()) {
|
||
buildSidebar(document.body.dataset.page);
|
||
loadModules();
|
||
}
|
||
})(); |