- Dashboard: real stats (groups, students, labs, average grade), labs/groups summary lists, no placeholder tile - Lab page: view + edit combined — metadata form in modal, condition rendered as markdown, per-level variant tables with add/edit/delete dialogs, level rename/delete, student level/variant selectors with group filter - Labs list: create-lab dialog with manual level entry (code; title lines) - Groups: create-group dialog, add-student dialog (auto password, toast card) - Content: public /learn reader is the single place; admin buttons (new article with chapter/section create-or-select, rename/delete chapters and sections, move articles between sections, import zip) appear for admins; removed separate 'Контент' admin section - Buttons use btn classes instead of nav-link - Favicon: generated SVG book icon - Security: CSRF synchronizer token interceptor (replaced spring-security in web which broke logback under nested jars), session regenerated on login, hardened session cookie, gateway actuator limited to health,info - Jackson 3: nullable-safe request DTOs for labs (FAIL_ON_NULL_FOR_PRIMITIVES) - academic: POST /labs (manual create with levels), level add/rename/delete, content chapters/sections CRUD, article create/move/delete endpoints
148 lines
8.4 KiB
HTML
148 lines
8.4 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/base :: layout(~{::title}, ~{::#content})}">
|
||
<head><title th:text="${group.name} + ' — LMS'">Группа</title></head>
|
||
<body>
|
||
<div id="content">
|
||
<a th:href="@{/admin/groups}" class="nav-link">← Все группы</a>
|
||
<div class="mt-4 mb-6 flex flex-wrap items-center justify-between gap-4">
|
||
<div>
|
||
<h1 class="page-title" th:text="${group.name}">Группа</h1>
|
||
<p class="text-sm text-muted mt-1">Студентов: <span th:text="${#lists.size(group.students)}">0</span></p>
|
||
</div>
|
||
<div class="flex gap-2">
|
||
<button class="btn btn-primary" type="button" onclick="openModal('modal-add-student')">+ Студент</button>
|
||
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">Выгрузить XLSX</a>
|
||
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='csv')}">CSV</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div th:if="${notice}" class="alert alert-success" th:text="${notice}"></div>
|
||
<div th:if="${error}" class="alert alert-error" th:text="${error}"></div>
|
||
<div th:if="${resetPassword}" class="alert alert-warning">
|
||
Новый пароль: <code class="code" th:text="${resetPassword}">password</code>
|
||
</div>
|
||
|
||
<div th:if="${importResult}" class="card import-card mb-4">
|
||
<div class="card-body">
|
||
<h2 class="section-title">Импорт: <span th:text="${importResult.groupName}">группа</span></h2>
|
||
<p class="text-sm text-muted">
|
||
Новых студентов: <span th:text="${#lists.size(importResult.added)}">0</span>,
|
||
обновлено: <span th:text="${#lists.size(importResult.updated)}">0</span>.
|
||
</p>
|
||
<div th:if="${not #lists.isEmpty(importResult.added)}" class="mt-4 overflow-x-auto">
|
||
<p class="text-sm alert-warning mini-alert">Сохраните пароли — они показываются для выдачи студентам.</p>
|
||
<table>
|
||
<thead><tr><th>ФИО</th><th>Логин</th><th>Пароль</th></tr></thead>
|
||
<tbody>
|
||
<tr th:each="credential : ${importResult.added}">
|
||
<td th:text="${credential.fullName}">—</td>
|
||
<td class="font-mono" th:text="${credential.studentNo}">—</td>
|
||
<td class="font-mono font-semibold" th:text="${credential.password}">—</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<h2 class="section-title">Баллы за лабораторные</h2>
|
||
<p class="text-sm text-muted mb-4">Введите балл (0–5) и нажмите «OK». Средний балл считается по оценённым лабам.</p>
|
||
<div class="card card-flush mb-6 overflow-x-auto">
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Студент</th>
|
||
<th class="text-center" th:each="lab : ${labs}">
|
||
<a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="|ЛР ${lab.number}|">ЛР</a>
|
||
</th>
|
||
<th class="text-center">Средний</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr th:each="row : ${studentGrades}">
|
||
<td th:text="${row.fullName}">—</td>
|
||
<td th:each="score, iter : ${row.scores}">
|
||
<form class="inline-form" th:action="@{/admin/groups/{id}/grades(id=${group.id})}" method="post">
|
||
<input type="hidden" name="studentId" th:value="${row.studentId}">
|
||
<input type="hidden" name="labId" th:value="${labs[iter.index].id}">
|
||
<input class="input input-sm input-score" name="score" type="number" step="0.1" min="0" max="5"
|
||
th:value="${score}">
|
||
<button class="btn btn-secondary btn-sm" type="submit">OK</button>
|
||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||
</form>
|
||
</td>
|
||
<td class="text-center font-semibold">
|
||
<span th:if="${row.average != null}" th:text="${#numbers.formatDecimal(row.average, 1, 2)}">—</span>
|
||
<span th:if="${row.average == null}" class="text-muted">—</span>
|
||
</td>
|
||
</tr>
|
||
<tr th:if="${#lists.isEmpty(group.students)}">
|
||
<td class="empty" th:colspan="${#lists.size(labs) + 2}">В группе нет студентов.</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<h2 class="section-title">Студенты</h2>
|
||
<div class="card card-flush">
|
||
<table>
|
||
<thead><tr><th>№</th><th>ФИО</th><th>Логин</th><th>Основа</th><th>Пароль</th><th></th></tr></thead>
|
||
<tbody>
|
||
<tr th:each="student, iter : ${group.students}">
|
||
<td class="text-muted" th:text="${iter.count}">1</td>
|
||
<td th:text="${student.fullName}">—</td>
|
||
<td class="font-mono" th:text="${student.studentNo}">—</td>
|
||
<td class="text-muted" th:text="${student.basis}">—</td>
|
||
<td><span th:if="${student.mustChangePassword}" class="badge badge-warn">временный</span>
|
||
<span th:unless="${student.mustChangePassword}" class="badge badge-ok">изменён</span></td>
|
||
<td class="text-center">
|
||
<form th:action="@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${student.id})}" method="post">
|
||
<button class="btn btn-danger btn-sm" type="submit">Сбросить</button>
|
||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<tr th:if="${#lists.isEmpty(group.students)}"><td colspan="6" class="empty">В группе пока нет студентов.</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div th:if="${createdStudent}" class="card import-card toast">
|
||
<div class="card-body">
|
||
<h2 class="section-title">Студент добавлен</h2>
|
||
<p class="text-sm" th:text="${createdStudent.fullName}">—</p>
|
||
<p class="text-sm">Логин: <code class="code font-mono" th:text="${createdStudent.studentNo}">—</code></p>
|
||
<p class="text-sm">Пароль: <code class="code font-mono font-semibold" th:text="${createdStudent.password}">—</code></p>
|
||
</div>
|
||
</div>
|
||
|
||
<dialog id="modal-add-student" class="modal">
|
||
<div class="modal-header">
|
||
<h2 class="modal-title">Добавить студента</h2>
|
||
<button class="modal-close" type="button" onclick="closeModal('modal-add-student')">✕</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form class="form-stack" th:action="@{/admin/groups/{gid}/students/create(gid=${group.id})}" method="post">
|
||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||
<div class="grid grid-2">
|
||
<div><label class="label" for="stNo">Студенческий номер (логин)</label>
|
||
<input class="input" id="stNo" name="studentNo" required></div>
|
||
<div><label class="label" for="stBasis">Основа обучения</label>
|
||
<input class="input" id="stBasis" name="basis" placeholder="бюджет"></div>
|
||
</div>
|
||
<div><label class="label" for="stLast">Фамилия</label>
|
||
<input class="input" id="stLast" name="lastName" required></div>
|
||
<div class="grid grid-2">
|
||
<div><label class="label" for="stFirst">Имя</label>
|
||
<input class="input" id="stFirst" name="firstName"></div>
|
||
<div><label class="label" for="stMiddle">Отчество</label>
|
||
<input class="input" id="stMiddle" name="middleName"></div>
|
||
</div>
|
||
<p class="hint">Пароль будет сгенерирован автоматически.</p>
|
||
<button class="btn btn-primary" type="submit">Добавить</button>
|
||
</form>
|
||
</div>
|
||
</dialog>
|
||
</body>
|
||
</html> |