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 @@
-
-
-
-
Назначить чтение
-
-
-
-
-
-
@@ -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 = `
+ `;
- 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');
+ });
};
/* ════════════════════════════════════════════════