From 6a874a341ddced79de80eaea40f76bc9ed7406eb Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Wed, 3 Jun 2026 13:43:49 +0300 Subject: [PATCH] =?UTF-8?q?feat(access):=20=D0=A4=D0=B0=D0=B7=D0=B0=202c?= =?UTF-8?q?=20=E2=80=94=20=C2=AB=D0=9E=D1=82=D0=BA=D1=80=D1=8B=D1=82=D1=8C?= =?UTF-8?q?=20=D0=B2=D0=B5=D1=81=D1=8C=20=D0=BF=D1=80=D0=B5=D0=B4=D0=BC?= =?UTF-8?q?=D0=B5=D1=82=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D1=83=C2=BB=20?= =?UTF-8?q?=D0=B2=20=D1=80=D0=B5=D0=B6=D0=B8=D0=BC=D0=B5=20=C2=AB=D0=9F?= =?UTF-8?q?=D0=BE=20=D0=BA=D0=BB=D0=B0=D1=81=D1=81=D1=83=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Панель кнопок по предметам: один клик открывает выбранному классу весь контент этого предмета (учебники/экзамены/симуляции/курсы вместе). Нормализация поля предмета (subject|subject_slug), метки через SUBJ_LABEL. Чистый фронтенд на существующем accSetRule. Закрывает находку ревью «нет операции открыть весь предмет». Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/admin/sections/access.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/frontend/js/admin/sections/access.js b/frontend/js/admin/sections/access.js index 2e1c955..98d65ce 100644 --- a/frontend/js/admin/sections/access.js +++ b/frontend/js/admin/sections/access.js @@ -42,6 +42,8 @@ const bucket = (type) => BUCKET[type] || (type + 's'); const keyName = (type) => KEYNAME[type] || 'id'; const itemsOf = (type) => (_catalog && _catalog[bucket(type)]) || []; + const subjOf = (it) => it.subject || it.subject_slug || ''; // нормализация поля предмета + const subjLabel = (s) => SUBJ_LABEL[s] || s || 'Прочее'; function contentTitle(type, ref) { const it = itemsOf(type).find(x => x[keyName(type)] === ref); return it ? it.title : ref; @@ -346,10 +348,17 @@ function renderClassDetail(right) { let html = `
Класс «${esc(_selClass.name)}»
-
+
`; + const subjects = [...new Set(CONTENT_TYPES.flatMap(t => itemsOf(t).map(subjOf)).filter(Boolean))].sort(); + if (subjects.length) { + html += `
+ Открыть по предмету: + ${subjects.map(s => ``).join('')} +
`; + } CONTENT_TYPES.forEach(type => { const items = itemsOf(type); html += `
${TYPE_LABEL[type]}
`; @@ -376,6 +385,18 @@ } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); selClass(_selClass.id); } } + /* открыть классу весь контент одного предмета (любого типа) */ + async function classSubjectBulk(subj) { + const items = CONTENT_TYPES.flatMap(t => itemsOf(t).filter(it => subjOf(it) === subj).map(it => [t, it[keyName(t)]])); + if (!items.length) return; + try { + await Promise.all(items.map(([t, ref]) => LS.accessSetRule(t, ref, 'class', _selClass.id, 1))); + items.forEach(([t, ref]) => { const set = _classOpen[bucket(t)]; if (set && !set.has(ref)) { set.add(ref); bumpSummary(t, ref, +1); } }); + renderRight(); + LS.toast(`Открыт весь контент по предмету «${subjLabel(subj)}»`, 'success'); + } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); selClass(_selClass.id); } + } + async function classBulk(allow) { if (!allow && !confirm(`Закрыть весь контент у класса «${_selClass.name}»?`)) return; const all = CONTENT_TYPES.flatMap(t => itemsOf(t).map(it => [t, it[keyName(t)]])); @@ -510,6 +531,7 @@ window.accSelClass = selClass; window.accClassToggle = classToggle; window.accClassBulk = classBulk; + window.accClassSubj = classSubjectBulk; window.accMx = mxToggle; window.accMxSearch = mxSearch; window.accMxRowBulk = mxRowBulk;