diff --git a/services/identity-service/src/main/kotlin/ru/ysu/lms/identity/roster/GroupService.kt b/services/identity-service/src/main/kotlin/ru/ysu/lms/identity/roster/GroupService.kt index 9a47d3e..0feabfb 100644 --- a/services/identity-service/src/main/kotlin/ru/ysu/lms/identity/roster/GroupService.kt +++ b/services/identity-service/src/main/kotlin/ru/ysu/lms/identity/roster/GroupService.kt @@ -222,7 +222,7 @@ class GroupService( mustChangePassword = account.mustChangePassword, tempPassword = account.tempPassword, ) - }.sortedWith(Comparator { a, b -> studentNoComparator.compare(a.studentNo, b.studentNo) }) + }.sortedWith(compareBy({ it.fullName.lowercase() }, { it.studentNo })) return GroupDetail(group.id.toString(), group.name, group.taskDurationMin, rows) } @@ -264,17 +264,4 @@ class GroupService( studentCount = enrollments.countByGroup_Id(group.id), taskDurationMin = group.taskDurationMin, ) - - private companion object { - val studentNoComparator = Comparator { a, b -> - val left = a.toIntOrNull() - val right = b.toIntOrNull() - when { - left != null && right != null -> left.compareTo(right) - left != null -> -1 - right != null -> 1 - else -> a.compareTo(b) - } - } - } } diff --git a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt index 396d4db..87428ca 100644 --- a/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt +++ b/web/src/main/kotlin/ru/ysu/lms/web/admin/AdminController.kt @@ -168,7 +168,7 @@ class AdminController( assignmentMd = d.assignmentMd, secondsLeft = d.secondsLeft, ) } - } + }.sortedWith(compareBy({ it.fullName.lowercase() }, { it.labNumber })) model.addAttribute("group", group) model.addAttribute("labs", labs) diff --git a/web/src/main/resources/static/css/app.css b/web/src/main/resources/static/css/app.css index a006e92..de86a24 100644 --- a/web/src/main/resources/static/css/app.css +++ b/web/src/main/resources/static/css/app.css @@ -329,3 +329,12 @@ details.sub-acc[open] > summary .sub-arrow { transform: rotate(-135deg); } .lab-head-row { display: flex; align-items: center; justify-content: center; gap: 0.3rem; } .actions-cell { white-space: nowrap; } .actions-cell .inline-form { display: inline-block; } + +/* tablekit: сортируемые таблицы и поиск */ +th.th-sortable { cursor: pointer; user-select: none; } +th.th-sortable:hover { background: var(--c-surface); } +th.th-sortable.is-asc::after { content: " ▲"; color: var(--c-text-muted); font-size: 0.7rem; } +th.th-sortable.is-desc::after { content: " ▼"; color: var(--c-text-muted); font-size: 0.7rem; } +.row-hidden { display: none !important; } +.table-search { width: 14rem; } +@media (max-width: 640px) { .table-search { width: 100%; } } diff --git a/web/src/main/resources/static/js/tablekit.js b/web/src/main/resources/static/js/tablekit.js new file mode 100644 index 0000000..4f8241d --- /dev/null +++ b/web/src/main/resources/static/js/tablekit.js @@ -0,0 +1,80 @@ +/* tablekit: сортировка по клику на заголовок + живой поиск по строке. + Таблицу размечают: , поле поиска + . + Начальная сортировка (ФИО) выполняется на сервере; клик по заголовку меняет её. */ +(function () { + function normalize(value) { + return (value || '').trim().toLowerCase().replace(/\s+/g, ' '); + } + + function cellValue(cell) { + if (!cell) return ''; + if (cell.dataset && cell.dataset.sort !== undefined) return normalize(cell.dataset.sort); + return normalize(cell.textContent); + } + + function compared(a, b) { + var na = a.match(/-?\d+([.,]\d+)?/g); + var nb = b.match(/-?\d+([.,]\d+)?/g); + var fa = na ? parseFloat(na[0].replace(',', '.')) : NaN; + var fb = nb ? parseFloat(nb[0].replace(',', '.')) : NaN; + if (!isNaN(fa) && !isNaN(fb) && fa !== fb) return fa - fb; + return a.localeCompare(b, 'ru', { numeric: true, sensitivity: 'base' }); + } + + function clearMarkers(table, exceptColumn) { + var cells = table.tHead.rows[table.tHead.rows.length - 1].cells; + for (var i = 0; i < cells.length; i++) { + if (i === exceptColumn) continue; + cells[i].classList.remove('is-asc', 'is-desc'); + } + } + + function sortColumn(table, index, direction) { + var body = table.tBodies[0]; + if (!body) return; + var rows = Array.prototype.slice.call(body.rows); + rows.sort(function (a, b) { + return compared(cellValue(a.cells[index]), cellValue(b.cells[index])); + }); + if (direction < 0) rows.reverse(); + rows.forEach(function (row) { body.appendChild(row); }); + } + + function bind(table) { + if (table.dataset.tablekitBound) return; + table.dataset.tablekitBound = '1'; + var head = table.tHead; + if (!head) return; + var cells = head.rows[head.rows.length - 1].cells; + Array.prototype.forEach.call(cells, function (th, index) { + if (th.hasAttribute('data-nosort')) return; + th.classList.add('th-sortable'); + th.title = th.title || 'Сортировать по этому столбцу'; + th.addEventListener('click', function (event) { + if (event.target.closest('form, button, a, select, input')) return; + var asc = !th.classList.contains('is-asc'); + clearMarkers(table, index); + th.classList.add(asc ? 'is-asc' : 'is-desc'); + sortColumn(table, index, asc ? 1 : -1); + th.title = (th.textContent.trim() || 'Столбец') + ' — ' + (asc ? 'по возрастанию' : 'по убыванию'); + }); + }); + } + + function bindSearch(input) { + var table = document.getElementById(input.dataset.target); + if (!table || !table.tBodies[0]) return; + input.addEventListener('input', function () { + var query = normalize(input.value); + Array.prototype.forEach.call(table.tBodies[0].rows, function (row) { + row.classList.toggle('row-hidden', !!query && normalize(row.textContent).indexOf(query) < 0); + }); + }); + } + + document.addEventListener('DOMContentLoaded', function () { + document.querySelectorAll('table.tablekit').forEach(bind); + document.querySelectorAll('input.table-search').forEach(bindSearch); + }); +})(); diff --git a/web/src/main/resources/templates/admin/group.html b/web/src/main/resources/templates/admin/group.html index f2be773..5f9e655 100644 --- a/web/src/main/resources/templates/admin/group.html +++ b/web/src/main/resources/templates/admin/group.html @@ -29,6 +29,8 @@

Результаты по лабораторным

+
+
- +
СтудентСтудент
@@ -73,7 +75,7 @@
—
—
-
+
@@ -113,11 +115,13 @@ +
- - +
СтудентЛРЗаданиеСтатусДействия
+
СтудентЛРЗаданиеСтатусДействия
@@ -162,10 +166,14 @@
-

Студенты

+
+

Студенты

+ +
- - +
№ФИОЛогинОсноваВариантыПарольДействия
+ diff --git a/web/src/main/resources/templates/admin/groups.html b/web/src/main/resources/templates/admin/groups.html index 2bb1ca5..80f8cd1 100644 --- a/web/src/main/resources/templates/admin/groups.html +++ b/web/src/main/resources/templates/admin/groups.html @@ -22,9 +22,14 @@ -
-
№ФИОЛогинОсноваВариантыПарольДействия
1
- +
+

Список групп

+ +
+
+
НазваниеСтудентовВыгрузка
+ diff --git a/web/src/main/resources/templates/admin/lab.html b/web/src/main/resources/templates/admin/lab.html index 4ecac4a..bc91466 100644 --- a/web/src/main/resources/templates/admin/lab.html +++ b/web/src/main/resources/templates/admin/lab.html @@ -71,13 +71,17 @@ -
+
-

Группа

+
+

Группа

+ +
-
НазваниеСтудентовВыгрузка
—
+
- + diff --git a/web/src/main/resources/templates/admin/labs.html b/web/src/main/resources/templates/admin/labs.html index 1f8a49b..58c8ce4 100644 --- a/web/src/main/resources/templates/admin/labs.html +++ b/web/src/main/resources/templates/admin/labs.html @@ -6,6 +6,8 @@

Лабораторные

+
@@ -16,8 +18,8 @@
-
ВыборСтудентУровеньВариантВыборСтудентУровеньВариант ИсточникСдача
- +
№НазваниеВариантовСтатус
+ diff --git a/web/src/main/resources/templates/fragments/base.html b/web/src/main/resources/templates/fragments/base.html index f6ee60c..78200b4 100644 --- a/web/src/main/resources/templates/fragments/base.html +++ b/web/src/main/resources/templates/fragments/base.html @@ -10,6 +10,7 @@ +
№НазваниеВариантовСтатус
1