Admin group: variant modal, cell badges/counters, header cleanup, bigger modal
This commit is contained in:
@@ -11,14 +11,8 @@
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button class="btn btn-primary" type="button" onclick="openModal('modal-add-student')">+ Студент</button>
|
||||
<button class="btn btn-secondary" type="button"
|
||||
th:disabled="${#lists.isEmpty(candidates)}"
|
||||
onclick="openModal('modal-enroll-student')">Добавить существующего</button>
|
||||
<button class="btn btn-secondary" type="button" onclick="openDefense(this)">Назначить задание</button>
|
||||
<button class="btn btn-secondary" type="button" onclick="openModal('modal-defense-import')">Импорт CSV</button>
|
||||
<a class="btn btn-secondary" th:href="'/admin/groups/' + ${group.id} + '/submissions.zip'">Скачать все работы</a>
|
||||
<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>
|
||||
<a class="btn btn-secondary" th:href="@{/admin/groups/{id}/credentials(id=${group.id}, format='xlsx')}">Скачать пароли</a>
|
||||
<button class="btn btn-secondary" type="button" onclick="openModal('modal-defense-import')">Импорт заданий (CSV)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -27,17 +21,12 @@
|
||||
<div class="flex flex-wrap items-center gap-2" th:if="${!#lists.isEmpty(labs)}">
|
||||
<input class="input input-sm table-search" data-target="group-table"
|
||||
placeholder="Поиск по студенту…" aria-label="Поиск по студенту">
|
||||
<label class="label m-0" for="lab-focus">Показать:</label>
|
||||
<select class="input input-sm" id="lab-focus" onchange="focusLab()">
|
||||
<option value="">все лабораторные</option>
|
||||
<option th:each="lab : ${labs}" th:value="${lab.id}" th:text="|ЛР ${lab.number}|">—</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm text-muted mb-4">
|
||||
Ячейка: «№» — вариант, «🛡» — защита (клик — задание и код), «↓» — сдача, «✎» — оценка.
|
||||
Раздача вариантов — на странице лабораторной.
|
||||
</p>
|
||||
|
||||
<div th:if="${#lists.isEmpty(labs)}" class="card card-body mb-6 text-muted">Опубликованных лабораторных пока нет.</div>
|
||||
<div class="card card-flush" th:if="${!#lists.isEmpty(labs)}">
|
||||
@@ -57,11 +46,14 @@
|
||||
th:value="${publishedLabIds.contains(lab.id) ? 'false' : 'true'}">
|
||||
<button class="mini-btn" type="submit"
|
||||
th:classappend="${publishedLabIds.contains(lab.id)} ? 'ok' : 'warn'"
|
||||
th:title="${publishedLabIds.contains(lab.id) ? 'Оценки опубликованы — нажмите, чтобы скрыть' : 'Показать оценки и комментарии студентам'}">👁</button>
|
||||
th:title="${publishedLabIds.contains(lab.id) ? 'Оценки опубликованы — нажмите, чтобы скрыть' : 'Показать оценки и комментарии студентам'}">
|
||||
<svg th:if="${publishedLabIds.contains(lab.id)}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></svg>
|
||||
<svg th:unless="${publishedLabIds.contains(lab.id)}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/><line x1="3" y1="3" x2="21" y2="21"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
<a class="mini-btn"
|
||||
th:href="'/admin/groups/' + ${group.id} + '/submissions.zip?labId=' + ${lab.id}"
|
||||
th:title="|Скачать решения по ЛР ${lab.number}|">⤓</a>
|
||||
th:title="|Скачать решения по ЛР ${lab.number}|">⤓<span class="mini-count" th:text="${labSubmissionCounts[lab.id] ?: 0}">0</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
@@ -78,18 +70,27 @@
|
||||
<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 : 'без варианта'}">
|
||||
<div class="cell-stack">
|
||||
<span class="variant-btn" th:classappend="${cell.variantManual} ? 'manual' : ''"
|
||||
th:title="${cell.variantManual} ? 'Вариант закреплён вручную (раздача его не тронет)' : 'Вариант выдан автоматически'"
|
||||
th:text="'Вариант ' + (${cell.variantNumber} ?: '—')">Вариант —</span>
|
||||
<button class="variant-btn" type="button"
|
||||
th:classappend="${cell.variantManual} ? 'manual' : ''"
|
||||
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-variant=${cell.variantId}"
|
||||
onclick="openVariant(this)"
|
||||
title="Назначить вариант вручную или выдать случайный">Вариант <b th:text="${cell.variantNumber} ?: '—'">—</b></button>
|
||||
<div class="cell-btns">
|
||||
<a th:if="${cell.submissionId}" class="cell-btn" title="Скачать последнее решение"
|
||||
th:href="@{/admin/submissions/{id}/download(id=${cell.submissionId}, group=${group.name}, no=${row.studentNo}, name=${row.fullName}, attempt=${cell.submissionSeq})}">↓</a>
|
||||
th:href="@{/admin/submissions/{id}/download(id=${cell.submissionId}, group=${group.name}, no=${row.studentNo}, name=${row.fullName}, attempt=${cell.submissionSeq})}">↓<span class="cell-num" th:text="${cell.submissionSeq}"></span></a>
|
||||
<span th:unless="${cell.submissionId}" class="cell-btn dim" title="Решений пока не сдавалось">↓</span>
|
||||
<button class="cell-btn" type="button"
|
||||
th:classappend="${cell.defenseStatus == null} ? 'dim' : (${cell.defenseStatus == 'ready'} ? 'ok' : 'warn')"
|
||||
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-md=${cell.defenseAssignmentMd},data-session=${cell.defenseSessionId},data-status=${cell.defenseStatus}"
|
||||
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-md=${cell.defenseAssignmentMd},data-session=${cell.defenseSessionId},data-status=${cell.defenseStatus},data-seconds=${cell.defenseSecondsLeft},data-code=${cell.defenseCode}"
|
||||
onclick="openDefense(this)"
|
||||
th:title="${cell.defenseStatus == null} ? 'Назначить задание на защиту' : 'Защита: задание и действия'">🛡</button>
|
||||
th:title="${cell.defenseStatus == null} ? 'Задание на защиту не назначено' : 'Защита: задание и действия'">
|
||||
<span>🛡</span>
|
||||
<span th:if="${cell.defenseStatus == null}">нет</span>
|
||||
<span th:if="${cell.defenseStatus == 'waiting' and cell.defenseCode == null}">ожид.</span>
|
||||
<span th:if="${cell.defenseStatus == 'waiting' and cell.defenseCode != null}">код</span>
|
||||
<span th:if="${cell.defenseStatus == 'started'}" class="defense-cell-timer" th:attr="data-seconds=${cell.defenseSecondsLeft}">идёт</span>
|
||||
<span th:if="${cell.defenseStatus == 'ready'}">готов</span>
|
||||
</button>
|
||||
<button class="cell-btn" type="button"
|
||||
th:attr="data-student=${row.studentId},data-student-name=${row.fullName},data-lab=${cell.labId},data-lab-name='ЛР ' + ${cell.labNumber},data-score=${cell.score},data-comments=${row.commentsJson},data-ci=${iterStat.index}"
|
||||
onclick="openGradeEditor(this)"
|
||||
@@ -149,6 +150,39 @@
|
||||
</dialog>
|
||||
|
||||
<!-- Диалог: индивидуальное задание (защита) -->
|
||||
<!-- Диалог: назначение варианта -->
|
||||
<dialog id="modal-variant" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Назначить вариант</h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal('modal-variant')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body form-stack">
|
||||
<p class="text-sm text-muted" id="variant-who">—</p>
|
||||
<div><label class="label" for="variant-level">Уровень</label>
|
||||
<select class="input" id="variant-level"></select></div>
|
||||
<div><label class="label" for="variant-variant">Вариант</label>
|
||||
<select class="input" id="variant-variant"></select></div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<form class="inline-form" id="variant-random-form" method="post"
|
||||
th:action="@{/admin/groups/{gid}/set-level(gid=${group.id})}">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="studentId" id="variant-random-student">
|
||||
<input type="hidden" name="labId" id="variant-random-lab">
|
||||
<input type="hidden" name="labLevelId" id="variant-random-level">
|
||||
<button class="btn btn-primary" type="submit">Выдать случайный из уровня</button>
|
||||
</form>
|
||||
<form class="inline-form" id="variant-pin-form" method="post"
|
||||
th:action="@{/admin/groups/{gid}/pin-variant(gid=${group.id})}">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="studentId" id="variant-pin-student">
|
||||
<input type="hidden" name="labId" id="variant-pin-lab">
|
||||
<input type="hidden" name="variantId" id="variant-pin-id">
|
||||
<button class="btn btn-secondary" type="submit">Закрепить выбранный</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="modal-defense" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Индивидуальное задание</h2>
|
||||
@@ -173,37 +207,29 @@
|
||||
<textarea class="input" id="defense-md" name="assignmentMd" rows="10" required></textarea></div>
|
||||
<button class="btn btn-primary" type="submit">Сохранить задание</button>
|
||||
</form>
|
||||
<div id="defense-actions" class="mt-3" hidden>
|
||||
<div id="defense-actions" class="mt-4" hidden>
|
||||
<p id="defense-status" class="text-sm text-muted mb-2"></p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<form class="inline-form" id="defense-issue-form" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="labId" id="defense-action-lab">
|
||||
<input type="hidden" name="groupId" th:value="${group.id}">
|
||||
<button class="btn btn-secondary btn-sm" type="submit">выдать код</button>
|
||||
<button class="btn btn-primary" type="submit">Выдать код</button>
|
||||
</form>
|
||||
<button class="btn btn-secondary btn-sm" type="button" onclick="showDefenseFromModal()">текст задания</button>
|
||||
<form class="inline-form" id="defense-reset-form" method="post"
|
||||
onsubmit="return confirm('Открыть защиту заново для нового (дополнительного) задания?')">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="labId" id="defense-reset-lab">
|
||||
<input type="hidden" name="groupId" th:value="${group.id}">
|
||||
<button class="btn btn-danger btn-sm" type="submit">начать заново</button>
|
||||
<button class="btn btn-danger" type="submit">Начать заново</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<!-- Диалог просмотра текста задания -->
|
||||
<dialog id="modal-defense-text" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title" id="defense-text-title">Текст задания</h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal('modal-defense-text')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre id="defense-text-body" class="pre-wrap"></pre>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
<!-- Диалог импорта CSV -->
|
||||
<dialog id="modal-defense-import" class="modal">
|
||||
@@ -240,7 +266,20 @@
|
||||
<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">
|
||||
<form class="form-stack" id="enroll-form" method="post"
|
||||
th:action="@{/admin/groups/{gid}/students/enroll(gid=${group.id})}">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div><label class="label" for="enroll-select">Существующий студент</label>
|
||||
<select class="input" id="enroll-select" name="studentId" required>
|
||||
<option value="">— выберите студента —</option>
|
||||
<option th:each="c : ${candidates}" th:value="${c.id}"
|
||||
th:text="${c.fullName} + ' (' + ${c.studentNo} + ')' + (${c.currentGroupName} ? ' — из ' + ${c.currentGroupName} : ' — без группы')">
|
||||
</option>
|
||||
</select></div>
|
||||
<p class="hint">Если студент уже в другой группе — он будет переведён в эту.</p>
|
||||
<button class="btn btn-secondary" type="submit" th:disabled="${#lists.isEmpty(candidates)}">Зачислить существующего</button>
|
||||
</form>
|
||||
<form class="form-stack mt-4" 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>
|
||||
@@ -256,34 +295,13 @@
|
||||
<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>
|
||||
<p class="hint">Новому студенту пароль будет сгенерирован автоматически.</p>
|
||||
<button class="btn btn-primary" type="submit">Создать нового</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<dialog id="modal-enroll-student" class="modal">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">Добавить существующего студента</h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal('modal-enroll-student')">✕</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="form-stack" id="enroll-form" method="post"
|
||||
th:action="@{/admin/groups/{gid}/students/enroll(gid=${group.id})}">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<div><label class="label" for="enroll-select">Студент</label>
|
||||
<select class="input" id="enroll-select" name="studentId" required>
|
||||
<option value="">— выберите студента —</option>
|
||||
<option th:each="c : ${candidates}" th:value="${c.id}"
|
||||
th:text="${c.fullName} + ' (' + ${c.studentNo} + ')' + (${c.currentGroupName} ? ' — из ' + ${c.currentGroupName} : ' — без группы')">
|
||||
</option>
|
||||
</select></div>
|
||||
<p class="hint">Если студент уже в другой группе — он будет переведён в эту.
|
||||
Отчисление не удаляет аккаунт, студент сможет войти и после перевода.</p>
|
||||
<button class="btn btn-primary" type="submit">Зачислить</button>
|
||||
</form>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
|
||||
<div th:if="${createdStudent}" class="card import-card toast">
|
||||
<div class="card-body">
|
||||
|
||||
Reference in New Issue
Block a user