Complete distribution flow and student UI

- progress-service: Distribute variants (round-robin per lab per level),
  student level assignment, API for levels/assignments
- progress-service: Inter-service clients (academic, identity) via RestClient
- web: Admin lab detail page with distribute/level buttons
- web: Student dashboard shows assigned labs from progress-service
- web: ApiClient methods for labs detail, distribute, levels, me/assignments
- academic-service: Add id to LabLevelDetail DTO for progress-service
- Fix: CSS-only layout, footer at bottom via flex column
This commit is contained in:
2026-09-10 22:18:47 +03:00
parent b4c801853a
commit 84532adbff
18 changed files with 753 additions and 26 deletions

View File

@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="ru" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/base :: layout(~{::title}, ~{::#content})}">
<head><title th:text="${lab.title} + ' — LMS'">Лабораторная</title></head>
<body>
<div id="content">
<a th:href="@{/admin/labs}" class="text-sm">← Все лабораторные</a>
<div class="mt-4 mb-6 flex flex-wrap items-center justify-between gap-4">
<div><h1 class="page-title" th:text="|${lab.number}. ${lab.title}|">ЛР 1</h1><p class="text-sm text-muted">Статус: <span class="badge" th:classappend="${lab.status == 'published'} ? 'badge-ok' : 'badge-warn'" th:text="${lab.status}">—</span></p></div>
<form th:action="@{/admin/labs/{id}/distribute(id=${lab.id})}" method="post">
<button class="btn btn-primary" type="submit" name="force" value="false">Распределить</button>
<button class="btn btn-secondary" type="submit" name="force" value="true">Перераспределить (сбросить ручные)</button>
</form>
</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>
<h2 class="section-title">Уровни вариантов</h2>
<div class="card card-flush mt-2 mb-6">
<table>
<thead><tr><th>Код</th><th>Название</th><th>Вариантов</th></tr></thead>
<tbody>
<tr th:each="level : ${lab.levels}">
<td th:text="${level.code}">—</td>
<td th:text="${level.title}">—</td>
<td th:text="${#lists.size(level.variants)}">0</td>
</tr>
</tbody>
</table>
</div>
<h2 class="section-title">Студенты и варианты</h2>
<p class="text-sm text-muted mb-4">Нажмите «Распределить», чтобы закрепить варианты. Можно задать уровень студента.</p>
<div class="card card-flush">
<table>
<thead><tr><th>Студент</th><th>Уровень</th><th>Вариант</th></tr></thead>
<tbody id="student-rows">
<!-- Filled dynamically after distribution -->
</tbody>
</table>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="ru" xmlns:th="http://www.thymeleaf.org" th:replace="~{fragments/base :: layout(~{::title}, ~{::#content})}">
<head><title>Лабораторные — LMS</title></head>
<body>
<div id="content">
<h1 class="page-title">Лабораторные</h1>
<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 class="grid grid-2">
<form class="card form-card form-stack" th:action="@{/admin/labs/import}" method="post" enctype="multipart/form-data">
<h2 class="section-title">Импорт лабораторной</h2>
<p class="text-sm text-muted">Принимаются .zip (lab.yaml + README.md + variants/*.md) или .json (старый формат).</p>
<div><label class="label" for="labFile">Файл</label><input class="input" id="labFile" name="file" type="file" accept=".zip,.json" required></div>
<button class="btn btn-primary" type="submit">Импортировать</button>
</form>
</div>
<div class="card card-flush mt-8">
<table>
<thead><tr><th>№</th><th>Название</th><th>Семестр</th><th>Вариантов</th><th>Статус</th><th></th></tr></thead>
<tbody>
<tr th:each="lab : ${labs}">
<td th:text="${lab.number}">1</td>
<td th:text="${lab.title}">—</td>
<td class="text-muted" th:text="${lab.semesterTitle}">—</td>
<td th:text="${lab.variantCount}">0</td>
<td><span class="badge" th:classappend="${lab.status == 'published'} ? 'badge-ok' : 'badge-warn'" th:text="${lab.status}">—</span></td>
<td class="text-center">
<form th:action="@{/admin/labs/{id}/delete(id=${lab.id})}" method="post" onsubmit="return confirm('Удалить лабораторную ${lab.title}?')">
<button class="nav-link text-sm" type="submit">Удалить</button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(labs)}"><td colspan="6" class="empty">Лабораторных пока нет.</td></tr>
</tbody>
</table>
</div>
</div>
</body>
</html>