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;