Files
LMS/web/src/main/resources/static/js/admin-group.js

414 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

function $id(id) { return document.getElementById(id); }
function fmtSeconds(v) {
var m = Math.floor(v / 60);
var s = v % 60;
return m + ':' + (s < 10 ? '0' : '') + s;
}
/* ---------- Оценка + комментарии ---------- */
function openGradeEditor(btn) {
var all = [];
try { all = JSON.parse(btn.getAttribute('data-comments') || '[]'); } catch (e) { all = []; }
var idx = parseInt(btn.getAttribute('data-ci') || '0', 10);
var list = Array.isArray(all[idx]) ? all[idx] : [];
$id('grade-student').value = btn.getAttribute('data-student') || '';
$id('grade-lab').value = btn.getAttribute('data-lab') || '';
$id('grade-who').value = btn.getAttribute('data-student-name') || '';
var title = $id('grade-title');
if (title) title.textContent = (btn.getAttribute('data-lab-name') || '') + ' — ' + (btn.getAttribute('data-student-name') || '');
$id('grade-score').value = btn.getAttribute('data-score') || '';
var box = $id('cs-rows');
box.innerHTML = '';
list.forEach(function (c) { csRow(c.text, c.tone); });
if (!list.length) csRow('', 'neutral');
var d = $id('modal-grade');
if (d && d.showModal) d.showModal();
}
function csRow(text, tone) {
var box = $id('cs-rows');
var row = document.createElement('div');
row.className = 'cs-row';
var wrap = document.createElement('div');
wrap.className = 'cs-text-wrap';
var inp = document.createElement('input');
inp.className = 'input cs-text';
inp.placeholder = 'Комментарий';
inp.value = text || '';
wrap.appendChild(inp);
var sel = document.createElement('select');
sel.className = 'input cs-tone';
sel.title = 'Тональность комментария';
[['positive', '+ позитивный'], ['negative', '— негативный'], ['neutral', '○ нейтральный']].forEach(function (o) {
var op = document.createElement('option');
op.value = o[0];
op.textContent = o[1];
sel.appendChild(op);
});
sel.value = tone || 'neutral';
var rm = document.createElement('button');
rm.type = 'button';
rm.className = 'btn btn-secondary btn-sm';
rm.textContent = 'удалить';
rm.onclick = function () { row.remove(); };
row.appendChild(wrap);
row.appendChild(sel);
row.appendChild(rm);
box.appendChild(row);
attachCommentAutocomplete(inp);
}
function csSerialize() {
var out = [];
document.querySelectorAll('#cs-rows .cs-row').forEach(function (r) {
var t = r.querySelector('.cs-text').value.trim();
if (t) out.push({ text: t, tone: r.querySelector('.cs-tone').value });
});
$id('grade-comments').value = JSON.stringify(out);
}
/* ---------- Автодополнение комментариев ----------
* При фокусе/вводе запрашивает ранее введённые комментарии (подстрока, без регистра),
* отсортированные по частоте; выбор подставляет текст и «тон». */
var AC_TIMER = null;
function attachCommentAutocomplete(input) {
var list = document.createElement('div');
list.className = 'ac-list';
list.hidden = true;
input.parentNode.appendChild(list);
var active = -1;
function hide() {
list.hidden = true;
active = -1;
}
function render(items) {
if (document.activeElement !== input) return;
if (!Array.isArray(items) || !items.length) {
hide();
return;
}
list.innerHTML = '';
items.forEach(function (item) {
var count = parseInt(item.count || 0, 10) || 0;
var row = document.createElement('div');
row.className = 'ac-item';
var text = document.createElement('span');
text.className = 'ac-text';
text.textContent = item.text;
row.appendChild(text);
var badge = document.createElement('span');
badge.className = 'badge badge-xs ac-count';
badge.textContent = count;
badge.title = 'Использований: ' + count;
row.appendChild(badge);
var del = document.createElement('button');
del.type = 'button';
del.className = 'ac-del';
del.textContent = '✕';
del.disabled = count > 0;
del.title = count > 0
? 'Нельзя удалить: комментарий используется ' + count + ' раз(а)'
: 'Удалить комментарий из подсказок';
del.addEventListener('mousedown', function (event) {
event.preventDefault();
event.stopPropagation();
});
del.addEventListener('click', function (event) {
event.stopPropagation();
if (count > 0) return;
removeCommentTemplate(item.text, load);
});
row.appendChild(del);
row.addEventListener('mousedown', function (event) {
if (event.target.closest('.ac-del')) return;
event.preventDefault();
input.value = item.text;
var rowEl = input.closest('.cs-row');
var tone = rowEl ? rowEl.querySelector('.cs-tone') : null;
if (tone && item.tone) tone.value = item.tone;
hide();
});
list.appendChild(row);
});
list.hidden = false;
active = -1;
positionList();
}
/* Если снизу внутри модалки мало места — раскрываем список вверх, чтобы он не обрезался. */
function positionList() {
var wrap = input.parentNode;
var scrollParent = input.closest('.modal-body') || document.documentElement;
var wr = wrap.getBoundingClientRect();
var sr = scrollParent.getBoundingClientRect();
var below = sr.bottom - wr.bottom;
var above = wr.top - sr.top;
list.classList.toggle('drop-up', below < 220 && above > below);
}
window.addEventListener('resize', function () { if (!list.hidden) positionList(); });
function load() {
clearTimeout(AC_TIMER);
AC_TIMER = setTimeout(function () {
fetch('/admin/comment-templates?q=' + encodeURIComponent(input.value.trim()))
.then(function (r) { return r.ok ? r.json() : []; })
.then(render)
.catch(hide);
}, 150);
}
input.addEventListener('focus', load);
input.addEventListener('input', load);
input.addEventListener('blur', function () { setTimeout(hide, 150); });
input.addEventListener('keydown', function (event) {
if (list.hidden) return;
var items = list.querySelectorAll('.ac-item');
if (!items.length) return;
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault();
active = (active + (event.key === 'ArrowDown' ? 1 : -1) + items.length) % items.length;
items.forEach(function (el, i) { el.classList.toggle('active', i === active); });
} else if (event.key === 'Enter' && active >= 0) {
event.preventDefault();
items[active].dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
} else if (event.key === 'Escape') {
hide();
}
});
}
/* ---------- Удаление подсказки комментария и всплывающие уведомления ---------- */
function showToast(text, kind) {
var host = $id('toast-host');
if (!host) return;
var toast = document.createElement('div');
toast.className = 'toast-item ' + (kind === 'error' ? 'toast-error' : 'toast-success');
toast.setAttribute('data-toast', '');
var span = document.createElement('span');
span.className = 'toast-text';
span.textContent = text;
toast.appendChild(span);
var close = document.createElement('button');
close.type = 'button';
close.className = 'toast-close';
close.textContent = '✕';
close.addEventListener('click', function () { toast.remove(); });
toast.appendChild(close);
host.appendChild(toast);
setTimeout(function () { toast.remove(); }, 5000);
}
function removeCommentTemplate(text, done) {
var form = $id('grade-form');
var csrf = form ? form.querySelector('input[name="_csrf"]') : null;
var body = '_csrf=' + encodeURIComponent(csrf ? csrf.value : '') +
'&text=' + encodeURIComponent(text);
fetch('/admin/comment-templates/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body
})
.then(function (r) { return r.ok ? r.json() : { ok: false }; })
.then(function (res) {
if (res && res.ok) {
showToast('Комментарий удалён из подсказок.');
if (done) done();
} else {
showToast((res && res.error) || 'Не удалось удалить комментарий.', 'error');
}
})
.catch(function () { showToast('Не удалось удалить комментарий.', 'error'); });
}
/* ---------- Индивидуальное задание (защита) ---------- */
function defenseStatusText(status, code, seconds) {
if (status === 'waiting') {
return code
? 'Задание назначено, код выдан — ждём входа студента.'
: 'Задание назначено, код ещё не выдан.';
}
if (status === 'started') return 'Студент выполняет работу, осталось ' + fmtSeconds(seconds) + '.';
if (status === 'ready') return 'Студент готов к сдаче. Можно начать заново для дополнительного задания.';
return '';
}
/** Открывает диалог задания; кнопка может не нести data-* (тогда это «назначить новое»). */
function openDefense(btn) {
var form = $id('defense-form');
if (!form) return;
form.reset();
var student = btn.getAttribute('data-student') || '';
var lab = btn.getAttribute('data-lab') || '';
if (student) $id('defense-student-select').value = student;
if (lab) $id('defense-lab-select').value = lab;
$id('defense-md').value = btn.getAttribute('data-md') || '';
var session = btn.getAttribute('data-session') || '';
var status = btn.getAttribute('data-status') || '';
var code = btn.getAttribute('data-code') || '';
var seconds = parseInt(btn.getAttribute('data-seconds') || '0', 10);
var actions = $id('defense-actions');
var statusEl = $id('defense-status');
if (session) {
$id('defense-issue-form').action = '/admin/defenses/' + session + '/issue-code';
$id('defense-reset-form').action = '/admin/defenses/' + session + '/reset';
$id('defense-action-lab').value = lab;
$id('defense-reset-lab').value = lab;
$id('defense-reset-form').hidden = status !== 'ready';
if (statusEl) statusEl.textContent = defenseStatusText(status, code, seconds);
actions.hidden = false;
} else {
if (statusEl) statusEl.textContent = '';
actions.hidden = true;
}
var d = $id('modal-defense');
if (d && d.showModal) d.showModal();
}
/* ---------- Назначение варианта (вручную или случайно из уровня) ---------- */
var VARIANT_CACHE = {};
function openVariant(btn) {
var student = btn.getAttribute('data-student') || '';
var lab = btn.getAttribute('data-lab') || '';
var who = btn.getAttribute('data-student-name') || '';
var labName = btn.getAttribute('data-lab-name') || '';
var currentVariant = btn.getAttribute('data-variant') || '';
$id('variant-who').textContent = who + ' — ' + labName;
$id('variant-random-student').value = student;
$id('variant-random-lab').value = lab;
$id('variant-pin-student').value = student;
$id('variant-pin-lab').value = lab;
var levelSel = $id('variant-level');
levelSel.innerHTML = '';
VARIANT_CACHE = {};
fetch('/admin/labs/' + encodeURIComponent(lab) + '/schema')
.then(function (r) { return r.ok ? r.json() : { levels: [] }; })
.then(function (data) {
(data.levels || []).forEach(function (lvl, i) {
var opt = document.createElement('option');
opt.value = lvl.id;
opt.textContent = lvl.title || ('Уровень ' + (i + 1));
opt.__variants = lvl.variants || [];
(lvl.variants || []).forEach(function (v) { VARIANT_CACHE[v.id] = v; });
levelSel.appendChild(opt);
});
// если вариант уже назначен — открываем уровень, в котором он лежит
if (currentVariant) {
Array.prototype.some.call(levelSel.options, function (opt) {
if ((opt.__variants || []).some(function (v) { return v.id === currentVariant; })) {
levelSel.value = opt.value;
return true;
}
return false;
});
}
levelSel.onchange = function () { fillVariantSelect(currentVariant); };
fillVariantSelect(currentVariant);
});
var d = $id('modal-variant');
if (d && d.showModal) d.showModal();
}
function fillVariantSelect(currentVariant) {
var levelSel = $id('variant-level');
var variantSel = $id('variant-variant');
variantSel.innerHTML = '';
var opt = levelSel.selectedOptions[0];
var variants = (opt && opt.__variants) || [];
variants.forEach(function (v) {
var o = document.createElement('option');
o.value = v.id;
o.textContent = 'Вариант ' + v.number;
if (currentVariant && v.id === currentVariant) o.selected = true;
variantSel.appendChild(o);
});
$id('variant-random-level').value = levelSel.value || '';
$id('variant-pin-id').value = variantSel.value || '';
variantSel.onchange = function () {
$id('variant-pin-id').value = variantSel.value || '';
showVariantTask();
};
showVariantTask();
}
function showVariantTask() {
var variantSel = $id('variant-variant');
var wrap = $id('variant-task-wrap');
var box = $id('variant-task');
if (!wrap || !box) return;
var sel = variantSel.selectedOptions[0];
var v = sel && VARIANT_CACHE[sel.value];
if (v && v.statementHtml) {
box.innerHTML = v.statementHtml;
wrap.hidden = false;
} else {
box.innerHTML = '';
wrap.hidden = true;
}
}
/* ---------- Таймеры защиты в ячейках ---------- */
function tickDefenseCells() {
document.querySelectorAll('.defense-cell-timer').forEach(function (el) {
var left = parseInt(el.getAttribute('data-seconds') || '0', 10);
var tick = setInterval(function () {
left -= 1;
if (left <= 0) { clearInterval(tick); el.textContent = '0:00'; return; }
el.textContent = fmtSeconds(left);
}, 1000);
el.textContent = fmtSeconds(left);
});
}
/* ---------- Матрица: фокус на лабораторной ---------- */
function focusLab() {
var s = $id('lab-focus');
if (!s) return;
var labId = s.value;
document.querySelectorAll('th.lab-col[data-lab], td.cell-lab[data-lab], .filter-row th[data-lab]').forEach(function (el) {
el.style.display = (!labId || el.getAttribute('data-lab') === labId) ? '' : 'none';
});
}
document.addEventListener('submit', function (ev) {
if (ev.target && ev.target.id === 'grade-form') csSerialize();
}, true);
window.addEventListener('DOMContentLoaded', function () {
focusLab();
tickDefenseCells();
});