Files
Maxim Dolgolyov 92030b462c 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.
2026-05-16 22:50:14 +03:00

105 lines
5.2 KiB
JavaScript

'use strict';
/* admin → sublog (submission log) section */
(function () {
'use strict';
let inited = false;
const SL_STATUSES = { new:'На проверке', reviewed:'Проверено', accepted:'Принято', revision:'На доработке', resubmitted:'Повторно' };
async function load() {
const el = document.getElementById('sublog-list');
const countEl = document.getElementById('sublog-count');
const classId = document.getElementById('sublog-class-filter').value;
el.innerHTML = '<div class="spinner"></div>';
countEl.textContent = '';
try {
const url = classId ? `/api/submissions/log?class_id=${classId}` : '/api/submissions/log';
const rows = await LS.api(url);
// Populate class filter on first load
const sel = document.getElementById('sublog-class-filter');
if (sel.options.length <= 1 && rows.length) {
const classMap = new Map();
rows.forEach(r => { if (r.class_id && r.class_name) classMap.set(r.class_id, r.class_name); });
classMap.forEach((name, id) => {
const opt = document.createElement('option');
opt.value = id; opt.textContent = name;
sel.appendChild(opt);
});
}
countEl.textContent = rows.length ? `${rows.length} записей` : '';
if (!rows.length) {
el.innerHTML = `<div class="sl-empty">
<div class="sl-empty-icon"><i data-lucide="inbox" style="width:48px;height:48px"></i></div>
Удалённых работ нет
</div>`;
if (window.lucide) lucide.createIcons({ nodes: [el] });
return;
}
const ROLE_LABELS = { admin: 'Админ', teacher: 'Учитель', student: 'Ученик' };
el.innerHTML = `<div class="sl-wrap"><table class="sl-table">
<thead><tr>
<th>Дата</th>
<th>Ученик</th>
<th>Файл</th>
<th>Задание</th>
<th>Класс</th>
<th>Статус</th>
<th>Оценка</th>
<th>Удалил</th>
</tr></thead>
<tbody>${rows.map(r => {
const dt = r.deleted_at ? new Date(r.deleted_at.includes('T') ? r.deleted_at : r.deleted_at.replace(' ','T')+'Z') : null;
const dateStr = dt ? dt.toLocaleDateString('ru',{day:'numeric',month:'short'}) + ' ' + dt.toLocaleTimeString('ru',{hour:'2-digit',minute:'2-digit'}) : '—';
const initials = (r.student_name || '?').split(' ').slice(0,2).map(w => w[0]?.toUpperCase() || '').join('');
const st = r.status || 'new';
const gradeVal = r.grade != null ? r.grade : null;
const gradeCls = gradeVal != null ? (gradeVal >= 80 ? 'sl-grade-hi' : gradeVal >= 50 ? 'sl-grade-mid' : 'sl-grade-lo') : 'sl-grade-none';
const roleCls = 'sl-role-' + (r.deleted_by_role || 'student');
return `<tr>
<td><span class="sl-date">${dateStr}</span></td>
<td><span class="sl-student"><span class="sl-student-avatar">${initials}</span>${esc(r.student_name || '—')}</span></td>
<td><span class="sl-file" title="${esc(r.original_name || '')}">${esc(r.original_name || '—')}</span></td>
<td><span class="sl-assignment">${esc(r.assignment_title || '—')}</span></td>
<td><span class="sl-class">${esc(r.class_name || '—')}</span></td>
<td><span class="sl-status sl-status-${st}">${SL_STATUSES[st] || st}</span></td>
<td><span class="sl-grade ${gradeCls}">${gradeVal != null ? gradeVal : '—'}</span></td>
<td><span class="sl-deleted-by">${esc(r.deleted_by_name || '—')} <span class="sl-role-badge ${roleCls}">${ROLE_LABELS[r.deleted_by_role] || r.deleted_by_role || '?'}</span></span></td>
</tr>`;
}).join('')}</tbody>
</table></div>`;
document.getElementById('btn-clear-sublog').style.display = '';
} catch (e) {
el.innerHTML = `<div class="sl-empty" style="color:#c0306a">Ошибка: ${esc(e.message)}</div>`;
}
}
async function clearSubmissionLog() {
if (!await LS.confirm('Очистить весь журнал удалённых работ? Это действие необратимо.', { title: 'Очистка журнала', confirmText: 'Очистить', danger: true })) return;
try {
await LS.api('/api/submissions/log', { method: 'DELETE' });
document.getElementById('btn-clear-sublog').style.display = 'none';
document.getElementById('sublog-count').textContent = '';
document.getElementById('sublog-list').innerHTML = `<div class="sl-empty">
<div class="sl-empty-icon"><i data-lucide="inbox" style="width:48px;height:48px"></i></div>
Журнал очищен
</div>`;
if (window.lucide) lucide.createIcons({ nodes: [document.getElementById('sublog-list')] });
LS.toast('Журнал очищен', 'success');
} catch (e) { LS.toast('Ошибка: ' + e.message, 'error'); }
}
// Expose handlers used by HTML onclicks
window.loadSubmissionLog = load;
window.clearSubmissionLog = clearSubmissionLog;
window.AdminSections = window.AdminSections || {};
window.AdminSections.sublog = {
init: async () => { if (inited) return; inited = true; await load(); },
reload: load,
};
})();