'use strict'; /* admin → sims (simulations) section — контент-движок, Фазы 4-5. * * Каталог берётся из БД (/api/lab/sims), а НЕ из захардкоженного списка. * Управление: вкл/выкл (зеркалится в legacy sim_disabled_ids), «рекомендуемая», * курикулумные связи (Фаза 5). Мастер-тумблер модуля — /api/settings/sims. */ (function () { 'use strict'; let inited = false; const CAT_LABEL = { math: 'Математика', phys: 'Физика', chem: 'Химия', bio: 'Биология', game: 'Игры' }; const CAT_ORDER = ['math', 'phys', 'chem', 'bio', 'game']; let _moduleDisabled = false; let _sims = []; // [{id,cat,title,enabled,featured,tags,subject,grade,sort}] let _textbooks = null; // кэш каталога учебников для выпадающего списка связей function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } async function load() { try { const data = await LS.api('/api/lab/sims'); _moduleDisabled = !!data.module_disabled; _sims = Array.isArray(data.sims) ? data.sims : []; _render(); } catch (e) { LS.toast('Ошибка загрузки симуляций: ' + e.message, 'error'); } } function _render() { const masterChk = document.getElementById('sims-master-chk'); if (masterChk) masterChk.checked = !_moduleDisabled; const grid = document.getElementById('sims-grid'); if (!grid) return; // group by category, preserving catalogue sort within group const byCat = {}; _sims.forEach(s => { (byCat[s.cat] = byCat[s.cat] || []).push(s); }); const cats = CAT_ORDER.filter(c => byCat[c]).concat( Object.keys(byCat).filter(c => !CAT_ORDER.includes(c))); let html = ''; cats.forEach(cat => { html += `
${esc(CAT_LABEL[cat] || cat)}
`; byCat[cat].forEach(s => { const tags = (s.tags || []).map(t => esc(t)).join(', '); html += `
${esc(s.title)}
${esc(s.id)}${tags ? ' · ' + tags : ''}
`; }); }); grid.innerHTML = html; if (window.lucide) lucide.createIcons(); } async function simsMasterToggle(checked) { try { await LS.api('/api/settings/sims', { method: 'PUT', body: JSON.stringify({ module_disabled: !checked }) }); _moduleDisabled = !checked; LS.toast(checked ? 'Модуль симуляций включён' : 'Модуль симуляций отключён', checked ? 'success' : 'warning'); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } } async function simToggleOne(simId, enabled) { try { await LS.api('/api/lab/sims/' + encodeURIComponent(simId), { method: 'PATCH', body: JSON.stringify({ enabled }) }); const s = _sims.find(x => x.id === simId); if (s) s.enabled = enabled; const card = document.getElementById('simcard-' + simId); if (card) card.classList.toggle('enabled', enabled); LS.toast(enabled ? `«${simId}» включена` : `«${simId}» отключена`, enabled ? 'success' : 'warning'); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } } async function simToggleFeatured(simId, featured) { try { await LS.api('/api/lab/sims/' + encodeURIComponent(simId), { method: 'PATCH', body: JSON.stringify({ featured }) }); const s = _sims.find(x => x.id === simId); if (s) s.featured = featured; _render(); LS.toast(featured ? `«${simId}» в рекомендуемых` : `«${simId}» убрана из рекомендуемых`, 'success'); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } } /* ── Фаза 5: редактор курикулумных связей (inline-панель под карточкой) ── */ async function _ensureTextbooks() { if (_textbooks) return _textbooks; try { const data = await LS.api('/api/access/catalog'); _textbooks = (data && data.textbooks) || []; } catch (e) { _textbooks = []; } return _textbooks; } async function simToggleLinks(simId) { const panel = document.getElementById('simlinks-' + simId); if (!panel) return; if (panel.style.display !== 'none') { panel.style.display = 'none'; return; } panel.style.display = 'block'; panel.innerHTML = '
Загрузка связей…
'; try { const [rel] = await Promise.all([ LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related'), _ensureTextbooks(), ]); _renderLinksPanel(simId, rel); } catch (e) { panel.innerHTML = '
Ошибка: ' + esc(e.message) + '
'; } } function _renderLinksPanel(simId, rel) { const panel = document.getElementById('simlinks-' + simId); if (!panel) return; const links = (rel && rel.links) || {}; const tb = links.textbook || []; let html = '
Связи с учебниками
'; if (tb.length) { html += '
'; tb.forEach(l => { html += ` ${esc(l.label || l.ref_id)} `; }); html += '
'; } else { html += '
Пока нет связей
'; } // add-form: textbook ${opts} `; panel.innerHTML = html; } async function simAddLink(simId) { const sel = document.getElementById('simlink-sel-' + simId); const slug = sel && sel.value; if (!slug) { LS.toast('Выберите учебник', 'warning'); return; } try { await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/links', { method: 'POST', body: JSON.stringify({ kind: 'textbook', ref_id: slug }) }); LS.toast('Связь добавлена', 'success'); const rel = await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related'); _renderLinksPanel(simId, rel); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } } async function simDelLink(simId, linkId) { try { await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/links/' + linkId, { method: 'DELETE' }); LS.toast('Связь удалена', 'success'); const rel = await LS.api('/api/lab/sims/' + encodeURIComponent(simId) + '/related'); _renderLinksPanel(simId, rel); } catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); } } window.simsMasterToggle = simsMasterToggle; window.simToggleOne = simToggleOne; window.simToggleFeatured = simToggleFeatured; window.simToggleLinks = simToggleLinks; window.simAddLink = simAddLink; window.simDelLink = simDelLink; window.AdminSections = window.AdminSections || {}; window.AdminSections.sims = { init: async () => { if (inited) return; inited = true; await load(); }, reload: load, }; })();