Tables: initial ФИО order, click-to-sort columns, substring search

- identity group detail (and thus every student list) is ordered by full name;
  defense rows on the group page sorted by name + lab number
- tablekit.js: click a header to sort (numeric-aware, ru locale), second click
  flips direction (▲/▼ indicator); search input with data-target filters rows
  by substring live
- tables wired: group matrix (with data-sort on variant cells), defense table
  (data-sort on status/date), students table, lab assignment lists per group,
  groups list and labs list; name/number columns pre-marked ascending
- non-sortable columns marked data-nosort (checkbox/actions)
This commit is contained in:
2026-09-15 22:38:42 +03:00
parent cdf4fb0ee0
commit a4f2c7db90
9 changed files with 128 additions and 32 deletions

View File

@@ -29,6 +29,8 @@
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
<h2 class="section-title m-0">Результаты по лабораторным</h2>
<div class="flex flex-wrap items-center gap-2" th:if="${!#lists.isEmpty(labs)}">
<input class="input input-sm table-search" data-target="matrix-table"
placeholder="Поиск по студенту…" aria-label="Поиск по студенту">
<label class="label m-0" for="lab-focus">Показать:</label>
<select class="input input-sm" id="lab-focus" onchange="focusLab()">
<option value="">все лабораторные</option>
@@ -44,10 +46,10 @@
<div th:if="${#lists.isEmpty(labs)}" class="card card-body mb-6 text-muted">Опубликованных лабораторных пока нет.</div>
<div class="card card-flush mb-6" th:if="${!#lists.isEmpty(labs)}">
<div class="scroll-x">
<table class="matrix">
<table class="matrix tablekit" id="matrix-table">
<thead>
<tr>
<th class="matrix-sticky">Студент</th>
<th class="matrix-sticky th-sortable is-asc" title="ФИО — по возрастанию">Студент</th>
<th th:each="lab : ${labs}" class="text-center lab-col" th:attr="data-lab=${lab.id}">
<div class="lab-col-head">
<div class="lab-head-row">
@@ -73,7 +75,7 @@
<div class="font-medium" th:text="${row.fullName}">—</div>
<div class="text-sm text-muted font-mono" th:text="${row.studentNo}">—</div>
</td>
<td th:each="cell, iterStat : ${row.cells}" class="text-center cell-lab" th:attr="data-lab=${cell.labId}">
<td th:each="cell, iterStat : ${row.cells}" class="text-center cell-lab" th:attr="data-lab=${cell.labId},data-sort=${cell.variantNumber}">
<div class="cell-chips">
<span class="cell-chip"
th:title="(${cell.variantNumber} ? 'Вариант ' + ${cell.variantNumber} : 'Вариант') + ' — ' + (${cell.variantManual} ? 'закреплён вручную (раздача его не тронет)' : 'выдан автоматически')">
@@ -113,11 +115,13 @@
</select>
<button class="btn btn-primary btn-sm" type="button" onclick="openDefenseForm(this)">Назначить задание</button>
<button class="btn btn-secondary btn-sm" type="button" onclick="openModal('modal-defense-import')">Импорт CSV</button>
<input class="input input-sm table-search" data-target="defense-table"
placeholder="Поиск по студенту…" aria-label="Поиск по заданиям защиты">
</div>
</div>
<div class="card card-flush mb-8">
<table id="defense-table">
<thead><tr><th>Студент</th><th>ЛР</th><th>Задание</th><th>Статус</th><th class="text-right">Действия</th></tr></thead>
<table id="defense-table" class="tablekit">
<thead><tr><th class="th-sortable is-asc" title="ФИО — по возрастанию">Студент</th><th>ЛР</th><th>Задание</th><th>Статус</th><th class="text-right" data-nosort>Действия</th></tr></thead>
<tbody>
<tr th:each="d : ${defenseRows}" th:attr="data-lab=${d.labId}">
<td>
@@ -162,10 +166,14 @@
</table>
</div>
<h2 class="section-title">Студенты</h2>
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
<h2 class="section-title m-0">Студенты</h2>
<input class="input input-sm table-search" data-target="students-table"
placeholder="Поиск по студенту…" aria-label="Поиск по студентам">
</div>
<div class="card card-flush">
<table>
<thead><tr><th>№</th><th>ФИО</th><th>Логин</th><th>Основа</th><th>Варианты</th><th>Пароль</th><th class="text-right">Действия</th></tr></thead>
<table id="students-table" class="tablekit">
<thead><tr><th data-nosort>№</th><th class="th-sortable is-asc" title="ФИО — по возрастанию">ФИО</th><th>Логин</th><th>Основа</th><th>Варианты</th><th>Пароль</th><th class="text-right" data-nosort>Действия</th></tr></thead>
<tbody>
<tr th:each="row, iter : ${studentGrades}">
<td class="text-muted" th:text="${iter.count}">1</td>

View File

@@ -22,9 +22,14 @@
<input type="hidden" name="_csrf" th:value="${csrfToken}">
</form>
</div>
<div class="card card-flush mt-8">
<table>
<thead><tr><th>Название</th><th>Студентов</th><th>Выгрузка</th></tr></thead>
<div class="flex flex-wrap items-center justify-between gap-2 mb-2 mt-8">
<h2 class="section-title m-0">Список групп</h2>
<input class="input input-sm table-search" data-target="groups-table"
placeholder="Поиск по названию…" aria-label="Поиск по группам">
</div>
<div class="card card-flush">
<table id="groups-table" class="tablekit">
<thead><tr><th class="th-sortable is-asc" title="Название — по возрастанию">Название</th><th>Студентов</th><th data-nosort>Выгрузка</th></tr></thead>
<tbody>
<tr th:each="group : ${groups}">
<td><a th:href="@{/admin/groups/{id}(id=${group.id})}" th:text="${group.name}">—</a></td>

View File

@@ -71,13 +71,17 @@
</div>
</div>
<div th:each="g : ${assignmentGroups}" class="card mb-4" th:if="${!#lists.isEmpty(g.students)}">
<div th:each="g, gStat : ${assignmentGroups}" class="card mb-4" th:if="${!#lists.isEmpty(g.students)}">
<div class="card-body">
<h3 class="group-title" th:text="${g.groupName}">Группа</h3>
<div class="flex flex-wrap items-center justify-between gap-2 mb-2">
<h3 class="group-title m-0" th:text="${g.groupName}">Группа</h3>
<input class="input input-sm table-search" th:attr="data-target='assign-table-' + ${gStat.index}"
placeholder="Поиск по студенту…" aria-label="Поиск по студентам группы">
</div>
<div class="scroll-x">
<table>
<table class="tablekit" th:attr="id='assign-table-' + ${gStat.index}">
<thead><tr>
<th class="th-w-sm">Выбор</th><th>Студент</th><th class="th-w-md">Уровень</th><th class="th-w-md">Вариант</th>
<th class="th-w-sm" data-nosort>Выбор</th><th class="th-sortable is-asc" title="ФИО — по возрастанию">Студент</th><th class="th-w-md">Уровень</th><th class="th-w-md">Вариант</th>
<th class="th-w-sm">Источник</th><th>Сдача</th>
</tr></thead>
<tbody>

View File

@@ -6,6 +6,8 @@
<div class="flex items-center justify-between gap-4 mb-6">
<h1 class="page-title">Лабораторные</h1>
<div class="flex gap-2">
<input class="input input-sm table-search" data-target="labs-table"
placeholder="Поиск по названию…" aria-label="Поиск по лабораторным">
<button class="btn btn-secondary" type="button" onclick="openModal('modal-import-lab')">Импорт</button>
<button class="btn btn-primary" type="button" onclick="openModal('modal-create-lab')">+ Создать</button>
</div>
@@ -16,8 +18,8 @@
<div th:if="${error}" class="alert alert-error" th:text="${error}"></div>
<div class="card card-flush">
<table>
<thead><tr><th>№</th><th>Название</th><th>Вариантов</th><th>Статус</th><th></th></tr></thead>
<table id="labs-table" class="tablekit">
<thead><tr><th class="th-sortable is-asc" title="Номер — по возрастанию">№</th><th>Название</th><th>Вариантов</th><th>Статус</th><th data-nosort></th></tr></thead>
<tbody>
<tr th:each="lab : ${labs}">
<td th:text="${lab.number}">1</td>