feat(identity): группы-когорты, история и импорт v2
This commit is contained in:
@@ -14,6 +14,11 @@
|
||||
th:text="${(p % 10 == 1 and p % 100 != 11) ? 'лабораторная' : ((p % 10 >= 2 and p % 10 <= 4 and (p % 100 < 12 or p % 100 > 14)) ? 'лабораторные' : 'лабораторных')}">лабораторных</span>
|
||||
с открытыми оценками
|
||||
</p>
|
||||
<p class="meta">
|
||||
<span th:if="${group.enrollmentYear != null}">Набор <b th:text="${group.enrollmentYear}">2025</b> · </span>
|
||||
<span class="badge" th:classappend="${group.status == 'ACTIVE' ? 'badge-ok' : 'badge-warn'}"
|
||||
th:text="${group.status == 'ACTIVE' ? 'активна' : (group.status == 'GRADUATED' ? 'выпущена' : 'архив')}">—</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">
|
||||
@@ -118,6 +123,8 @@
|
||||
aria-label="Статус пароля"></span>
|
||||
<div class="text-sm text-muted font-mono" th:text="${row.studentNo}">—</div>
|
||||
</div>
|
||||
<div th:if="${row.status != 'ACTIVE'}" class="badge badge-warn"
|
||||
th:text="${row.status == 'EXPELLED' ? 'отчислен' : (row.status == 'ACADEMIC_LEAVE' ? 'академ' : (row.status == 'GRADUATED' ? 'выпущен' : 'архив'))}">—</div>
|
||||
</td>
|
||||
<td th:each="cell, iterStat : ${row.cells}" class="text-center cell-lab"
|
||||
th:attr="data-lab=${cell.labId},data-sort=${cell.variantNumber},data-filter-label=${cell.variantNumber != null ? 'вариант ' + cell.variantNumber : 'без варианта'}">
|
||||
@@ -184,7 +191,7 @@
|
||||
title="Сгенерировать новый пароль (покажется один раз, потребуется смена при входе)">Сбросить</button>
|
||||
</form>
|
||||
<form th:action="@{/admin/groups/{gid}/students/{sid}/unenroll(gid=${group.id}, sid=${row.studentId})}"
|
||||
method="post" data-confirm="Отчислить этого студента из группы? Аккаунт, сдача и оценки сохранятся; студент сможет войти и будет без группы.">
|
||||
method="post" data-confirm="Отчислить этого студента из группы? Аккаунт, сдача и оценки сохранятся, но вход в систему будет закрыт.">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Отчислить</button>
|
||||
</form>
|
||||
|
||||
@@ -13,16 +13,45 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div th:if="${importResult != null and !importResult.applied}" class="card card-body mb-6">
|
||||
<h2 class="section-title">Импорт не применён</h2>
|
||||
<p class="text-sm text-muted">Исправьте ошибки в файле и загрузите его снова. Данные не изменены.</p>
|
||||
<div class="scroll-x">
|
||||
<table class="tablekit">
|
||||
<thead><tr><th>Строка</th><th>Ошибка</th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="e : ${importResult.errors}">
|
||||
<td th:text="${e.row}">—</td>
|
||||
<td th:text="${e.message}">—</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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>
|
||||
<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="group : ${groups}">
|
||||
<td><a th:href="@{/admin/groups/{id}(id=${group.id})}" th:text="${group.name}">—</a></td>
|
||||
<td th:text="${group.enrollmentYear} ?: '—'">—</td>
|
||||
<td>
|
||||
<span class="badge" th:classappend="${group.status == 'ACTIVE' ? 'badge-ok' : 'badge-warn'}"
|
||||
th:text="${group.status == 'ACTIVE' ? 'активна' : (group.status == 'GRADUATED' ? 'выпущена' : 'архив')}">—</span>
|
||||
</td>
|
||||
<td th:text="${group.studentCount}">0</td>
|
||||
<td><a th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">XLSX</a><span class="nav-sep ml-2">·</span><a class="ml-2" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='csv')}">CSV</a></td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(groups)}"><td colspan="3" class="empty">Групп пока нет.</td></tr>
|
||||
<tr th:if="${#lists.isEmpty(groups)}"><td colspan="5" class="empty">Групп пока нет.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -34,26 +63,44 @@
|
||||
</div>
|
||||
<div class="dlg-body">
|
||||
<form class="form-stack" th:action="@{/admin/groups}" method="post">
|
||||
<div><label class="label" for="name">Название</label>
|
||||
<input class="input" id="name" name="name" placeholder="ПИ-11БО" required autofocus></div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6"><label class="label" for="stream">Поток</label>
|
||||
<input class="input" id="stream" name="stream" placeholder="ПИ" required autofocus></div>
|
||||
<div class="col-md-6"><label class="label" for="degree">Степень</label>
|
||||
<select class="input" id="degree" name="degree" required>
|
||||
<option value="БО">БО</option>
|
||||
<option value="МО">МО</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4"><label class="label" for="course">Курс обучения</label>
|
||||
<input class="input" id="course" name="course" type="number" min="1" max="4" required></div>
|
||||
<div class="col-md-4"><label class="label" for="number">Номер группы</label>
|
||||
<input class="input" id="number" name="number" type="number" min="1" max="999" required></div>
|
||||
<div class="col-md-4"><label class="label" for="enrollmentYear">Год набора</label>
|
||||
<input class="input" id="enrollmentYear" name="enrollmentYear" type="number" min="2000" max="2100" required></div>
|
||||
</div>
|
||||
<p class="hint">Имя группы будет собрано автоматически: поток + курс + номер + степень, например ПИ-11БО.</p>
|
||||
<button class="btn btn-primary" type="submit">Создать</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="modal-import-group" class="dlg">
|
||||
<dialog id="modal-import-group" class="dlg dlg-lg">
|
||||
<div class="dlg-header">
|
||||
<h2 class="dlg-title">Импорт студентов</h2>
|
||||
<button class="dlg-close" type="button" aria-label="Закрыть">✕</button>
|
||||
</div>
|
||||
<div class="dlg-body">
|
||||
<form class="form-stack" th:action="@{/admin/groups/import}" method="post" enctype="multipart/form-data">
|
||||
<p class="text-sm text-muted">Файл .xls, .xlsx или .csv со столбцами: студенческий номер, ФИО, основа обучения. Логин — студенческий номер, пароль сгенерируется и покажется после импорта.</p>
|
||||
<p class="text-sm text-muted">
|
||||
Файл .xls, .xlsx или .csv. Столбцы:
|
||||
<b>поток, курс, номер, степень, год набора, ФИО, №, база</b>.
|
||||
Все строки — одна группа. Импорт атомарный: при ошибке данные не меняются.
|
||||
</p>
|
||||
<div><label class="label" for="file">Файл со списком</label>
|
||||
<input class="input" id="file" name="file" type="file" accept=".xls,.xlsx,.csv" required autofocus></div>
|
||||
<div><label class="label" for="groupName">Название группы (необязательно)</label>
|
||||
<input class="input" id="groupName" name="name" placeholder="Возьмём из файла"></div>
|
||||
<button class="btn btn-primary" type="submit">Импортировать</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<th class="th-sortable is-asc" title="ФИО — по возрастанию">ФИО</th>
|
||||
<th>Логин</th>
|
||||
<th>Группа</th>
|
||||
<th>Статус</th>
|
||||
<th class="text-right" data-nosort>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -27,6 +28,10 @@
|
||||
<td class="font-medium" th:text="${u.fullName}">—</td>
|
||||
<td class="font-mono" th:text="${u.login}">—</td>
|
||||
<td><a th:if="${u.groupId}" th:href="@{/admin/groups/{id}(id=${u.groupId})}" th:text="${u.groupName}">—</a><span th:unless="${u.groupId}" class="text-muted">без группы</span></td>
|
||||
<td>
|
||||
<span class="badge" th:classappend="${u.status == 'ACTIVE' ? 'badge-ok' : 'badge-warn'}"
|
||||
th:text="${u.status == 'ACTIVE' ? 'активен' : (u.status == 'EXPELLED' ? 'отчислен' : (u.status == 'ACADEMIC_LEAVE' ? 'академ' : (u.status == 'GRADUATED' ? 'выпущен' : 'архив')))}">—</span>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<form class="inline-form" th:action="@{/admin/users/{id}/delete(id=${u.id})}" method="post" data-confirm="Удалить пользователя безвозвратно? Будут удалены аккаунт, группа, оценки с комментариями, сдачи с файлами и задания защит.">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
@@ -35,7 +40,7 @@
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(users)}"><td colspan="4" class="empty">Никого не нашли.</td></tr>
|
||||
<tr th:if="${#lists.isEmpty(users)}"><td colspan="5" class="empty">Никого не нашли.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user