Admin group: rework lab cells/columns, fix sort toggle and card rounding
This commit is contained in:
@@ -102,7 +102,7 @@ a:not(.btn):hover { color: var(--c-text-link-hover); }
|
||||
|
||||
/* Cards */
|
||||
.card { border-radius: 0.75rem; background: var(--c-surface); border: 1px solid var(--c-border); }
|
||||
.card-flush { border-radius: 0.75rem; background: var(--c-surface); border: 1px solid var(--c-border); padding: 0; }
|
||||
.card-flush { border-radius: 0.75rem; background: var(--c-surface); border: 1px solid var(--c-border); padding: 0; overflow: hidden; }
|
||||
.card-body { padding: 1.5rem; }
|
||||
|
||||
/* Grid */
|
||||
@@ -302,6 +302,36 @@ code.code { padding: 0.125rem 0.5rem; border-radius: 0.25rem; font-family: monos
|
||||
.ac-item { padding: 0.4rem 0.6rem; cursor: pointer; font-size: 0.875rem; }
|
||||
.ac-item:hover, .ac-item.active { background: var(--c-bg); }
|
||||
.ac-empty { padding: 0.4rem 0.6rem; font-size: 0.875rem; color: var(--c-text-muted); }
|
||||
/* Ячейка матрицы: вариант и кнопки действий под ним; кнопки в шапке ЛР */
|
||||
.cell-stack { display: flex; flex-direction: column; align-items: center; gap: 0.3rem; }
|
||||
.variant-btn {
|
||||
display: inline-block; padding: 0.18rem 0.5rem; border-radius: 0.4rem;
|
||||
border: 1px solid var(--c-border); background: var(--c-bg);
|
||||
font-size: 0.8rem; white-space: nowrap;
|
||||
}
|
||||
.variant-btn.manual { border-color: var(--c-border-strong, var(--c-border)); font-weight: 600; }
|
||||
.cell-btns { display: flex; gap: 0.2rem; }
|
||||
.cell-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 0.15rem;
|
||||
min-width: 1.6rem; height: 1.6rem; padding: 0 0.35rem; cursor: pointer;
|
||||
border: 1px solid var(--c-border); border-radius: 0.35rem;
|
||||
background: var(--c-surface); color: var(--c-text); text-decoration: none; font-size: 0.85rem;
|
||||
}
|
||||
.cell-btn:hover { background: var(--c-bg); }
|
||||
.cell-btn.dim { color: var(--c-text-muted); }
|
||||
.cell-btn.warn { color: #d97706; }
|
||||
.cell-btn.ok { color: #16a34a; }
|
||||
.lab-head-actions { display: flex; gap: 0.2rem; }
|
||||
.mini-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
min-width: 1.5rem; height: 1.5rem; padding: 0 0.3rem; cursor: pointer;
|
||||
border: 1px solid var(--c-border); border-radius: 0.35rem;
|
||||
background: var(--c-surface); color: var(--c-text); text-decoration: none; font-size: 0.85rem;
|
||||
}
|
||||
.mini-btn:hover { background: var(--c-bg); }
|
||||
.mini-btn.ok { color: #16a34a; }
|
||||
.mini-btn.warn { color: #d97706; }
|
||||
|
||||
/* Увеличенный диалог (оценка + комментарии) */
|
||||
dialog.modal.modal-lg { width: min(56rem, calc(100vw - 2rem)); }
|
||||
dialog.modal.modal-lg .modal-body { max-height: 78vh; }
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
return table.__headerRow || table.tHead.rows[table.tHead.rows.length - 1];
|
||||
}
|
||||
|
||||
function clearMarkers(table, keep) {
|
||||
Array.prototype.forEach.call(headerRow(table).cells, function (th, i) {
|
||||
if (i !== keep) th.classList.remove('is-asc', 'is-desc');
|
||||
function clearAllMarkers(table) {
|
||||
Array.prototype.forEach.call(headerRow(table).cells, function (th) {
|
||||
th.classList.remove('is-asc', 'is-desc');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@
|
||||
th.addEventListener('click', function (event) {
|
||||
if (event.target.closest('form, button, a, select, input')) return;
|
||||
var asc = !th.classList.contains('is-asc');
|
||||
clearMarkers(table, index);
|
||||
clearAllMarkers(table);
|
||||
th.classList.add(asc ? 'is-asc' : 'is-desc');
|
||||
sortColumn(table, index, asc ? 1 : -1);
|
||||
th.title = (th.textContent.trim() || 'Столбец') + ' — ' + (asc ? 'по возрастанию' : 'по убыванию');
|
||||
|
||||
@@ -48,24 +48,24 @@
|
||||
<th class="matrix-sticky th-sortable is-asc" title="ФИО — по возрастанию">Студент</th>
|
||||
<th th:each="lab : ${labs}" class="text-center lab-col" data-filter th:attr="data-lab=${lab.id}">
|
||||
<div class="lab-col-head">
|
||||
<div class="lab-head-row">
|
||||
<a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="|ЛР ${lab.number}|">ЛР</a>
|
||||
<a th:href="@{/admin/labs/{id}(id=${lab.id})}" th:text="|ЛР ${lab.number}|">ЛР</a>
|
||||
<div class="lab-head-actions">
|
||||
<form class="inline-form" th:action="@{/admin/groups/{id}/publish(id=${group.id})}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<input type="hidden" name="labId" th:value="${lab.id}">
|
||||
<input type="hidden" name="published"
|
||||
th:value="${publishedLabIds.contains(lab.id) ? 'false' : 'true'}">
|
||||
<button class="badge badge-btn badge-sm" type="submit"
|
||||
th:classappend="${publishedLabIds.contains(lab.id)} ? 'badge-ok' : 'badge-warn'"
|
||||
th:title="${publishedLabIds.contains(lab.id) ? 'Скрыть оценки и комментарии от студентов' : 'Показать оценки и комментарии студентам'}"
|
||||
th:text="${publishedLabIds.contains(lab.id) ? 'опубликовано' : 'опубликовать оценки'}">опубликовать оценки</button>
|
||||
<button class="mini-btn" type="submit"
|
||||
th:classappend="${publishedLabIds.contains(lab.id)} ? 'ok' : 'warn'"
|
||||
th:title="${publishedLabIds.contains(lab.id) ? 'Оценки опубликованы — нажмите, чтобы скрыть' : 'Показать оценки и комментарии студентам'}">👁</button>
|
||||
</form>
|
||||
<a class="mini-btn"
|
||||
th:href="'/admin/groups/' + ${group.id} + '/submissions.zip?labId=' + ${lab.id}"
|
||||
th:title="|Скачать решения по ЛР ${lab.number}|">⤓</a>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
<th data-filter>Основа</th>
|
||||
<th>Варианты</th>
|
||||
<th data-filter>Пароль</th>
|
||||
<th data-nosort>Пароль</th>
|
||||
<th class="text-right" data-nosort>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -77,52 +77,48 @@
|
||||
</td>
|
||||
<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-chips">
|
||||
<span class="cell-chip"
|
||||
th:title="(${cell.variantNumber} ? 'Вариант ' + ${cell.variantNumber} : 'Вариант') + ' — ' + (${cell.variantManual} ? 'закреплён вручную (раздача его не тронет)' : 'выдан автоматически')">
|
||||
№<span class="font-mono" th:classappend="${cell.variantManual} ? 'text-strong' : ''"
|
||||
th:text="${cell.variantNumber} ?: '—'">12</span>
|
||||
</span>
|
||||
<button class="cell-chip" type="button"
|
||||
th:classappend="${cell.defenseStatus == null} ? 'chip-dim' : (${cell.defenseStatus == 'ready'} ? 'chip-ok' : 'chip-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}"
|
||||
onclick="openDefense(this)"
|
||||
th:title="${cell.defenseStatus == null} ? 'Назначить задание на защиту' : 'Защита: задание и действия'"
|
||||
th:text="${cell.defenseStatus == null} ? '🛡—' : (${cell.defenseStatus == 'waiting'} ? (${cell.defenseCode != null} ? '🛡🔑' : '🛡⌛') : (${cell.defenseStatus == 'started'} ? '🛡⏳' : '🛡✓'))">🛡—</button>
|
||||
<a th:if="${cell.submissionId}" class="cell-chip chip-link"
|
||||
th:href="@{/admin/submissions/{id}/download(id=${cell.submissionId}, group=${group.name}, no=${row.studentNo}, name=${row.fullName}, attempt=${cell.submissionSeq})}"
|
||||
title="Скачать последнее решение студента">↓<span class="font-mono" th:text="${cell.submissionSeq}">1</span></a>
|
||||
<span th:unless="${cell.submissionId}" class="cell-chip chip-dim" title="Решений пока не сдавалось">↓—</span>
|
||||
<button class="cell-chip" 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)"
|
||||
th:title="${cell.score} ? 'Оценка ' + ${cell.score} + ' — открыть правку оценки и комментариев' : 'Поставить оценку и комментарии'"
|
||||
>✎<span class="font-mono" th:if="${cell.score}" th:text="' ' + ${cell.score}"></span></button>
|
||||
<div class="cell-stack">
|
||||
<span class="variant-btn" th:classappend="${cell.variantManual} ? 'manual' : ''"
|
||||
th:title="${cell.variantManual} ? 'Вариант закреплён вручную (раздача его не тронет)' : 'Вариант выдан автоматически'"
|
||||
th:text="'Вариант ' + (${cell.variantNumber} ?: '—')">Вариант —</span>
|
||||
<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>
|
||||
<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}"
|
||||
onclick="openDefense(this)"
|
||||
th:title="${cell.defenseStatus == null} ? 'Назначить задание на защиту' : 'Защита: задание и действия'">🛡</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)"
|
||||
th:title="${cell.score} ? 'Оценка ' + ${cell.score} + ' — изменить' : 'Выставить балл и комментарии'">✎<span class="font-mono" th:if="${cell.score}" th:text="${cell.score}"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-muted" th:text="${row.basis}">—</td>
|
||||
<td class="text-sm">
|
||||
<span th:if="${!#strings.isEmpty(row.variantsSummary)}" th:text="${row.variantsSummary}">—</span>
|
||||
<span th:if="${#strings.isEmpty(row.variantsSummary)}" class="text-muted">не распределены</span>
|
||||
<td class="text-center">
|
||||
<div class="cell-stack">
|
||||
<span th:if="${row.mustChangePassword}" class="badge badge-warn">временный</span>
|
||||
<span th:unless="${row.mustChangePassword}" class="badge badge-ok">изменён</span>
|
||||
<form class="inline-form" th:action="@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${row.studentId})}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="btn btn-secondary btn-sm" type="submit"
|
||||
title="Сгенерировать новый пароль (покажется один раз, потребуется смена при входе)">Сбросить</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
<td><span th:if="${row.mustChangePassword}" class="badge badge-warn">временный</span>
|
||||
<span th:unless="${row.mustChangePassword}" class="badge badge-ok">изменён</span></td>
|
||||
<td class="text-right actions-cell">
|
||||
<form class="inline-form" th:action="@{/admin/groups/{gid}/students/{sid}/reset-password(gid=${group.id}, sid=${row.studentId})}" method="post">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="btn btn-secondary btn-sm" type="submit"
|
||||
title="Сгенерировать новый пароль (покажется один раз, потребуется смена при входе)">Сбросить пароль</button>
|
||||
</form>
|
||||
<form class="inline-form"
|
||||
th:action="@{/admin/groups/{gid}/students/{sid}/unenroll(gid=${group.id}, sid=${row.studentId})}"
|
||||
method="post"
|
||||
onsubmit="return confirm('Отчислить этого студента из группы? Аккаунт, сдача и оценки сохранятся; студент сможет войти и будет без группы.')">
|
||||
<input type="hidden" name="_csrf" th:value="${csrfToken}">
|
||||
<button class="btn btn-danger btn-sm" type="submit">Отчислить из группы</button>
|
||||
<button class="btn btn-danger btn-sm" type="submit">Отчислить</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(studentGrades)}"><td class="empty" th:attr="colspan=${#lists.size(labs) + 5}">В группе нет студентов.</td></tr>
|
||||
<tr th:if="${#lists.isEmpty(studentGrades)}"><td class="empty" th:attr="colspan=${#lists.size(labs) + 3}">В группе нет студентов.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user