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
This commit is contained in:
@@ -4,24 +4,51 @@
|
||||
<body>
|
||||
<div id="content">
|
||||
<h1 class="page-title">Панель преподавателя</h1>
|
||||
|
||||
<div class="grid grid-stats">
|
||||
<div class="card card-body"><p class="stat-title">Группы</p><p class="stat-value" th:text="${groupCount}">0</p></div>
|
||||
<div class="card card-body"><p class="stat-title">Студенты</p><p class="stat-value" th:text="${studentCount}">0</p></div>
|
||||
<div class="card card-body"><p class="stat-title">Лабораторные</p><p class="stat-value text-muted">скоро</p></div>
|
||||
<div class="card card-body"><p class="stat-title">Лабораторные</p><p class="stat-value" th:text="${labCount}">0</p>
|
||||
<p class="hint" th:if="${labCount > 0}">опубликовано: <span th:text="${publishedLabs}">0</span></p></div>
|
||||
<div class="card card-body"><p class="stat-title">Средний балл</p>
|
||||
<p class="stat-value" th:if="${avgGrade != null}" th:text="${#numbers.formatDecimal(avgGrade, 1, 2)}">—</p>
|
||||
<p class="stat-value text-muted" th:if="${avgGrade == null}">—</p></div>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="section-title">Группы</h2>
|
||||
<a th:href="@{/admin/groups}" class="text-sm">Все группы →</a>
|
||||
|
||||
<div class="grid grid-2 mt-8">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="section-title">Лабораторные</h2>
|
||||
<a th:href="@{/admin/labs}" class="text-sm">Все →</a>
|
||||
</div>
|
||||
<div class="card card-flush">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr th:each="lab : ${labs}">
|
||||
<td><a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="|ЛР ${lab.number}. ${lab.title}|">—</a></td>
|
||||
<td class="text-muted" th:text="${lab.variantCount} + ' вар.'">—</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(labs)}"><td class="empty">Лабораторных нет.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card card-flush">
|
||||
<table>
|
||||
<thead><tr><th>Название</th><th>Студентов</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.studentCount}">0</td></tr>
|
||||
<tr th:if="${#lists.isEmpty(groups)}"><td colspan="2" class="empty">Групп пока нет.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="section-title">Группы</h2>
|
||||
<a th:href="@{/admin/groups}" class="text-sm">Все →</a>
|
||||
</div>
|
||||
<div class="card card-flush">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr th:each="group : ${groups}">
|
||||
<td><a th:href="@{/admin/groups/{id}(id=${group.id})}" th:text="${group.name}">—</a></td>
|
||||
<td class="text-muted" th:text="${group.studentCount} + ' студ.'">—</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(groups)}"><td class="empty">Групп нет.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<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>
|
||||
@@ -67,6 +68,7 @@
|
||||
<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">
|
||||
@@ -95,7 +97,8 @@
|
||||
<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="nav-link text-sm" type="submit">Сбросить</button>
|
||||
<button class="btn btn-danger btn-sm" type="submit">Сбросить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -104,5 +107,42 @@
|
||||
</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>
|
||||
@@ -11,6 +11,7 @@
|
||||
<h2 class="section-title">Создать группу</h2>
|
||||
<div><label class="label" for="name">Название</label><input class="input" id="name" name="name" placeholder="ПИ-11БО" required></div>
|
||||
<button class="btn btn-primary" type="submit">Создать</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
<form class="card form-card form-stack" th:action="@{/admin/groups/import}" method="post" enctype="multipart/form-data">
|
||||
<h2 class="section-title">Импорт студентов</h2>
|
||||
@@ -18,6 +19,7 @@
|
||||
<div><label class="label" for="file">Файл со списком</label><input class="input" id="file" name="file" type="file" accept=".xls,.xlsx,.csv" required></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>
|
||||
</div>
|
||||
<div class="card card-flush mt-8">
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<textarea class="input" id="descriptionMd" name="descriptionMd" rows="14"
|
||||
th:text="${lab.descriptionMd}"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
|
||||
<h2 class="section-title mt-8">Варианты</h2>
|
||||
@@ -52,10 +53,12 @@
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
</div>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
<form th:action="@{/admin/labs/{labId}/variants/{vid}/delete(labId=${lab.id}, vid=${v.id})}" method="post"
|
||||
onsubmit="return confirm('Удалить вариант?')">
|
||||
<button class="btn btn-secondary btn-sm" type="submit">Удалить вариант</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -75,6 +78,7 @@
|
||||
<div><label class="label" for="newStatement">Условие (markdown)</label>
|
||||
<textarea class="input" id="newStatement" name="statementMd" rows="6"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Добавить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -6,39 +6,83 @@
|
||||
<a th:href="@{/admin/labs}" class="nav-link">← Все лабораторные</a>
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mt-4 mb-6">
|
||||
<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>
|
||||
<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>
|
||||
<span th:if="${lab.deadlineAt != null}"> · Срок: <span th:text="${lab.deadlineAt}"></span></span>
|
||||
<span th:if="${lab.defenseAt != null}"> · Защита: <span th:text="${lab.defenseAt}"></span></span>
|
||||
· Максимум: <span th:text="${lab.pointsMax}"></span> б.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<a class="btn btn-secondary" th:href="@{/admin/labs/{id}/edit(id=${lab.id})}">Редактировать</a>
|
||||
<button class="btn btn-secondary" type="button" onclick="openModal('modal-meta')">Править описание</button>
|
||||
<form th:action="@{/admin/labs/{id}/distribute(id=${lab.id})}" method="post">
|
||||
<button class="btn btn-primary" type="submit" name="force" value="false">Распределить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
<form th:action="@{/admin/labs/{id}/distribute(id=${lab.id})}" method="post">
|
||||
<button class="btn btn-secondary" type="submit" name="force" value="true">Перераспределить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</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>
|
||||
|
||||
<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 class="font-mono" th:text="${level.code}">—</td>
|
||||
<td th:text="${level.title}">—</td>
|
||||
<td th:text="${#lists.size(level.variants)}">0</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(lab.levels)}"><td colspan="3" class="empty">Уровни не найдены.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<h2 class="section-title">Условие</h2>
|
||||
<div class="card card-body md-body mb-6" th:utext="${descriptionHtml}"></div>
|
||||
|
||||
<div class="flex items-center justify-between gap-4 mb-4">
|
||||
<h2 class="section-title">Студенты и варианты</h2>
|
||||
<h2 class="section-title">Уровни и варианты</h2>
|
||||
<button class="btn btn-secondary" type="button" onclick="openModal('modal-add-level')">+ Уровень</button>
|
||||
</div>
|
||||
|
||||
<div th:each="level : ${lab.levels}" class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-4">
|
||||
<h3 class="group-title">
|
||||
<span class="font-mono text-muted" th:text="${level.code}">basic</span> —
|
||||
<span th:text="${level.title}">Уровень</span>
|
||||
(<span th:text="${#lists.size(level.variants)}">0</span> вар.)
|
||||
</h3>
|
||||
<div class="flex gap-2">
|
||||
<button class="btn btn-secondary btn-sm" type="button"
|
||||
th:onclick="openModal('modal-rename-level-'+${level.id})">Переименовать</button>
|
||||
<form th:action="@{/admin/labs/{labId}/levels/{lid}/delete(labId=${lab.id}, lid=${level.id})}" method="post"
|
||||
onsubmit="return confirm('Удалить уровень без вариантов?')">
|
||||
<button class="btn btn-danger btn-sm" type="submit">Удалить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
<button class="btn btn-primary btn-sm" type="button"
|
||||
th:onclick="openModal('modal-add-variant-'+${level.id})">+ Вариант</button>
|
||||
</div>
|
||||
</div>
|
||||
<table>
|
||||
<thead><tr><th>№</th><th>Название</th><th>Условие</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
<tr th:each="v : ${level.variants}">
|
||||
<td class="font-mono" th:text="${v.number}">—</td>
|
||||
<td th:text="${v.title}">—</td>
|
||||
<td class="text-muted" th:text="${#strings.abbreviate(v.statementMd, 60)}">—</td>
|
||||
<td class="text-center">
|
||||
<button class="btn btn-secondary btn-sm" type="button"
|
||||
th:onclick="openModal('modal-edit-variant-'+${v.id})">Править</button>
|
||||
<form class="inline-form" th:action="@{/admin/labs/{labId}/variants/{vid}/delete(labId=${lab.id}, vid=${v.id})}" method="post"
|
||||
onsubmit="return confirm('Удалить вариант?')">
|
||||
<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(level.variants)}"><td colspan="4" class="empty">Вариантов пока нет.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="section-title">Студенты и варианты</h2>
|
||||
<div class="flex items-center justify-between gap-4 mb-4">
|
||||
<p class="text-sm text-muted">Уровень по умолчанию — самый простой. Селекты: выдать вариант посложнее или назначить вручную.</p>
|
||||
<label class="text-sm text-muted">Группа:
|
||||
<select id="group-filter" class="input filter-select" onchange="filterGroups(this.value)">
|
||||
<option value="">Все</option>
|
||||
@@ -46,8 +90,6 @@
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-sm text-muted mb-4">Уровень по умолчанию — самый простой. Селекты позволяют выдать вариант посложнее или назначить вариант вручную.</p>
|
||||
|
||||
<div class="group-section" th:each="g : ${groups}" th:data-group="${g.groupName}">
|
||||
<h3 class="group-title" th:text="${g.groupName}">Группа</h3>
|
||||
<div class="card card-flush mb-6">
|
||||
@@ -65,6 +107,7 @@
|
||||
th:selected="${level.id == s.levelId}">—</option>
|
||||
</select>
|
||||
<button class="btn btn-secondary btn-sm" type="submit">OK</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
@@ -78,6 +121,7 @@
|
||||
</optgroup>
|
||||
</select>
|
||||
<button class="btn btn-secondary btn-sm" type="submit">OK</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</td>
|
||||
<td><span th:if="${s.variantNumber != null}" class="font-mono" th:text="${s.variantNumber}">—</span></td>
|
||||
@@ -88,13 +132,126 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function filterGroups(name) {
|
||||
document.querySelectorAll('.group-section').forEach(function (el) {
|
||||
el.classList.toggle('hidden', !!name && el.getAttribute('data-group') !== name);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
<dialog id="modal-meta" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Правка лабораторной</h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal('modal-meta')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/labs/{id}/edit(id=${lab.id})}" method="post">
|
||||
<div><label class="label" for="metaTitle">Название</label>
|
||||
<input class="input" id="metaTitle" name="title" th:value="${lab.title}" required></div>
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="metaDeadline">Срок сдачи</label>
|
||||
<input class="input" id="metaDeadline" name="deadlineAt" type="datetime-local" th:value="${deadlineLocal}"></div>
|
||||
<div><label class="label" for="metaDefense">Дата защиты</label>
|
||||
<input class="input" id="metaDefense" name="defenseAt" type="datetime-local" th:value="${defenseLocal}"></div>
|
||||
</div>
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="metaPoints">Максимум баллов</label>
|
||||
<input class="input" id="metaPoints" name="pointsMax" type="number" step="0.1" min="0" th:value="${lab.pointsMax}" required></div>
|
||||
<div><label class="label" for="metaStatus">Статус</label>
|
||||
<select class="input" id="metaStatus" name="status">
|
||||
<option value="draft" th:selected="${lab.status == 'draft'}">draft</option>
|
||||
<option value="published" th:selected="${lab.status == 'published'}">published</option>
|
||||
<option value="archived" th:selected="${lab.status == 'archived'}">archived</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<div><label class="label" for="metaDesc">Условие (markdown)</label>
|
||||
<textarea class="input" id="metaDesc" name="descriptionMd" rows="12" th:text="${lab.descriptionMd}"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="modal-add-level" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Добавить уровень</h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal('modal-add-level')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/labs/{labId}/levels/save(labId=${lab.id})}" method="post">
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="lvlCode">Код</label>
|
||||
<input class="input" id="lvlCode" name="code" placeholder="pro" required></div>
|
||||
<div><label class="label" for="lvlTitle">Название</label>
|
||||
<input class="input" id="lvlTitle" name="title" placeholder="Профессиональный" required></div>
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">Добавить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog th:each="level : ${lab.levels}" th:id="|modal-rename-level-${level.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Переименовать уровень</h2>
|
||||
<button class="modal-close" type="button" th:onclick="closeModal('modal-rename-level-'+[[${level.id}]])">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/labs/{labId}/levels/save(labId=${lab.id})}" method="post">
|
||||
<input type="hidden" name="levelId" th:value="${level.id}">
|
||||
<input type="hidden" name="code" th:value="${level.code}">
|
||||
<div><label class="label">Название</label>
|
||||
<input class="input" name="title" th:value="${level.title}" required></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog th:each="level : ${lab.levels}" th:id="|modal-add-variant-${level.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Новый вариант — <span th:text="${level.title}">уровень</span></h2>
|
||||
<button class="modal-close" type="button" th:onclick="closeModal('modal-add-variant-'+[[${level.id}]])">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}" method="post">
|
||||
<input type="hidden" name="labLevelId" th:value="${level.id}">
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label">Номер</label><input class="input" name="number" required></div>
|
||||
<div><label class="label">Название</label><input class="input" name="title"></div>
|
||||
</div>
|
||||
<div><label class="label">Условие (markdown)</label>
|
||||
<textarea class="input" name="statementMd" rows="8"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Добавить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<th:block th:each="level : ${lab.levels}">
|
||||
<dialog th:each="v : ${level.variants}" th:id="|modal-edit-variant-${v.id}|" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Правка варианта</h2>
|
||||
<button class="modal-close" type="button" th:onclick="closeModal('modal-edit-variant-'+[[${v.id}]])">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/labs/{labId}/variants/save(labId=${lab.id})}" method="post">
|
||||
<input type="hidden" name="variantId" th:value="${v.id}">
|
||||
<input type="hidden" name="labLevelId" th:value="${level.id}">
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label">Номер</label><input class="input" name="number" th:value="${v.number}" required></div>
|
||||
<div><label class="label">Название</label><input class="input" name="title" th:value="${v.title}"></div>
|
||||
</div>
|
||||
<div><label class="label">Условие (markdown)</label>
|
||||
<textarea class="input" name="statementMd" rows="8" th:text="${v.statementMd}"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</th:block>
|
||||
|
||||
<script>
|
||||
function filterGroups(name) {
|
||||
document.querySelectorAll('.group-section').forEach(function (el) {
|
||||
el.classList.toggle('hidden', !!name && el.getAttribute('data-group') !== name);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,18 +3,14 @@
|
||||
<head><title>Лабораторные — LMS</title></head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<h1 class="page-title">Лабораторные</h1>
|
||||
<div class="flex items-center justify-between gap-4 mb-6">
|
||||
<h1 class="page-title">Лабораторные</h1>
|
||||
<button class="btn btn-primary" type="button" onclick="openModal('modal-create-lab')">+ Создать</button>
|
||||
</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 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">
|
||||
|
||||
<div class="card card-flush">
|
||||
<table>
|
||||
<thead><tr><th>№</th><th>Название</th><th>Вариантов</th><th>Статус</th><th></th></tr></thead>
|
||||
<tbody>
|
||||
@@ -23,12 +19,49 @@
|
||||
<td><a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="${lab.title}">—</a></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"><button class="nav-link text-sm" type="submit">Удалить</button></form></td>
|
||||
<td class="text-center"><form th:action="@{/admin/labs/{id}/delete(id=${lab.id})}" method="post"><input type="hidden" name="_csrf" th:value="${csrfToken}"><button class="btn btn-danger btn-sm" type="submit">Удалить</button></form></td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(labs)}"><td colspan="5" class="empty">Лабораторных пока нет.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<dialog id="modal-create-lab" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Новая лабораторная</h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal('modal-create-lab')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" th:action="@{/admin/labs/create}" method="post">
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="labNumber">Номер</label>
|
||||
<input class="input" id="labNumber" name="number" type="number" min="1" required></div>
|
||||
<div><label class="label" for="labPoints">Максимум баллов</label>
|
||||
<input class="input" id="labPoints" name="pointsMax" type="number" step="0.1" min="0" value="5" required></div>
|
||||
</div>
|
||||
<div><label class="label" for="labTitle">Название</label>
|
||||
<input class="input" id="labTitle" name="title" required></div>
|
||||
<div class="grid grid-2">
|
||||
<div><label class="label" for="labDeadline">Срок сдачи</label>
|
||||
<input class="input" id="labDeadline" name="deadlineAt" type="datetime-local"></div>
|
||||
<div><label class="label" for="labDefense">Дата защиты</label>
|
||||
<input class="input" id="labDefense" name="defenseAt" type="datetime-local"></div>
|
||||
</div>
|
||||
<div><label class="label" for="labStatus">Статус</label>
|
||||
<select class="input" id="labStatus" name="status">
|
||||
<option value="draft">draft</option>
|
||||
<option value="published">published</option>
|
||||
</select></div>
|
||||
<div><label class="label" for="labLevels">Уровни (по одному на строку: код; название)</label>
|
||||
<textarea class="input" id="labLevels" name="levels" rows="4" placeholder="basic; Основная группа advanced; Повышенная сложность"></textarea>
|
||||
<p class="hint">Если оставить пустым, будет создан уровень «Основная группа». Варианты добавите на странице лабы.</p></div>
|
||||
<div><label class="label" for="labDesc">Условие (markdown, можно позже)</label>
|
||||
<textarea class="input" id="labDesc" name="descriptionMd" rows="6"></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Создать</button>
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user