feat(admin): phase 2 — split admin.js into 13 section modules
Replace ~3500L admin.js monolith with thin orchestrator (~700L) + 14 IIFE-wrapped per-section modules under /js/admin/sections/. Section modules expose AdminSections.<name>.init/reload (lazy init via switchTab/router) and re-expose onclick handlers via window.X for backward compat. Shared helpers (MODES/DIFFS, fmtDate, pctClass, renderMath, qTypeBadge, pagination) live in /js/admin/_shared.js exposed on window.AdminCtx. switchTab now dispatches to AdminSections via ROUTE_TO_SECTION map; non-extracted system tabs (topics/audit/errors/health/classroom/avatars) remain inline in admin.js. user-panel overlay markup untouched — Phase 6 will remove it.
This commit is contained in:
@@ -0,0 +1,477 @@
|
||||
'use strict';
|
||||
/* admin → assignments section (классные/индивидуальные задания) */
|
||||
(function () {
|
||||
'use strict';
|
||||
let inited = false;
|
||||
|
||||
let allAssignments = [];
|
||||
let editingAId = null;
|
||||
const SUBJ_NAMES = { bio:'Биология', chem:'Химия', math:'Математика', phys:'Физика' };
|
||||
const SUBJ_COLORS_A = { bio:'#9B5DE5', chem:'#06D6A0', math:'#06B6D4', phys:'#F59E0B' };
|
||||
const SUBJ_ICONS_A = { bio:'dna', chem:'flask-conical', math:'calculator', phys:'zap' };
|
||||
let _aFilter = 'all';
|
||||
|
||||
// create-modal state
|
||||
let _afSrc = 'random';
|
||||
let _afLoadedTests = [];
|
||||
let _acSrc = 'random';
|
||||
let _acTarget = 'class';
|
||||
let _acFileId = null, _acAllFiles = null;
|
||||
let _acStudentId = null, _acAllStudents = null;
|
||||
|
||||
async function load() {
|
||||
document.getElementById('a-body').innerHTML = '<div class="spinner"></div>';
|
||||
try {
|
||||
allAssignments = await LS.teacherAssignments();
|
||||
renderAssignments();
|
||||
} catch (e) {
|
||||
document.getElementById('a-body').innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function setAFilter(f) {
|
||||
_aFilter = f;
|
||||
document.querySelectorAll('.a-f-chip').forEach(c =>
|
||||
c.classList.toggle('active', c.textContent.trim() === {all:'Все',active:'Активные',overdue:'Просрочены',done:'Завершены'}[f])
|
||||
);
|
||||
renderAssignments();
|
||||
}
|
||||
|
||||
function aClassify(a) {
|
||||
const pct = a.total_members ? Math.round(a.completed_count / a.total_members * 100) : null;
|
||||
if (pct === 100) return 'done';
|
||||
if (a.deadline && new Date(a.deadline) < new Date()) return 'overdue';
|
||||
return 'active';
|
||||
}
|
||||
|
||||
function renderAssignments() {
|
||||
const { MODES, pctClass } = AdminCtx;
|
||||
const subjF = document.getElementById('a-subject').value;
|
||||
const searchF = document.getElementById('a-search').value.toLowerCase();
|
||||
const sortF = document.getElementById('a-sort')?.value || 'date';
|
||||
|
||||
let list = allAssignments.filter(a => {
|
||||
if (subjF && a.subject_slug !== subjF) return false;
|
||||
if (searchF && !a.title.toLowerCase().includes(searchF)) return false;
|
||||
if (_aFilter === 'active' && aClassify(a) !== 'active') return false;
|
||||
if (_aFilter === 'overdue' && aClassify(a) !== 'overdue') return false;
|
||||
if (_aFilter === 'done' && aClassify(a) !== 'done') return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
list = [...list].sort((a, b) => {
|
||||
if (sortF === 'deadline') {
|
||||
const da = a.deadline ? new Date(a.deadline) : new Date(9e15);
|
||||
const db = b.deadline ? new Date(b.deadline) : new Date(9e15);
|
||||
return da - db;
|
||||
}
|
||||
if (sortF === 'progress_asc') {
|
||||
const pa = a.total_members ? a.completed_count / a.total_members : 0;
|
||||
const pb = b.total_members ? b.completed_count / b.total_members : 0;
|
||||
return pa - pb;
|
||||
}
|
||||
if (sortF === 'progress_desc') {
|
||||
const pa = a.total_members ? a.completed_count / a.total_members : 0;
|
||||
const pb = b.total_members ? b.completed_count / b.total_members : 0;
|
||||
return pb - pa;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const all = allAssignments;
|
||||
const nActive = all.filter(a => aClassify(a) === 'active').length;
|
||||
const nOverdue = all.filter(a => aClassify(a) === 'overdue').length;
|
||||
const nDone = all.filter(a => aClassify(a) === 'done').length;
|
||||
document.getElementById('a-summary').innerHTML = [
|
||||
`<span class="a-sum-chip s-all">Всего: ${all.length}</span>`,
|
||||
nActive ? `<span class="a-sum-chip s-active">Активных: ${nActive}</span>` : '',
|
||||
nOverdue ? `<span class="a-sum-chip s-overdue">Просрочено: ${nOverdue}</span>` : '',
|
||||
nDone ? `<span class="a-sum-chip s-done">Завершено: ${nDone}</span>` : '',
|
||||
].join('');
|
||||
|
||||
document.getElementById('a-count').textContent = `${list.length} заданий`;
|
||||
const container = document.getElementById('a-body');
|
||||
|
||||
if (!list.length) {
|
||||
container.innerHTML = '<div class="empty">Заданий нет</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
container.innerHTML = list.map(a => {
|
||||
const pct = a.total_members ? Math.round(a.completed_count / a.total_members * 100) : null;
|
||||
const cls = aClassify(a);
|
||||
const rowCls = cls === 'overdue' ? 'a-overdue' : cls === 'done' ? 'a-done' : '';
|
||||
const sColor = SUBJ_COLORS_A[a.subject_slug] || '#9B5DE5';
|
||||
const dlMs = a.deadline ? new Date(a.deadline) - now : Infinity;
|
||||
const isUrgent = cls === 'active' && dlMs > 0 && dlMs < 24 * 3600 * 1000;
|
||||
|
||||
const dl = a.deadline
|
||||
? new Date(a.deadline).toLocaleDateString('ru', {day:'numeric', month:'short'})
|
||||
: null;
|
||||
|
||||
const targetStr = a.target_user_id
|
||||
? esc(a.target_user_name || 'Ученик')
|
||||
: esc(a.class_name || '—');
|
||||
|
||||
const metaParts = [
|
||||
targetStr,
|
||||
SUBJ_NAMES[a.subject_slug] || a.subject_slug,
|
||||
`<span class="mode-badge mode-${a.mode}">${MODES[a.mode]||a.mode}</span>`,
|
||||
a.count + ' вопр.',
|
||||
dl ? `до ${dl}` : null,
|
||||
isUrgent ? `<span class="a-tag-urgent"><i data-lucide="zap" style="width:10px;height:10px;vertical-align:-1px"></i> срочно</span>` : null,
|
||||
cls === 'overdue' ? `<span class="a-tag-over">просрочено</span>` : null,
|
||||
].filter(Boolean);
|
||||
|
||||
const barColor = pct >= 75 ? '#06D6A0' : pct >= 40 ? '#F59E0B' : '#F15BB5';
|
||||
const pctLabel = pct !== null ? `${pct}%` : '—';
|
||||
|
||||
return `<div class="a-row ${rowCls}${isUrgent ? ' a-urgent' : ''}" style="--ac:${sColor}">
|
||||
<div class="a-icon" style="background:${sColor}18;color:${sColor}"><i data-lucide="${SUBJ_ICONS_A[a.subject_slug]||'file-text'}" style="width:18px;height:18px"></i></div>
|
||||
<div class="a-main">
|
||||
<div class="a-title">${esc(a.title)}</div>
|
||||
<div class="a-meta">${metaParts.join(' · ')}</div>
|
||||
</div>
|
||||
<div class="a-prog">
|
||||
<div class="a-prog-nums">
|
||||
<span>${a.completed_count} / ${a.total_members} сдали</span>
|
||||
<span class="a-prog-pct ${pctClass(pct)}">${pctLabel}</span>
|
||||
</div>
|
||||
<div class="a-prog-bar">
|
||||
<div class="a-prog-fill" style="width:${pct||0}%;background:${barColor}"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="a-actions">
|
||||
<button class="btn-edit-q" onclick="openAModal(${a.id})">Изменить</button>
|
||||
<button class="btn-del-q" onclick="deleteAsgn(${a.id})"><i data-lucide="x" style="width:14px;height:14px"></i></button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function setAfSrc(src) {
|
||||
_afSrc = src;
|
||||
document.querySelectorAll('[data-afsrc]').forEach(b => b.classList.toggle('active', b.dataset.afsrc === src));
|
||||
document.getElementById('af-random-fields').style.display = src === 'random' ? '' : 'none';
|
||||
document.getElementById('af-test-fields').style.display = src === 'test' ? '' : 'none';
|
||||
}
|
||||
|
||||
async function openAModal(id) {
|
||||
const a = allAssignments.find(x => x.id === id);
|
||||
if (!a) return;
|
||||
editingAId = id;
|
||||
document.getElementById('a-modal-title').textContent = `Редактировать: ${a.title}`;
|
||||
document.getElementById('af-title').value = a.title;
|
||||
document.getElementById('af-deadline').value = a.deadline ? a.deadline.split('T')[0] : '';
|
||||
document.getElementById('af-error').textContent = '';
|
||||
|
||||
const testSel = document.getElementById('af-test');
|
||||
testSel.innerHTML = '<option value="">Загрузка…</option>';
|
||||
try {
|
||||
_afLoadedTests = await LS.getTests();
|
||||
testSel.innerHTML = _afLoadedTests.length
|
||||
? '<option value="">— выберите тест —</option>' + _afLoadedTests.map(t => `<option value="${t.id}">${esc(t.title)} (${t.question_count} вопр.)</option>`).join('')
|
||||
: '<option value="">Нет тестов</option>';
|
||||
} catch {
|
||||
testSel.innerHTML = '<option value="">Ошибка загрузки</option>';
|
||||
_afLoadedTests = [];
|
||||
}
|
||||
|
||||
if (a.test_id) {
|
||||
setAfSrc('test');
|
||||
testSel.value = a.test_id;
|
||||
document.getElementById('af-mode-test').value = a.mode;
|
||||
} else {
|
||||
setAfSrc('random');
|
||||
document.getElementById('af-subject').value = a.subject_slug;
|
||||
document.getElementById('af-mode').value = a.mode;
|
||||
document.getElementById('af-count').value = a.count;
|
||||
}
|
||||
|
||||
document.getElementById('a-modal').classList.add('open');
|
||||
setTimeout(() => document.getElementById('af-title').focus(), 80);
|
||||
}
|
||||
|
||||
function closeAModal() {
|
||||
document.getElementById('a-modal').classList.remove('open');
|
||||
editingAId = null;
|
||||
}
|
||||
|
||||
async function saveAssignment() {
|
||||
const title = document.getElementById('af-title').value.trim();
|
||||
const deadline = document.getElementById('af-deadline').value || null;
|
||||
const errEl = document.getElementById('af-error');
|
||||
errEl.textContent = '';
|
||||
if (!title) { errEl.textContent = 'Введите название'; return; }
|
||||
|
||||
let payload = { title, deadline };
|
||||
|
||||
if (_afSrc === 'test') {
|
||||
const test_id = document.getElementById('af-test').value;
|
||||
const mode = document.getElementById('af-mode-test').value;
|
||||
if (!test_id) { errEl.textContent = 'Выберите тест'; return; }
|
||||
const testObj = _afLoadedTests.find(t => t.id === Number(test_id));
|
||||
if (testObj && testObj.question_count === 0) { errEl.textContent = 'В выбранном тесте нет вопросов'; return; }
|
||||
payload = { ...payload, test_id: Number(test_id), mode };
|
||||
} else {
|
||||
const subject_slug = document.getElementById('af-subject').value;
|
||||
const mode = document.getElementById('af-mode').value;
|
||||
const count = Number(document.getElementById('af-count').value);
|
||||
if (!subject_slug) { errEl.textContent = 'Выберите предмет'; return; }
|
||||
if (!count || count < 1) { errEl.textContent = 'Введите количество вопросов'; return; }
|
||||
payload = { ...payload, subject_slug, mode, count, test_id: null };
|
||||
}
|
||||
|
||||
const btn = document.getElementById('af-save');
|
||||
btn.disabled = true; btn.textContent = 'Сохранение…';
|
||||
try {
|
||||
await LS.updateAssignment(editingAId, payload);
|
||||
const idx = allAssignments.findIndex(x => x.id === editingAId);
|
||||
if (idx !== -1) Object.assign(allAssignments[idx], payload);
|
||||
closeAModal();
|
||||
renderAssignments();
|
||||
} catch (e) {
|
||||
errEl.textContent = 'Ошибка: ' + e.message;
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = 'Сохранить';
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Create assignment modal ─── */
|
||||
function setAcTarget(t) {
|
||||
_acTarget = t;
|
||||
document.querySelectorAll('[data-actgt]').forEach(b => b.classList.toggle('active', b.dataset.actgt === t));
|
||||
document.getElementById('acf-class-field').style.display = t === 'class' ? '' : 'none';
|
||||
document.getElementById('acf-user-field').style.display = t === 'user' ? '' : 'none';
|
||||
if (t === 'user' && !_acAllStudents) loadAcStudents();
|
||||
}
|
||||
|
||||
async function loadAcStudents() {
|
||||
const drop = document.getElementById('acf-student-drop');
|
||||
drop.innerHTML = '<div style="padding:8px 12px;font-size:13px;color:#9ca3af">Загрузка…</div>';
|
||||
drop.style.display = '';
|
||||
try {
|
||||
_acAllStudents = await LS.getStudentsList();
|
||||
openAcStudentDrop();
|
||||
} catch(e) {
|
||||
_acAllStudents = [];
|
||||
drop.innerHTML = `<div style="padding:8px 12px;font-size:13px;color:#ef4444">Ошибка загрузки: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function filterAcStudents(q) {
|
||||
openAcStudentDrop(q);
|
||||
}
|
||||
|
||||
function openAcStudentDrop(q) {
|
||||
const drop = document.getElementById('acf-student-drop');
|
||||
if (_acAllStudents === null) { loadAcStudents(); return; }
|
||||
const list = _acAllStudents;
|
||||
const term = (q !== undefined ? q : document.getElementById('acf-student-search').value).toLowerCase().trim();
|
||||
const filtered = term ? list.filter(s => s.name.toLowerCase().includes(term) || s.email.toLowerCase().includes(term)) : list;
|
||||
if (!filtered.length) {
|
||||
drop.innerHTML = '<div style="padding:8px 12px;font-size:13px;color:#9ca3af">Нет учеников</div>';
|
||||
drop.style.display = '';
|
||||
return;
|
||||
}
|
||||
drop.innerHTML = filtered.slice(0, 50).map(s =>
|
||||
`<div style="padding:8px 12px;cursor:pointer;border-bottom:1px solid #f3f4f6;font-size:13px" data-id="${s.id}" data-name="${esc(s.name)}" data-email="${esc(s.email)}" onmousedown="selectAcStudent(+this.dataset.id,this.dataset.name,this.dataset.email)" onmouseover="this.style.background='#f9fafb'" onmouseout="this.style.background=''">${esc(s.name)} <span style="color:#9ca3af">${esc(s.email)}</span></div>`
|
||||
).join('');
|
||||
drop.style.display = '';
|
||||
}
|
||||
|
||||
function closeAcStudentDrop() {
|
||||
document.getElementById('acf-student-drop').style.display = 'none';
|
||||
}
|
||||
|
||||
function selectAcStudent(id, name, email) {
|
||||
_acStudentId = id;
|
||||
document.getElementById('acf-student-search').value = name;
|
||||
document.getElementById('acf-student-selected').textContent = `${name} (${email})`;
|
||||
document.getElementById('acf-student-selected').style.display = '';
|
||||
closeAcStudentDrop();
|
||||
}
|
||||
|
||||
function setAcSrc(src) {
|
||||
_acSrc = src;
|
||||
document.querySelectorAll('[data-src]').forEach(b => b.classList.toggle('active', b.dataset.src === src));
|
||||
document.getElementById('acf-random-fields').style.display = src === 'random' ? '' : 'none';
|
||||
document.getElementById('acf-test-fields').style.display = src === 'test' ? '' : 'none';
|
||||
document.getElementById('acf-file-fields').style.display = src === 'file' ? '' : 'none';
|
||||
if (src === 'file' && !_acAllFiles) loadAcFiles();
|
||||
}
|
||||
|
||||
async function loadAcFiles() {
|
||||
try {
|
||||
_acAllFiles = await LS.getFiles();
|
||||
renderAcFiles('');
|
||||
} catch { _acAllFiles = []; }
|
||||
}
|
||||
|
||||
function renderAcFiles(q) {
|
||||
const el = document.getElementById('acf-file-list');
|
||||
if (!_acAllFiles) { el.innerHTML = '<div style="padding:10px;color:var(--text-3);font-size:.82rem;text-align:center">Загрузка…</div>'; return; }
|
||||
const lq = q.toLowerCase();
|
||||
const items = q ? _acAllFiles.filter(f => (f.title||'').toLowerCase().includes(lq)) : _acAllFiles;
|
||||
const SUBJ = { bio:'Биология', chem:'Химия', math:'Математика', phys:'Физика' };
|
||||
if (!items.length) { el.innerHTML = '<div style="padding:10px;color:var(--text-3);font-size:.82rem;text-align:center">Нет файлов</div>'; return; }
|
||||
el.innerHTML = items.map(f => `
|
||||
<div onclick="selectAcFile(${f.id},'${esc(f.title||'Файл')}','${f.subject_slug||''}')"
|
||||
style="padding:9px 12px;cursor:pointer;border-bottom:1px solid rgba(15,23,42,0.07);display:flex;align-items:center;gap:8px;${_acFileId===f.id?'background:rgba(155,93,229,0.08);':''} transition:background .15s">
|
||||
<div style="flex:1">
|
||||
<div style="font-size:.84rem;font-weight:600">${esc(f.title||'Файл')}</div>
|
||||
<div style="font-size:.74rem;color:var(--text-3)">${SUBJ[f.subject_slug]||f.subject_slug||''}</div>
|
||||
</div>
|
||||
${_acFileId===f.id ? '<span style="color:var(--violet)"><i data-lucide="check" style="width:15px;height:15px"></i></span>' : ''}
|
||||
</div>`).join('');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function filterAcFiles(q) { renderAcFiles(q); }
|
||||
|
||||
function selectAcFile(id, title, subject_slug) {
|
||||
_acFileId = id;
|
||||
renderAcFiles(document.getElementById('acf-file-search').value);
|
||||
const sel = document.getElementById('acf-file-selected');
|
||||
sel.textContent = 'Выбран: ' + title;
|
||||
sel.style.display = '';
|
||||
}
|
||||
|
||||
async function openCreateAModal() {
|
||||
_acSrc = 'random'; _acTarget = 'class'; _acFileId = null; _acStudentId = null; _acAllStudents = null;
|
||||
setAcSrc('random');
|
||||
setAcTarget('class');
|
||||
loadAcStudents();
|
||||
document.getElementById('acf-title').value = '';
|
||||
document.getElementById('acf-subject').value = '';
|
||||
document.getElementById('acf-mode').value = 'exam';
|
||||
document.getElementById('acf-mode-test').value = 'exam';
|
||||
document.getElementById('acf-count').value = '25';
|
||||
document.getElementById('acf-deadline').value = '';
|
||||
document.getElementById('acf-student-search').value = '';
|
||||
document.getElementById('acf-student-selected').style.display = 'none';
|
||||
_acStudentId = null;
|
||||
document.getElementById('acf-error').textContent = '';
|
||||
document.getElementById('acf-file-search').value = '';
|
||||
document.getElementById('acf-file-selected').style.display = 'none';
|
||||
|
||||
const [clsSel, testSel] = [document.getElementById('acf-class'), document.getElementById('acf-test')];
|
||||
clsSel.innerHTML = '<option value="">Загрузка…</option>';
|
||||
testSel.innerHTML = '<option value="">Загрузка…</option>';
|
||||
|
||||
const [classesP, testsP] = await Promise.allSettled([LS.getClasses(), LS.getTests()]);
|
||||
|
||||
if (classesP.status === 'fulfilled') {
|
||||
const classes = classesP.value;
|
||||
clsSel.innerHTML = classes.length
|
||||
? '<option value="">— выберите класс —</option>' + classes.map(c => `<option value="${c.id}">${esc(c.name)} (${c.member_count} уч.)</option>`).join('')
|
||||
: '<option value="">Нет классов — создайте класс</option>';
|
||||
} else {
|
||||
clsSel.innerHTML = `<option value="">Ошибка загрузки классов</option>`;
|
||||
}
|
||||
|
||||
if (testsP.status === 'fulfilled') {
|
||||
const tests = testsP.value;
|
||||
testSel.innerHTML = tests.length
|
||||
? '<option value="">— выберите тест —</option>' + tests.map(t => `<option value="${t.id}">${esc(t.title)} (${t.question_count} вопр.)</option>`).join('')
|
||||
: '<option value="">Нет тестов — создайте тест</option>';
|
||||
} else {
|
||||
testSel.innerHTML = `<option value="">Ошибка загрузки тестов</option>`;
|
||||
}
|
||||
|
||||
document.getElementById('ac-modal').classList.add('open');
|
||||
setTimeout(() => document.getElementById('acf-title').focus(), 80);
|
||||
}
|
||||
|
||||
function closeCreateAModal() {
|
||||
document.getElementById('ac-modal').classList.remove('open');
|
||||
}
|
||||
|
||||
async function saveNewAssignment() {
|
||||
const title = document.getElementById('acf-title').value.trim();
|
||||
const deadline = document.getElementById('acf-deadline').value || null;
|
||||
const errEl = document.getElementById('acf-error');
|
||||
errEl.textContent = '';
|
||||
if (!title) { errEl.textContent = 'Введите название'; return; }
|
||||
|
||||
let payload = { title, deadline };
|
||||
|
||||
if (_acSrc === 'file') {
|
||||
if (!_acFileId) { errEl.textContent = 'Выберите файл из библиотеки'; return; }
|
||||
const f = _acAllFiles.find(x => x.id === _acFileId);
|
||||
payload = { ...payload, file_id: _acFileId, subject_slug: f?.subject_slug || 'bio', mode: 'exam', count: 1 };
|
||||
} else if (_acSrc === 'test') {
|
||||
const test_id = document.getElementById('acf-test').value;
|
||||
const mode = document.getElementById('acf-mode-test').value;
|
||||
if (!test_id) { errEl.textContent = 'Выберите тест'; return; }
|
||||
const selOpt = document.querySelector(`#acf-test option[value="${test_id}"]`);
|
||||
if (selOpt && selOpt.textContent.includes('(0 вопр.)')) { errEl.textContent = 'В выбранном тесте нет вопросов. Добавьте вопросы во вкладке «Тесты».'; return; }
|
||||
payload = { ...payload, test_id: Number(test_id), mode };
|
||||
} else {
|
||||
const subject_slug = document.getElementById('acf-subject').value;
|
||||
const mode = document.getElementById('acf-mode').value;
|
||||
const count = Number(document.getElementById('acf-count').value);
|
||||
if (!subject_slug) { errEl.textContent = 'Выберите предмет'; return; }
|
||||
if (!count || count < 1) { errEl.textContent = 'Укажите количество вопросов'; return; }
|
||||
payload = { ...payload, subject_slug, mode, count };
|
||||
}
|
||||
|
||||
const btn = document.getElementById('acf-save');
|
||||
btn.disabled = true; btn.textContent = 'Создание…';
|
||||
try {
|
||||
if (_acTarget === 'user') {
|
||||
if (!_acStudentId) { errEl.textContent = 'Выберите ученика из списка'; btn.disabled=false; btn.textContent='Создать'; return; }
|
||||
await LS.createDirectAssignment({ ...payload, student_id: _acStudentId });
|
||||
} else {
|
||||
const class_id = document.getElementById('acf-class').value;
|
||||
if (!class_id) { errEl.textContent = 'Выберите класс'; btn.disabled=false; btn.textContent='Создать'; return; }
|
||||
await LS.createAssignment(class_id, payload);
|
||||
}
|
||||
closeCreateAModal();
|
||||
await load();
|
||||
} catch (e) {
|
||||
errEl.textContent = 'Ошибка: ' + e.message;
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = 'Создать';
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteAsgn(id) {
|
||||
const a = allAssignments.find(x => x.id === id);
|
||||
if (!await LS.confirm(`Удалить задание «${a?.title}»?\nВсе связанные сессии будут удалены.`, { title: 'Удалить задание', confirmText: 'Удалить' })) return;
|
||||
try {
|
||||
await LS.deleteAssignment(id);
|
||||
allAssignments = allAssignments.filter(x => x.id !== id);
|
||||
renderAssignments();
|
||||
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
}
|
||||
|
||||
// Expose handlers used by HTML/inline onclicks
|
||||
window.loadAssignments = load;
|
||||
window.renderAssignments = renderAssignments;
|
||||
window.setAFilter = setAFilter;
|
||||
window.setAfSrc = setAfSrc;
|
||||
window.openAModal = openAModal;
|
||||
window.closeAModal = closeAModal;
|
||||
window.saveAssignment = saveAssignment;
|
||||
window.setAcTarget = setAcTarget;
|
||||
window.filterAcStudents = filterAcStudents;
|
||||
window.openAcStudentDrop = openAcStudentDrop;
|
||||
window.closeAcStudentDrop = closeAcStudentDrop;
|
||||
window.selectAcStudent = selectAcStudent;
|
||||
window.setAcSrc = setAcSrc;
|
||||
window.filterAcFiles = filterAcFiles;
|
||||
window.selectAcFile = selectAcFile;
|
||||
window.openCreateAModal = openCreateAModal;
|
||||
window.closeCreateAModal = closeCreateAModal;
|
||||
window.saveNewAssignment = saveNewAssignment;
|
||||
window.deleteAsgn = deleteAsgn;
|
||||
|
||||
window.AdminSections = window.AdminSections || {};
|
||||
window.AdminSections.assignments = {
|
||||
init: async () => { if (inited) return; inited = true; await load(); },
|
||||
reload: load,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user