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 = '
Модулей пока нет. Нажмите «Добавить модуль».
';
} else {
list.innerHTML = modulesList.map(m => `
${esc(m.name)}
${m.lessons_count} ${mPlural(m.lessons_count, 'занятие', 'занятия', 'занятий')} · ${m.entries_count} ${mPlural(m.entries_count, 'запись', 'записи', 'записей')}
`).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 || 'Ошибка');
}
});
function parseBatchModules(raw) {
const items = [];
const errors = [];
const seen = new Set();
String(raw).split('\n').forEach((line, idx) => {
const text = line.trim();
if (!text) return;
const lineNo = idx + 1;
let name = '';
let lessonsRaw = '';
const sepIndex = text.search(/[|;=\t]/);
if (sepIndex !== -1) {
name = text.slice(0, sepIndex).trim();
lessonsRaw = text.slice(sepIndex + 1).trim();
} else {
const numMatch = text.match(/^(.*\S)\s+(\d+)$/);
if (numMatch) {
name = numMatch[1].trim();
lessonsRaw = numMatch[2];
}
}
if (!name && !lessonsRaw) { errors.push(`Строка ${lineNo}: «${text}» — укажите название и количество занятий, например: Основы Python | 10`); return; }
if (!name) { errors.push(`Строка ${lineNo}: «${text}» — не указано название`); return; }
if (!lessonsRaw) { errors.push(`Строка ${lineNo}: «${text}» — не указано количество занятий`); return; }
const lessons = Number(lessonsRaw);
if (!Number.isInteger(lessons) || lessons < 0 || lessons > 10000) {
errors.push(`Строка ${lineNo}: «${text}» — количество занятий должно быть целым числом от 0 до 10000`);
return;
}
const key = name.toLowerCase();
if (seen.has(key)) { errors.push(`Строка ${lineNo}: «${text}» — дубликат названия в списке`); return; }
seen.add(key);
items.push({ name, lessons_count: lessons });
});
return { items, errors };
}
function openBatchModuleModal() {
document.getElementById('batchModuleForm').reset();
document.getElementById('batchModuleModal').classList.add('open');
setTimeout(() => document.getElementById('batchModuleText').focus(), 100);
}
function closeBatchModuleModal() {
document.getElementById('batchModuleModal').classList.remove('open');
}
async function batchAddModules() {
const submitBtn = document.getElementById('batchModuleSubmitBtn');
const { items, errors } = parseBatchModules(document.getElementById('batchModuleText').value);
if (errors.length) {
alert('Исправьте строки:\n\n' + errors.join('\n'));
return;
}
if (!items.length) { alert('Добавьте хотя бы один модуль'); return; }
submitBtn.disabled = true;
let added = 0, dupes = 0, failed = 0;
for (const item of items) {
const res = await fetch(`${API}/api/modules`, { method: 'POST', headers: hdrJson(), body: JSON.stringify(item) });
if (res.ok) added++;
else if (res.status === 409) dupes++;
else failed++;
}
submitBtn.disabled = false;
closeBatchModuleModal();
loadModules();
showToast(`Добавлено: ${added}${dupes ? `, дубликатов: ${dupes}` : ''}${failed ? `, ошибок: ${failed}` : ''}`);
}
document.getElementById('batchModuleForm').addEventListener('submit', e => {
e.preventDefault();
batchAddModules();
});
document.getElementById('batchModuleBtn').addEventListener('click', openBatchModuleModal);
document.getElementById('batchModuleCancelBtn').addEventListener('click', closeBatchModuleModal);
document.getElementById('batchModuleModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeBatchModuleModal(); });
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();
}
})();