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(); } })();