214 lines
8.8 KiB
JavaScript
214 lines
8.8 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 || 'Ошибка');
|
||
}
|
||
});
|
||
|
||
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();
|
||
}
|
||
})(); |