feat(admin): phase 2 — split admin.js into 13 section modules
Replace ~3500L admin.js monolith with thin orchestrator (~700L) + 14 IIFE-wrapped per-section modules under /js/admin/sections/. Section modules expose AdminSections.<name>.init/reload (lazy init via switchTab/router) and re-expose onclick handlers via window.X for backward compat. Shared helpers (MODES/DIFFS, fmtDate, pctClass, renderMath, qTypeBadge, pagination) live in /js/admin/_shared.js exposed on window.AdminCtx. switchTab now dispatches to AdminSections via ROUTE_TO_SECTION map; non-extracted system tabs (topics/audit/errors/health/classroom/avatars) remain inline in admin.js. user-panel overlay markup untouched — Phase 6 will remove it.
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
'use strict';
|
||||
/* admin → shop section: items + purchases + award coins */
|
||||
(function () {
|
||||
'use strict';
|
||||
let inited = false;
|
||||
let _shopItems = [];
|
||||
let _shopEditId = null;
|
||||
let _shopSaving = false;
|
||||
let _shopSearchTimer = null;
|
||||
let _coinsAwarding = false;
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const [stats, items] = await Promise.all([
|
||||
LS.adminShopStats(),
|
||||
LS.adminShopGetItems()
|
||||
]);
|
||||
const topName = stats.topItems?.[0]?.name || '—';
|
||||
document.getElementById('shop-stats-grid').innerHTML = `
|
||||
<div class="stat-card" style="--stat-top:var(--violet)">
|
||||
<div class="stat-card-icon" style="background:rgba(155,93,229,0.1)"><i data-lucide="shopping-bag" class="stat-icon"></i></div>
|
||||
<div class="stat-val violet">${stats.activeItems}/${stats.totalItems}</div>
|
||||
<div class="stat-label">Товаров</div>
|
||||
</div>
|
||||
<div class="stat-card" style="--stat-top:var(--cyan)">
|
||||
<div class="stat-card-icon" style="background:rgba(6,214,224,0.1)"><i data-lucide="receipt" class="stat-icon"></i></div>
|
||||
<div class="stat-val cyan">${stats.totalPurchases}</div>
|
||||
<div class="stat-label">Покупок</div>
|
||||
</div>
|
||||
<div class="stat-card" style="--stat-top:var(--green)">
|
||||
<div class="stat-card-icon" style="background:rgba(6,214,100,0.1)"><i data-lucide="coins" class="stat-icon"></i></div>
|
||||
<div class="stat-val green">${stats.totalCoinsInCirculation}</div>
|
||||
<div class="stat-label">Монет в обороте</div>
|
||||
</div>
|
||||
<div class="stat-card" style="--stat-top:var(--amber, #FFB347)">
|
||||
<div class="stat-card-icon" style="background:rgba(255,179,71,0.1)"><i data-lucide="star" class="stat-icon"></i></div>
|
||||
<div class="stat-val" style="color:var(--amber, #FFB347);font-size:1.1rem">${esc(topName)}</div>
|
||||
<div class="stat-label">Топ товар</div>
|
||||
</div>`;
|
||||
_shopItems = items;
|
||||
renderShopItems();
|
||||
if (window.lucide) lucide.createIcons();
|
||||
} catch(e) {
|
||||
document.getElementById('shop-stats-grid').innerHTML = `<div class="error">Ошибка: ${esc(e.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderShopItems() {
|
||||
const body = document.getElementById('shop-items-body');
|
||||
if (!_shopItems.length) { body.innerHTML = '<tr><td colspan="7" class="empty">Нет товаров</td></tr>'; return; }
|
||||
const typeLabels = { frame:'Рамка', title:'Титул', theme:'Тема', effect:'Эффект' };
|
||||
body.innerHTML = _shopItems.map(it => `<tr>
|
||||
<td>${it.id}</td>
|
||||
<td><strong>${esc(it.name)}</strong></td>
|
||||
<td><span class="mode-badge mode-practice">${typeLabels[it.type] || esc(it.type)}</span></td>
|
||||
<td>${it.price} <i data-lucide="coins" style="width:12px;height:12px;vertical-align:-2px;color:var(--amber, #FFB347)"></i></td>
|
||||
<td>${it.sold_count || 0}</td>
|
||||
<td>
|
||||
<label class="adm-toggle">
|
||||
<input type="checkbox" ${it.is_active ? 'checked' : ''} onchange="shopAdminToggleActive(${it.id}, this.checked)" />
|
||||
<span class="track"></span><span class="thumb"></span>
|
||||
</label>
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn-edit-q" onclick="shopAdminEditItem(${it.id})">Ред.</button>
|
||||
<button class="btn-del-q" onclick="shopAdminDeleteItem(${it.id})">Удалить</button>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
if (window.lucide) lucide.createIcons();
|
||||
}
|
||||
|
||||
function shopAdminCreateItem() {
|
||||
_shopEditId = null;
|
||||
document.getElementById('shop-form-title').textContent = 'Добавить товар';
|
||||
document.getElementById('shop-f-name').value = '';
|
||||
document.getElementById('shop-f-type').value = 'frame';
|
||||
document.getElementById('shop-f-price').value = '100';
|
||||
document.getElementById('shop-f-desc').value = '';
|
||||
document.getElementById('shop-f-icon').value = '';
|
||||
document.getElementById('shop-f-data').value = '';
|
||||
document.getElementById('shop-f-active').checked = true;
|
||||
document.getElementById('shop-item-form').style.display = '';
|
||||
}
|
||||
|
||||
function shopAdminEditItem(id) {
|
||||
const it = _shopItems.find(i => i.id === id);
|
||||
if (!it) return;
|
||||
_shopEditId = id;
|
||||
document.getElementById('shop-form-title').textContent = 'Редактировать товар #' + id;
|
||||
document.getElementById('shop-f-name').value = it.name || '';
|
||||
document.getElementById('shop-f-type').value = it.type || 'frame';
|
||||
document.getElementById('shop-f-price').value = it.price ?? 100;
|
||||
document.getElementById('shop-f-desc').value = it.description || '';
|
||||
document.getElementById('shop-f-icon').value = it.icon || '';
|
||||
document.getElementById('shop-f-data').value = it.data ? (typeof it.data === 'string' ? it.data : JSON.stringify(it.data)) : '';
|
||||
document.getElementById('shop-f-active').checked = !!it.is_active;
|
||||
document.getElementById('shop-item-form').style.display = '';
|
||||
}
|
||||
|
||||
function shopAdminCancelForm() {
|
||||
document.getElementById('shop-item-form').style.display = 'none';
|
||||
_shopEditId = null;
|
||||
}
|
||||
|
||||
async function shopAdminSaveItem() {
|
||||
if (_shopSaving) return;
|
||||
_shopSaving = true;
|
||||
const data = {
|
||||
name: document.getElementById('shop-f-name').value.trim(),
|
||||
type: document.getElementById('shop-f-type').value,
|
||||
price: parseInt(document.getElementById('shop-f-price').value) || 0,
|
||||
description: document.getElementById('shop-f-desc').value.trim(),
|
||||
icon: document.getElementById('shop-f-icon').value.trim(),
|
||||
data: document.getElementById('shop-f-data').value.trim() || null,
|
||||
is_active: document.getElementById('shop-f-active').checked ? 1 : 0
|
||||
};
|
||||
if (!data.name) { LS.toast('Введите название', 'error'); _shopSaving = false; return; }
|
||||
try {
|
||||
if (_shopEditId) {
|
||||
await LS.adminShopUpdateItem(_shopEditId, data);
|
||||
LS.toast('Товар обновлён', 'success');
|
||||
} else {
|
||||
await LS.adminShopCreateItem(data);
|
||||
LS.toast('Товар создан', 'success');
|
||||
}
|
||||
shopAdminCancelForm();
|
||||
inited = false;
|
||||
await load();
|
||||
inited = true;
|
||||
} catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
finally { _shopSaving = false; }
|
||||
}
|
||||
|
||||
async function shopAdminDeleteItem(id) {
|
||||
if (!await LS.confirm('Все покупки этого товара будут удалены.', { title: 'Удалить товар?', confirmText: 'Удалить', danger: true })) return;
|
||||
try {
|
||||
await LS.adminShopDeleteItem(id);
|
||||
LS.toast('Товар удалён', 'success');
|
||||
inited = false;
|
||||
await load();
|
||||
inited = true;
|
||||
} catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function shopAdminToggleActive(id, active) {
|
||||
try {
|
||||
await LS.adminShopUpdateItem(id, { is_active: active ? 1 : 0 });
|
||||
LS.toast(active ? 'Товар активирован' : 'Товар деактивирован', 'success');
|
||||
} catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
}
|
||||
|
||||
async function shopSearchUser(q) {
|
||||
clearTimeout(_shopSearchTimer);
|
||||
const box = document.getElementById('shop-award-results');
|
||||
if (q.length < 2) { box.classList.remove('open'); return; }
|
||||
_shopSearchTimer = setTimeout(async () => {
|
||||
try {
|
||||
const r = await LS.adminGetUsers({ q, limit: 8 });
|
||||
box.innerHTML = (r.users || []).map(u => `<div class="us-item" onclick="shopPickUser(${u.id}, '${esc(u.name || u.email)}')">
|
||||
<span>${esc(u.name || u.email)}</span><span class="us-role">${u.role}</span>
|
||||
</div>`).join('') || '<div class="us-item" style="color:var(--text-3)">Не найдено</div>';
|
||||
box.classList.add('open');
|
||||
} catch(e) { box.classList.remove('open'); }
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function shopPickUser(id, name) {
|
||||
document.getElementById('shop-award-uid').value = id;
|
||||
document.getElementById('shop-award-user').value = name;
|
||||
document.getElementById('shop-award-results').classList.remove('open');
|
||||
}
|
||||
|
||||
async function shopAdminAwardCoins() {
|
||||
if (_coinsAwarding) return;
|
||||
const userId = parseInt(document.getElementById('shop-award-uid').value);
|
||||
const amount = parseInt(document.getElementById('shop-award-amount').value);
|
||||
const reason = document.getElementById('shop-award-reason').value.trim();
|
||||
if (!userId) { LS.toast('Выберите пользователя', 'error'); return; }
|
||||
if (!amount || amount <= 0) { LS.toast('Введите количество монет', 'error'); return; }
|
||||
_coinsAwarding = true;
|
||||
try {
|
||||
const r = await LS.adminShopAwardCoins({ userId, amount, reason });
|
||||
LS.toast(`Начислено ${amount} монет. Баланс: ${r.coins}`, 'success');
|
||||
document.getElementById('shop-award-uid').value = '';
|
||||
document.getElementById('shop-award-user').value = '';
|
||||
document.getElementById('shop-award-reason').value = '';
|
||||
} catch(e) { LS.toast('Ошибка: ' + e.message, 'error'); }
|
||||
finally { _coinsAwarding = false; }
|
||||
}
|
||||
|
||||
// Expose onclick handlers
|
||||
window.shopAdminCreateItem = shopAdminCreateItem;
|
||||
window.shopAdminEditItem = shopAdminEditItem;
|
||||
window.shopAdminCancelForm = shopAdminCancelForm;
|
||||
window.shopAdminSaveItem = shopAdminSaveItem;
|
||||
window.shopAdminDeleteItem = shopAdminDeleteItem;
|
||||
window.shopAdminToggleActive = shopAdminToggleActive;
|
||||
window.shopSearchUser = shopSearchUser;
|
||||
window.shopPickUser = shopPickUser;
|
||||
window.shopAdminAwardCoins = shopAdminAwardCoins;
|
||||
|
||||
window.AdminSections = window.AdminSections || {};
|
||||
window.AdminSections.shop = {
|
||||
init: async () => { if (inited) return; inited = true; await load(); },
|
||||
reload: load,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user