diff --git a/frontend/textbooks.html b/frontend/textbooks.html index 06ac793..39e4216 100644 --- a/frontend/textbooks.html +++ b/frontend/textbooks.html @@ -255,30 +255,6 @@ .am-saved.show { opacity:1; } /* ── Assign modal (reused styling from exam9) ── */ - .ex-overlay { - display:none; position:fixed; inset:0; - background:rgba(15,23,42,.55); z-index:300; - align-items:flex-start; justify-content:center; padding-top:80px; - backdrop-filter:blur(2px); - } - .ex-overlay.visible { display:flex; } - .ex-panel { - background:var(--surface); border:1.5px solid var(--border); - border-radius:16px; box-shadow:0 24px 64px rgba(0,0,0,.32); - width:min(520px, 94vw); max-height:calc(100vh - 120px); - overflow-y:auto; padding:22px 22px 26px; - } - .ex-panel-head { - display:flex; align-items:center; justify-content:space-between; margin-bottom:18px; - } - .ex-panel-head h2 { font-family:'Unbounded',sans-serif; font-size:1rem; font-weight:800; } - .ex-panel-close { - width:32px; height:32px; border:none; background:none; - color:var(--text-2); cursor:pointer; border-radius:8px; - display:flex; align-items:center; justify-content:center; transition:background .15s; - } - .ex-panel-close:hover { background:var(--border); color:var(--text); } - .ex-panel-close svg { width:18px; height:18px; } .ax-form { display:flex; flex-direction:column; gap:14px; } .ax-field label { display:block; font-size:.78rem; font-weight:700; color:var(--text-2); @@ -304,22 +280,6 @@ } .ax-input:focus { outline:none; border-color:var(--violet); } .ax-hint { font-size:.74rem; color:var(--text-3); margin-top:4px; } - .ax-actions { display:flex; gap:10px; justify-content:flex-end; margin-top:6px; } - .ax-btn { - padding:9px 18px; border-radius:10px; border:1.5px solid var(--border-h); - background:transparent; color:var(--text); - font-family:'Manrope',sans-serif; font-size:.88rem; font-weight:700; - cursor:pointer; transition:all .15s; - } - .ax-btn:hover { border-color:var(--text-2); } - .ax-btn-primary { background:var(--violet); border-color:var(--violet); color:#fff; } - .ax-btn-primary:hover { background:#7e3eca; border-color:#7e3eca; } - .ax-btn-primary:disabled { opacity:.5; cursor:not-allowed; } - .ax-error, .ax-success { - padding:9px 12px; border-radius:8px; font-size:.84rem; display:none; - } - .ax-error.visible { display:block; background:rgba(241,91,68,.1); border:1px solid rgba(241,91,68,.3); color:#F94144; } - .ax-success.visible { display:block; background:rgba(6,214,160,.1); border:1px solid rgba(6,214,160,.3); color:#06D6A0; } .ax-tabs { display:flex; gap:6px; background:var(--border); padding:4px; border-radius:10px; } .ax-tab { @@ -419,51 +379,6 @@ -
-
-
-

Назначить чтение

- -
-
-
- -
- - -
-
-
- -
Загрузка…
-
- -
- - -
Диапазон («15-18») или список через запятую («1,3,5»). Пустое = весь учебник.
-
-
- - -
-
-
-
- - -
-
-
-
- @@ -573,178 +488,165 @@ if (window.lucide) lucide.createIcons(); } - /* ── Assign modal ── */ - let assignSlug = null; - let assignTitle = null; - let assignTab = 'class'; // 'class' or 'student' - let teacherStudents = null; // cached list of students-in-teacher's-classes + /* ── Assign modal (via LS.modal) ── */ + let teacherStudents = null; async function loadTeacherClasses() { if (teacherClasses) return teacherClasses; - try { - const list = await LS.api('/api/classes'); - teacherClasses = Array.isArray(list) ? list : []; - } catch { teacherClasses = []; } + try { const list = await LS.api('/api/classes'); teacherClasses = Array.isArray(list) ? list : []; } + catch { teacherClasses = []; } return teacherClasses; } async function loadTeacherStudents() { if (teacherStudents) return teacherStudents; - try { - const r = await LS.api('/api/classes/students'); - teacherStudents = Array.isArray(r) ? r : (r.students || []); - } catch { teacherStudents = []; } + try { const r = await LS.api('/api/classes/students'); teacherStudents = Array.isArray(r) ? r : (r.students || []); } + catch { teacherStudents = []; } return teacherStudents; } - window.setAssignTab = function (tab) { - assignTab = tab; - document.querySelectorAll('.ax-tab').forEach(t => t.classList.toggle('active', t.dataset.tab === tab)); - document.getElementById('ax-class-field').style.display = tab === 'class' ? '' : 'none'; - document.getElementById('ax-student-field').style.display = tab === 'student' ? '' : 'none'; - document.getElementById('ax-student-id').value = ''; - document.getElementById('ax-student-search').value = ''; - document.getElementById('ax-student-results').classList.remove('visible'); - }; - window.openAssignModal = async function (slug, title) { - assignSlug = slug; - assignTitle = title; - document.getElementById('assign-title').textContent = `Назначить чтение: «${title}»`; - ['ax-error', 'ax-success'].forEach(id => document.getElementById(id).classList.remove('visible')); - document.getElementById('ax-paragraphs').value = ''; - document.getElementById('ax-deadline').value = ''; - document.getElementById('ax-submit').disabled = false; - document.getElementById('ax-submit').textContent = 'Назначить'; - setAssignTab('class'); - - const listEl = document.getElementById('ax-classes-list'); - listEl.textContent = 'Загрузка…'; const classes = await loadTeacherClasses(); - if (!classes.length) { - listEl.innerHTML = '
У вас пока нет классов
'; - } else { - listEl.innerHTML = classes.map(c => ` - `).join(''); - } + const classesHtml = classes.length + ? classes.map(c => ` + `).join('') + : '
У вас пока нет классов
'; - document.getElementById('assign-overlay').classList.add('visible'); - document.addEventListener('keydown', onAssignEsc); - }; + const body = ` +
+
+ +
+ + +
+
+
+ +
${classesHtml}
+
+ +
+ + +
Диапазон («15-18») или список через запятую («1,3,5»). Пустое = весь учебник.
+
+
+ + +
+
`; - window.closeAssignModal = function () { - document.getElementById('assign-overlay').classList.remove('visible'); - document.removeEventListener('keydown', onAssignEsc); - }; - - window.onAssignOverlayClick = function (e) { - if (e.target === document.getElementById('assign-overlay')) closeAssignModal(); - }; - function onAssignEsc(e) { if (e.key === 'Escape') closeAssignModal(); } - - /* Student search (debounced) */ - let stSearchTimer = null; - document.addEventListener('input', e => { - if (e.target?.id !== 'ax-student-search') return; - clearTimeout(stSearchTimer); - stSearchTimer = setTimeout(() => filterStudents(e.target.value), 200); - }); - - async function filterStudents(q) { - const resultsEl = document.getElementById('ax-student-results'); - q = q.trim().toLowerCase(); - if (q.length < 2) { resultsEl.classList.remove('visible'); return; } - const students = await loadTeacherStudents(); - const matches = students.filter(s => - (s.name && s.name.toLowerCase().includes(q)) || - (s.email && s.email.toLowerCase().includes(q)) - ).slice(0, 12); - if (!matches.length) { - resultsEl.innerHTML = '
Не найдено
'; - } else { - resultsEl.innerHTML = matches.map(s => ` -
- ${esc(s.name)} - ${esc(s.email || '')} -
`).join(''); - } - resultsEl.classList.add('visible'); - } - - document.addEventListener('click', e => { - const row = e.target.closest('.ax-student-row'); - if (!row || !row.dataset.id) return; - document.querySelectorAll('.ax-student-row').forEach(r => r.classList.remove('selected')); - row.classList.add('selected'); - document.getElementById('ax-student-id').value = row.dataset.id; - document.getElementById('ax-student-search').value = row.dataset.name; - document.getElementById('ax-student-results').classList.remove('visible'); - }); - - window.submitAssign = async function () { - const errorEl = document.getElementById('ax-error'); - const successEl = document.getElementById('ax-success'); - const submitBtn = document.getElementById('ax-submit'); - errorEl.classList.remove('visible'); - successEl.classList.remove('visible'); - - const paragraphs = document.getElementById('ax-paragraphs').value.trim(); - const deadline = document.getElementById('ax-deadline').value || null; - const titleSuffix = paragraphs ? ` (§${paragraphs})` : ''; - - submitBtn.disabled = true; - submitBtn.textContent = 'Назначаю…'; - - try { - let resultMsg; - if (assignTab === 'class') { - const checked = [...document.querySelectorAll('#ax-classes-list input[name="cls"]:checked')] - .map(el => Number(el.value)); - if (!checked.length) throw new Error('Выберите хотя бы один класс'); - - const r = await LS.api('/api/assignments/bulk', { - method: 'POST', - body: { - title: `Учебник: ${assignTitle}${titleSuffix}`, - class_ids: checked, - mode: 'exam', count: 1, subject_slug: 'other', is_homework: 1, - deadline, - textbook_slug: assignSlug, - textbook_paragraphs: paragraphs || null, + let currentTab = 'class'; + const m = LS.modal({ + title: `Назначить чтение: «${title}»`, + content: body, + size: 'sm', + actions: [ + { label: 'Отмена', onClick: () => m.close() }, + { + label: 'Назначить', primary: true, + onClick: async () => { + const f = m.body.querySelector('form'); + const paragraphs = f['paragraphs'].value.trim(); + const deadline = f['deadline'].value || null; + const titleSuffix = paragraphs ? ` (§${paragraphs})` : ''; + const btns = m.root.querySelectorAll('.ls-mod-btn'); + btns.forEach(b => b.disabled = true); + btns[1].textContent = 'Назначаю…'; + try { + if (currentTab === 'class') { + const checked = [...f.querySelectorAll('input[name="cls"]:checked')].map(el => Number(el.value)); + if (!checked.length) throw new Error('Выберите хотя бы один класс'); + const r = await LS.api('/api/assignments/bulk', { + method: 'POST', + body: { + title: `Учебник: ${title}${titleSuffix}`, + class_ids: checked, + mode: 'exam', count: 1, subject_slug: 'other', is_homework: 1, + deadline, textbook_slug: slug, textbook_paragraphs: paragraphs || null, + }, + }); + LS.toast(`Назначено в ${r.count || checked.length} класс(ах)`, 'success'); + } else { + const studentId = Number(f['student-id'].value); + if (!studentId) throw new Error('Выберите ученика'); + await LS.api('/api/assignments', { + method: 'POST', + body: { + title: `Учебник: ${title}${titleSuffix}`, + student_id: studentId, + mode: 'exam', count: 1, subject_slug: 'other', is_homework: 1, + deadline, textbook_slug: slug, textbook_paragraphs: paragraphs || null, + }, + }); + LS.toast('Личное задание создано', 'success'); + } + m.close(); + } catch (e) { + m.setError(e.message || 'Не удалось создать задание'); + btns.forEach(b => b.disabled = false); + btns[1].textContent = 'Назначить'; + } }, - }); - resultMsg = `Назначено в ${r.count || checked.length} класс(е/ах)`; - } else { - const studentId = Number(document.getElementById('ax-student-id').value); - if (!studentId) throw new Error('Выберите ученика'); + }, + ], + }); - await LS.api('/api/assignments', { - method: 'POST', - body: { - title: `Учебник: ${assignTitle}${titleSuffix}`, - student_id: studentId, - mode: 'exam', count: 1, subject_slug: 'other', is_homework: 1, - deadline, - textbook_slug: assignSlug, - textbook_paragraphs: paragraphs || null, - }, - }); - resultMsg = 'Личное задание создано'; - } - successEl.textContent = resultMsg; - successEl.classList.add('visible'); - submitBtn.textContent = 'Готово'; - setTimeout(closeAssignModal, 1500); - } catch (e) { - errorEl.textContent = e.message || 'Не удалось создать задание'; - errorEl.classList.add('visible'); - submitBtn.disabled = false; - submitBtn.textContent = 'Назначить'; - } + // Tab switching within modal + m.body.querySelectorAll('.ax-tab').forEach(tab => { + tab.addEventListener('click', () => { + currentTab = tab.dataset.tab; + m.body.querySelectorAll('.ax-tab').forEach(t => t.classList.toggle('active', t === tab)); + m.body.querySelectorAll('[data-pane]').forEach(p => p.style.display = p.dataset.pane === currentTab ? '' : 'none'); + }); + }); + + // Student search (debounced, scoped to this modal) + let stTimer = null; + const searchInput = m.body.querySelector('input[name="student-search"]'); + const resultsEl = m.body.querySelector('.ax-student-results'); + const idInput = m.body.querySelector('input[name="student-id"]'); + + searchInput.addEventListener('input', () => { + clearTimeout(stTimer); + stTimer = setTimeout(async () => { + const q = searchInput.value.trim().toLowerCase(); + if (q.length < 2) { resultsEl.classList.remove('visible'); return; } + const students = await loadTeacherStudents(); + const matches = students.filter(s => + (s.name && s.name.toLowerCase().includes(q)) || + (s.email && s.email.toLowerCase().includes(q)) + ).slice(0, 12); + resultsEl.innerHTML = matches.length + ? matches.map(s => ` +
+ ${esc(s.name)} + ${esc(s.email || '')} +
`).join('') + : '
Не найдено
'; + resultsEl.classList.add('visible'); + }, 200); + }); + + resultsEl.addEventListener('click', e => { + const row = e.target.closest('.ax-student-row'); + if (!row || !row.dataset.id) return; + m.body.querySelectorAll('.ax-student-row').forEach(r => r.classList.remove('selected')); + row.classList.add('selected'); + idInput.value = row.dataset.id; + searchInput.value = row.dataset.name; + resultsEl.classList.remove('visible'); + }); }; /* ════════════════════════════════════════════════