From c28fd373d646dd76137595ebe66b1b0bc9ef22a2 Mon Sep 17 00:00:00 2001
From: Vladislav Kopnin
Date: Mon, 28 Sep 2026 16:23:29 +0300
Subject: [PATCH] Admin group: single table, defense actions in cells; flash
messages as toasts
---
.../ysu/lms/web/admin/AdminGroupController.kt | 35 +---
.../ru/ysu/lms/web/admin/AdminLabView.kt | 2 +
web/src/main/resources/static/css/app.css | 27 +++
.../main/resources/static/js/admin-group.js | 45 +++--
.../main/resources/templates/admin/group.html | 182 ++++++------------
.../resources/templates/admin/groups.html | 2 -
.../resources/templates/admin/lab-edit.html | 2 -
.../main/resources/templates/admin/lab.html | 2 -
.../main/resources/templates/admin/labs.html | 2 -
.../resources/templates/change_password.html | 1 -
.../resources/templates/fragments/base.html | 28 +++
.../main/resources/templates/learn/index.html | 3 -
web/src/main/resources/templates/login.html | 1 -
web/src/main/resources/templates/setup.html | 1 -
.../main/resources/templates/student/lab.html | 3 -
15 files changed, 149 insertions(+), 187 deletions(-)
diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt
index 3d840e8..97c524b 100644
--- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt
+++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminGroupController.kt
@@ -163,6 +163,8 @@ class AdminGroupController(
defenseCode = def?.code,
defenseSecondsLeft = def?.secondsLeft,
defenseAssigned = def?.assignmentMd?.isNotBlank() == true,
+ defenseSessionId = def?.id,
+ defenseAssignmentMd = def?.assignmentMd,
)
}
GroupStudentRow(
@@ -185,11 +187,8 @@ class AdminGroupController(
)
}
- val defenseRows =
- buildDefenseRows(t, group, labs)
-
val candidates = runCatching { api.groupCandidates(t, id) }.getOrDefault(emptyList())
- fillGroupModel(model, GroupViewModel(group, labs, candidates, rows, defenseRows, publishedLabs))
+ fillGroupModel(model, GroupViewModel(group, labs, candidates, rows, publishedLabs))
return "admin/group"
}
@@ -198,38 +197,11 @@ class AdminGroupController(
id: String,
): Pair> = api.groupDetail(token, id) to api.listLabs(token).filter { it.status == "published" }
- /** Строки для блока «Защиты»: сессии защиты по всем лабораторным группы. */
- private fun buildDefenseRows(
- token: String,
- group: GroupDetailDto,
- labs: List,
- ): List =
- labs
- .flatMap { lab ->
- val byStudent = api.labDefenses(token, lab.id).associateBy { it.studentId }
- group.students.mapNotNull { s ->
- val d = byStudent[s.id] ?: return@mapNotNull null
- GroupDefenseRow(
- sessionId = d.id,
- studentId = s.id,
- fullName = s.fullName,
- studentNo = s.studentNo,
- labId = lab.id,
- labNumber = lab.number,
- status = d.status,
- code = d.code?.takeIf { d.status == "waiting" },
- assignmentMd = d.assignmentMd,
- secondsLeft = d.secondsLeft,
- )
- }
- }.sortedWith(compareBy({ it.fullName.lowercase() }, { it.labNumber }))
-
private data class GroupViewModel(
val group: GroupDetailDto,
val labs: List,
val candidates: List,
val rows: List,
- val defenseRows: List,
val publishedLabs: Set,
)
@@ -241,7 +213,6 @@ class AdminGroupController(
model.addAttribute("labs", view.labs)
model.addAttribute("candidates", view.candidates)
model.addAttribute("studentGrades", view.rows)
- model.addAttribute("defenseRows", view.defenseRows)
model.addAttribute("publishedLabIds", view.publishedLabs)
}
diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabView.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabView.kt
index bb95302..912d744 100644
--- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabView.kt
+++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminLabView.kt
@@ -51,6 +51,8 @@ data class GroupLabCell(
val defenseCode: String? = null,
val defenseSecondsLeft: Long? = null,
val defenseAssigned: Boolean = false,
+ val defenseSessionId: String? = null,
+ val defenseAssignmentMd: String? = null,
)
/** Строка студента в матрице + комментарии для диалога. */
diff --git a/web/src/main/resources/static/css/app.css b/web/src/main/resources/static/css/app.css
index bac1ec3..cd58f24 100644
--- a/web/src/main/resources/static/css/app.css
+++ b/web/src/main/resources/static/css/app.css
@@ -200,6 +200,33 @@ tr:last-child td { border-bottom: none; }
/* Toast-уведомление */
.toast { position: fixed; top: 1rem; right: 1rem; z-index: 50; width: 20rem; }
+/* Всплывающие уведомления (flash notice/error) вместо статичных алертов */
+.toast-host {
+ position: fixed; top: 1rem; right: 1rem; z-index: 100;
+ display: flex; flex-direction: column; gap: 0.5rem;
+ width: min(24rem, calc(100vw - 2rem)); pointer-events: none;
+}
+.toast-item {
+ pointer-events: auto; display: flex; align-items: flex-start; gap: 0.5rem;
+ padding: 0.7rem 0.85rem; border-radius: 0.5rem; border: 1px solid var(--c-border);
+ border-left-width: 4px; background: var(--c-surface); color: var(--c-text);
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
+ animation: toast-in 0.18s ease-out;
+}
+.toast-item .toast-text { flex: 1 1 auto; min-width: 0; white-space: pre-line; overflow-wrap: anywhere; }
+.toast-item.toast-success { border-left-color: #16a34a; }
+.toast-item.toast-error { border-left-color: #dc2626; }
+.toast-item.toast-warning { border-left-color: #d97706; }
+.toast-close {
+ flex: 0 0 auto; border: none; background: none; cursor: pointer;
+ color: var(--c-text-muted); font-size: 1.05rem; line-height: 1; padding: 0 0.1rem;
+}
+.toast-close:hover { color: var(--c-text); }
+@keyframes toast-in {
+ from { opacity: 0; transform: translateY(-6px); }
+ to { opacity: 1; transform: none; }
+}
+
/* Modals */
dialog.modal {
border: none; border-radius: 0.75rem; padding: 0;
diff --git a/web/src/main/resources/static/js/admin-group.js b/web/src/main/resources/static/js/admin-group.js
index 6354b16..c663b51 100644
--- a/web/src/main/resources/static/js/admin-group.js
+++ b/web/src/main/resources/static/js/admin-group.js
@@ -65,9 +65,10 @@ function csSerialize() {
$id('grade-comments').value = JSON.stringify(out);
}
-/* ---------- Индивидуальные задания (защита) ---------- */
+/* ---------- Индивидуальное задание (защита) ---------- */
-function openDefenseForm(btn) {
+/** Открывает диалог задания; кнопка может не нести data-* (тогда это «назначить новое»). */
+function openDefense(btn) {
var form = $id('defense-form');
if (!form) return;
form.reset();
@@ -76,29 +77,41 @@ function openDefenseForm(btn) {
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 actions = $id('defense-actions');
+ 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-reset-form').hidden = status !== 'ready';
+ if (actions) actions.hidden = false;
+ } else if (actions) {
+ actions.hidden = true;
+ }
+
var d = $id('modal-defense');
if (d && d.showModal) d.showModal();
}
-function showDefenseText(btn) {
+function showDefenseFromModal() {
var el = $id('defense-text-body');
- if (el) el.textContent = btn.getAttribute('data-md') || '(пусто)';
+ if (el) el.textContent = $id('defense-md').value || '(пусто)';
var title = $id('defense-text-title');
- if (title) title.textContent = (btn.getAttribute('data-student-name') || '') + ' — ' + (btn.getAttribute('data-lab-name') || '');
+ 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');
if (d && d.showModal) d.showModal();
}
-function filterDefense() {
- var s = $id('defense-lab-filter');
- if (!s) return;
- var labId = s.value;
- document.querySelectorAll('#defense-table tr[data-lab]').forEach(function (tr) {
- tr.style.display = (!labId || tr.getAttribute('data-lab') === labId) ? '' : 'none';
- });
-}
-
-/* ---------- Матрица: фокус на лабораторной и закрепление варианта ---------- */
+/* ---------- Матрица: фокус на лабораторной ---------- */
function focusLab() {
var s = $id('lab-focus');
@@ -113,4 +126,4 @@ document.addEventListener('submit', function (ev) {
if (ev.target && ev.target.id === 'grade-form') csSerialize();
}, true);
-window.addEventListener('DOMContentLoaded', function () { filterDefense(); focusLab(); });
+window.addEventListener('DOMContentLoaded', function () { focusLab(); });
diff --git a/web/src/main/resources/templates/admin/group.html b/web/src/main/resources/templates/admin/group.html
index 5c63f02..7f7e07c 100644
--- a/web/src/main/resources/templates/admin/group.html
+++ b/web/src/main/resources/templates/admin/group.html
@@ -14,22 +14,18 @@
Добавить существующего
+ Назначить задание
+ Импорт CSV
Скачать все работы
Логины XLSX
CSV
-
-
-
- Новый пароль: password
-
-
-
Результаты по лабораторным
+
Студенты и лабораторные
-
Показать:
@@ -39,14 +35,14 @@
- Значки в ячейке: «№» — вариант, «🛡» — защита, «↓» — сдача (скачать), «✎» — оценка.
- Наведите на значок — появится пояснение; клик по «↓» скачивает последнее решение,
- клик по «✎» открывает оценку и комментарии. Раздача вариантов — на странице лабораторной.
+ Ячейка: «№» — вариант, «🛡» — защита (клик — задание и код), «↓» — сдача, «✎» — оценка.
+ Раздача вариантов — на странице лабораторной.
+
Опубликованных лабораторных пока нет.
-
Группы
-
-
-
-
Условие лабораторной
diff --git a/web/src/main/resources/templates/admin/labs.html b/web/src/main/resources/templates/admin/labs.html
index dcc3490..0027ff4 100644
--- a/web/src/main/resources/templates/admin/labs.html
+++ b/web/src/main/resources/templates/admin/labs.html
@@ -14,8 +14,6 @@
Статус draft — черновик (редактирование, студентам не видна).
published — опубликована (доступна студентам, видны распределённые варианты). Нажмите на бейдж статуса, чтобы переключить.
-
-
diff --git a/web/src/main/resources/templates/change_password.html b/web/src/main/resources/templates/change_password.html
index 7117d39..21ae338 100644
--- a/web/src/main/resources/templates/change_password.html
+++ b/web/src/main/resources/templates/change_password.html
@@ -12,7 +12,6 @@
Смените пароль, если хотите. После сохранения вы останетесь в системе.
-
Текущий пароль
diff --git a/web/src/main/resources/templates/fragments/base.html b/web/src/main/resources/templates/fragments/base.html
index a2f6178..4401f8e 100644
--- a/web/src/main/resources/templates/fragments/base.html
+++ b/web/src/main/resources/templates/fragments/base.html
@@ -108,5 +108,33 @@
LMS v0.2.0
+
+
+ …
+ ✕
+
+
+ …
+ ✕
+
+
+ Новый пароль: password
+ ✕
+
+
+