Files
LMS/web/src/main/resources/templates/admin/group.html
Vladislav Kopnin 006951adae UI rework: modal dialogs, inline lab editing, manual creation, content management
- 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
2026-09-11 16:00:56 +03:00

148 lines
8.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>