diff --git a/frontend/admin.html b/frontend/admin.html index f54b78b..50496a3 100644 --- a/frontend/admin.html +++ b/frontend/admin.html @@ -486,6 +486,13 @@ .tst-search { width: 100%; padding: 7px 12px; border: 1.5px solid var(--border-h); border-radius: 8px; font-family: 'Manrope', sans-serif; font-size: 0.83rem; background: #fff; color: var(--text); margin-bottom: 8px; outline: none; } .tst-search:focus { border-color: var(--violet); } .tst-empty { text-align: center; padding: 20px; color: var(--text-3); font-size: 0.82rem; } + .tst-pick-filters { display: flex; gap: 8px; margin-bottom: 8px; } + .tst-pick-sel { flex: 1; min-width: 0; padding: 6px 10px; border: 1.5px solid var(--border-h); border-radius: 8px; font-family: 'Manrope', sans-serif; font-size: 0.78rem; background: #fff; color: var(--text); cursor: pointer; outline: none; } + .tst-pick-sel:focus { border-color: var(--violet); } + .tst-pick-foot { display: flex; align-items: center; justify-content: space-between; gap: 10px; margin-top: 10px; min-height: 24px; } + .tst-pick-count { font-size: 0.74rem; color: var(--text-3); } + .btn-tst-more { padding: 6px 14px; border: 1.5px solid var(--violet); border-radius: 8px; background: rgba(155,93,229,0.06); color: var(--violet); font-family: 'Manrope', sans-serif; font-size: 0.78rem; font-weight: 700; cursor: pointer; transition: background var(--tr); } + .btn-tst-more:hover { background: rgba(155,93,229,0.14); } .src-toggle { display: flex; gap: 6px; flex-wrap: wrap; margin-bottom: 16px; } /* formula bar */ /* Formula bar: hidden by default, toggled via #qf-fml-toggle */ diff --git a/frontend/js/admin/sections/tests.js b/frontend/js/admin/sections/tests.js index 651162a..0c3dd89 100644 --- a/frontend/js/admin/sections/tests.js +++ b/frontend/js/admin/sections/tests.js @@ -79,16 +79,15 @@ if (!inner) return; inner.innerHTML = '
'; try { - const [t, subjectQs] = await Promise.all([ - LS.getTest(id), - LS.getQuestions( - (_tstPickerCache[id]?.subject_slug) || allTests.find(x => x.id === id)?.subject_slug || '', - null, 'date_asc' - ).catch(() => []), - ]); - + const t = await LS.getTest(id); const inIds = new Set(t.questions.map(q => q.id)); - _tstPickerCache[id] = { subjectQs, inIds, subject_slug: t.subject_slug }; + // Сохраняем поиск/фильтры между перерисовками (напр. после добавления вопроса). + const prev = _tstPickerCache[id] || {}; + _tstPickerCache[id] = { + subject_slug: t.subject_slug, inIds, + q: prev.q || '', difficulty: prev.difficulty || '', type: prev.type || '', + rows: [], total: 0, page: 1, loading: false, + }; inner.innerHTML = `
@@ -98,17 +97,89 @@
Добавить вопросы
- -
${renderTstPicker(subjectQs, inIds, id)}
+ +
+ + +
+
+
`; + // restore search/filters into controls + const si = document.getElementById('tstps-' + id); if (si) si.value = _tstPickerCache[id].q; + const fd = document.getElementById('tstfd-' + id); if (fd) fd.value = _tstPickerCache[id].difficulty; + const ft = document.getElementById('tstft-' + id); if (ft) ft.value = _tstPickerCache[id].type; AdminCtx.renderMath(inner); if (window.lucide) lucide.createIcons(); + await pickerLoad(id, true); } catch (e) { inner.innerHTML = `
Ошибка: ${esc(e.message)}
`; } } + /* Серверная подгрузка вопросов в пикер (весь банк предмета, не первые 100). + reset=true — новый поиск/фильтр (страница 1, заменяем); иначе — «показать ещё». */ + async function pickerLoad(id, reset) { + const cache = _tstPickerCache[id]; + if (!cache || cache.loading) return; + cache.loading = true; + if (reset) { cache.page = 1; cache.rows = []; } + const listEl = document.getElementById('tstpicker-' + id); + const footEl = document.getElementById('tstfoot-' + id); + if (reset && listEl) listEl.innerHTML = '
'; + try { + const p = new URLSearchParams(); + p.set('subject', cache.subject_slug || ''); + p.set('sort', 'date_desc'); + p.set('page', cache.page); + p.set('limit', 100); + if (cache.q) p.set('q', cache.q); + if (cache.difficulty) p.set('difficulty', cache.difficulty); + if (cache.type) p.set('type', cache.type); + const data = await LS.get('/api/questions?' + p.toString()); + const rows = Array.isArray(data) ? data : (data.rows || []); + cache.total = Array.isArray(data) ? rows.length : (data.total != null ? data.total : rows.length); + cache.rows = reset ? rows : cache.rows.concat(rows); + cache.page += 1; + if (listEl) { listEl.innerHTML = renderTstPicker(cache.rows, cache.inIds, id); AdminCtx.renderMath(listEl); } + if (footEl) footEl.innerHTML = pickerFootHtml(id); + if (window.lucide) lucide.createIcons(); + } catch (e) { + if (listEl) listEl.innerHTML = `
Ошибка: ${esc(e.message)}
`; + } finally { cache.loading = false; } + } + + function pickerFootHtml(id) { + const c = _tstPickerCache[id]; + if (!c || !c.total) return ''; + const more = c.rows.length < c.total + ? `` : ''; + return `Показано ${c.rows.length} из ${c.total}${more}`; + } + + function pickerMore(id) { pickerLoad(id, false); } + + function pickerFilterChange(id) { + const c = _tstPickerCache[id]; + if (!c) return; + c.difficulty = document.getElementById('tstfd-' + id)?.value || ''; + c.type = document.getElementById('tstft-' + id)?.value || ''; + pickerLoad(id, true); + } + function renderTstQList(questions, tid) { const { DIFF_LABELS, qTypeBadge, qOptsPreview } = AdminCtx; if (!questions.length) return '
Вопросов нет. Добавьте справа
'; @@ -129,7 +200,11 @@ function renderTstPicker(questions, inIds, tid) { const { DIFF_LABELS, qTypeBadge, qOptsPreview } = AdminCtx; - if (!questions.length) return '
Вопросов нет в этом предмете
'; + if (!questions.length) { + const c = _tstPickerCache[tid] || {}; + const searching = c.q || c.difficulty || c.type; + return `
${searching ? 'Ничего не найдено — измените запрос или фильтры' : 'Вопросов нет в этом предмете'}
`; + } return questions.map(q => { const added = inIds.has(q.id); return `
@@ -147,15 +222,13 @@ }).join(''); } - async function filterTstPicker(tid) { - const search = document.getElementById('tstps-'+tid)?.value.toLowerCase() || ''; - const cache = _tstPickerCache[tid]; + const _pickDebounce = {}; + function filterTstPicker(tid) { + const cache = _tstPickerCache[tid]; if (!cache) return; - const filtered = search - ? cache.subjectQs.filter(q => q.text.toLowerCase().includes(search)) - : cache.subjectQs; - const picker = document.getElementById('tstpicker-'+tid); - if (picker) { picker.innerHTML = renderTstPicker(filtered, cache.inIds, tid); AdminCtx.renderMath(picker); if(window.lucide)lucide.createIcons(); } + cache.q = (document.getElementById('tstps-' + tid)?.value || '').trim(); + clearTimeout(_pickDebounce[tid]); + _pickDebounce[tid] = setTimeout(() => pickerLoad(tid, true), 300); // серверный поиск по всему банку } async function tstAddQ(tid, qid) { @@ -282,6 +355,8 @@ window.renderTests = renderTests; window.toggleTstDrawer = toggleTstDrawer; window.filterTstPicker = filterTstPicker; + window.pickerMore = pickerMore; + window.pickerFilterChange = pickerFilterChange; window.tstAddQ = tstAddQ; window.tstRemoveQ = tstRemoveQ; window.setTstShowAnswers = setTstShowAnswers;