feat(access): Фаза 2c — «Открыть весь предмет классу» в режиме «По классу»

Панель кнопок по предметам: один клик открывает выбранному классу весь контент
этого предмета (учебники/экзамены/симуляции/курсы вместе). Нормализация поля
предмета (subject|subject_slug), метки через SUBJ_LABEL. Чистый фронтенд на
существующем accSetRule. Закрывает находку ревью «нет операции открыть весь предмет».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-03 13:43:49 +03:00
parent 8467d7202a
commit 6a874a341d
+23 -1
View File
@@ -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 = `
<div style="font-size:16px;font-weight:700;color:var(--text-1);margin-bottom:14px">Класс «${esc(_selClass.name)}»</div>
<div style="display:flex;gap:8px;margin-bottom:14px">
<div style="display:flex;gap:8px;margin-bottom:10px">
<button class="adm-btn adm-btn-small" onclick="accClassBulk(1)">Открыть весь контент</button>
<button class="adm-btn adm-btn-small" style="background:var(--border-h);color:var(--text-3)" onclick="accClassBulk(0)">Закрыть весь</button>
</div>`;
const subjects = [...new Set(CONTENT_TYPES.flatMap(t => itemsOf(t).map(subjOf)).filter(Boolean))].sort();
if (subjects.length) {
html += `<div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:14px">
<span style="font-size:12px;color:var(--muted)">Открыть по предмету:</span>
${subjects.map(s => `<button class="adm-btn adm-btn-small" style="background:var(--accent-soft,#eef2ff);color:var(--accent,#4f46e5)" onclick="accClassSubj('${esc(s)}')">+ ${esc(subjLabel(s))}</button>`).join('')}
</div>`;
}
CONTENT_TYPES.forEach(type => {
const items = itemsOf(type);
html += `<div style="font-weight:600;font-size:13px;color:var(--text-3);margin:14px 0 8px">${TYPE_LABEL[type]}</div>`;
@@ -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;