Admin group: variant modal, cell badges/counters, header cleanup, bigger modal

This commit is contained in:
2026-09-28 17:09:55 +03:00
parent fb67f5313d
commit 9df1af7647
5 changed files with 235 additions and 92 deletions

View File

@@ -321,6 +321,9 @@ code.code { padding: 0.125rem 0.5rem; border-radius: 0.25rem; font-family: monos
.cell-btn.dim { color: var(--c-text-muted); }
.cell-btn.warn { color: #d97706; }
.cell-btn.ok { color: #16a34a; }
.cell-num { font-size: 0.72rem; opacity: 0.75; }
.mini-count { font-size: 0.72rem; margin-left: 0.15rem; color: var(--c-text-muted); }
.defense-cell-timer { font-variant-numeric: tabular-nums; }
.lab-head-actions { display: flex; gap: 0.2rem; }
.mini-btn {
display: inline-flex; align-items: center; justify-content: center;
@@ -333,8 +336,9 @@ code.code { padding: 0.125rem 0.5rem; border-radius: 0.25rem; font-family: monos
.mini-btn.warn { color: #d97706; }
/* Увеличенный диалог (оценка + комментарии) */
dialog.modal.modal-lg { width: min(56rem, calc(100vw - 2rem)); }
dialog.modal.modal-lg .modal-body { max-height: 78vh; }
dialog.modal.modal-lg { width: min(72rem, calc(100vw - 2rem)); }
dialog.modal.modal-lg .modal-body { max-height: 88vh; min-height: 62vh; }
.mini-btn svg, .cell-btn svg { width: 1rem; height: 1rem; }
.chip { display: inline-block; margin: 2px 4px 2px 0; padding: 2px 8px; border-radius: 9999px; font-size: 11.5px; line-height: 1.5; }
.chip-pos { background: #16803c; color: #fff; }
@@ -376,7 +380,8 @@ dialog.modal.modal-lg .modal-body { max-height: 78vh; }
.font-medium { font-weight: 500; }
.scroll-x { overflow-x: auto; }
.matrix { border-collapse: separate; border-spacing: 0; }
.matrix th, .matrix td { vertical-align: top; }
.matrix th { vertical-align: top; }
.matrix td { vertical-align: middle; }
.matrix th.lab-col { min-width: 9.5rem; border-left: 1px solid var(--c-border); }
.matrix td.cell-lab { min-width: 9.5rem; border-left: 1px solid var(--c-border); }
.matrix td.student-cell { min-width: 12rem; }

View File

@@ -1,5 +1,11 @@
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) {
@@ -58,7 +64,16 @@ function csRow(text, tone) {
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);
}
/* ---------- Автодополнение комментариев ----------
* При фокусе/вводе запрашивает ранее введённые комментарии (подстрока, без регистра),
* отсортированные по частоте; выбор подставляет текст и «тон». */
@@ -131,17 +146,19 @@ function attachCommentAutocomplete(input) {
});
}
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);
}
/* ---------- Индивидуальное задание (защита) ---------- */
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');
@@ -155,15 +172,20 @@ function openDefense(btn) {
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';
if ($id('defense-action-lab')) $id('defense-action-lab').value = lab;
if ($id('defense-reset-lab')) $id('defense-reset-lab').value = lab;
$id('defense-action-lab').value = lab;
$id('defense-reset-lab').value = lab;
$id('defense-reset-form').hidden = status !== 'ready';
if (actions) actions.hidden = false;
} else if (actions) {
if (statusEl) statusEl.textContent = defenseStatusText(status, code, seconds);
actions.hidden = false;
} else {
if (statusEl) statusEl.textContent = '';
actions.hidden = true;
}
@@ -171,21 +193,73 @@ function openDefense(btn) {
if (d && d.showModal) d.showModal();
}
function showDefenseFromModal() {
var el = $id('defense-text-body');
if (el) el.textContent = $id('defense-md').value || '(пусто)';
var title = $id('defense-text-title');
if (title) {
var s = $id('defense-student-select');
var l = $id('defense-lab-select');
var who = s && s.selectedOptions[0] ? s.selectedOptions[0].textContent : '';
var lab = l && l.selectedOptions[0] ? l.selectedOptions[0].textContent : '';
title.textContent = who + ' — ' + lab;
}
var d = $id('modal-defense-text');
/* ---------- Назначение варианта (вручную или случайно из уровня) ---------- */
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 = '';
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 || [];
levelSel.appendChild(opt);
});
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 || ''; };
}
/* ---------- Таймеры защиты в ячейках ---------- */
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() {
@@ -201,4 +275,7 @@ document.addEventListener('submit', function (ev) {
if (ev.target && ev.target.id === 'grade-form') csSerialize();
}, true);
window.addEventListener('DOMContentLoaded', function () { focusLab(); });
window.addEventListener('DOMContentLoaded', function () {
focusLab();
tickDefenseCells();
});