Files
WhatIDo/public/js/modules.js
T

214 lines
8.8 KiB
JavaScript
Raw 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.
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="Удалить">&times;</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();
}
})();