поправил вид сетки и прочие баги

This commit is contained in:
2026-05-28 00:47:51 +03:00
parent bbfee068da
commit 5b6ffa5c08
10 changed files with 338 additions and 77 deletions

View File

@@ -582,6 +582,12 @@
animation-duration: 0.28s;
}
.schedule-view-results-card {
display: flex;
flex-direction: column;
gap: 1rem;
}
.schedule-view-control-header,
.schedule-view-results-header,
.schedule-view-table-header {
@@ -589,6 +595,7 @@
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
flex-wrap: wrap;
}
.schedule-view-control-header h2,
@@ -684,10 +691,11 @@
.schedule-view-result-tabs {
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
margin-top: 1rem;
margin-top: 0;
padding-bottom: 0.1rem;
overflow-x: auto;
overflow-x: visible;
}
.schedule-view-result-tab {

View File

@@ -194,10 +194,6 @@
color: var(--text-secondary);
}
.department-workload-list {
margin-top: 1rem;
}
.department-comment-list {
display: grid;
gap: 0.75rem;
@@ -262,6 +258,8 @@
.schedule-slots-panel {
margin-top: 1rem;
margin-bottom: 1.25rem;
min-width: 0;
}
.schedule-slots-list {
@@ -269,6 +267,7 @@
flex-direction: column;
gap: 0.75rem;
margin-top: 0.75rem;
overflow: visible;
}
.schedule-slot-row {
@@ -282,10 +281,19 @@
border-radius: var(--radius-sm);
}
.schedule-slot-row:last-child {
margin-bottom: 0;
}
.schedule-slot-row.has-subgroup {
grid-template-columns: repeat(8, minmax(125px, 1fr)) auto;
}
.schedule-rule-actions {
margin-top: 0.25rem;
clear: both;
}
.slot-subgroup-multi .select-text {
overflow: hidden;
text-overflow: ellipsis;
@@ -780,11 +788,15 @@
align-items: stretch;
gap: 0.65rem;
padding: 0.65rem;
overflow-x: hidden;
overflow-y: hidden;
}
.calendar-semester-block {
display: flex;
flex-direction: column;
width: 100%;
max-width: 100%;
height: auto;
min-height: 0;
border: 1px solid var(--bg-card-border);
@@ -830,9 +842,14 @@
}
.calendar-semester-table {
width: max-content;
min-width: 100%;
--calendar-day-head-width: 32px;
--calendar-day-cell-size: 22px;
--calendar-min-table-width: 538px;
width: 100%;
min-width: var(--calendar-min-table-width);
max-width: none;
min-height: 100%;
flex: 1 1 auto;
border-collapse: collapse;
table-layout: fixed;
font-size: 0.62rem;
@@ -841,32 +858,43 @@
.calendar-semester-table th,
.calendar-semester-table td {
box-sizing: border-box;
border: 1px solid var(--bg-card-border);
}
.calendar-semester-table tbody tr {
height: 19px;
height: var(--calendar-day-cell-size);
}
.calendar-day-head-col {
width: var(--calendar-day-head-width);
}
.calendar-week-col {
width: calc((100% - var(--calendar-day-head-width)) / var(--calendar-week-columns, 1));
}
.calendar-semester-table thead th {
position: sticky;
top: 0;
z-index: 1;
height: 18px;
min-width: 18px;
padding: 0 0.12rem;
height: var(--calendar-day-cell-size);
padding: 0;
background: var(--bg-input);
color: var(--text-secondary);
font-weight: 700;
text-align: center;
line-height: 1;
white-space: nowrap;
}
.calendar-semester-table .calendar-day-head {
position: sticky;
left: 0;
z-index: 2;
width: 30px;
min-width: 30px;
width: var(--calendar-day-head-width);
min-width: var(--calendar-day-head-width);
max-width: var(--calendar-day-head-width);
background: var(--bg-input);
color: var(--text-secondary);
font-weight: 700;
@@ -875,10 +903,8 @@
.calendar-day {
position: relative;
width: 18px;
min-width: 18px;
height: 19px;
min-height: 19px;
height: var(--calendar-day-cell-size);
min-height: var(--calendar-day-cell-size);
padding: 0;
background: color-mix(in srgb, var(--activity-color, var(--accent)) 12%, var(--bg-card));
border-color: color-mix(in srgb, var(--activity-color, var(--accent)) 28%, var(--bg-card-border));
@@ -986,30 +1012,36 @@
}
.calendar-day-tooltip-code {
width: 34px;
min-width: 34px;
height: 34px;
box-sizing: border-box;
width: 34px;
min-width: 34px;
height: 34px;
box-sizing: border-box;
border-radius: 8px;
background: color-mix(in srgb, var(--activity-color, var(--accent)) 24%, transparent);
border: 1px solid color-mix(in srgb, var(--activity-color, var(--accent)) 56%, transparent);
color: var(--text-primary);
font-size: 0.86rem;
font-weight: 800;
line-height: 1;
padding: 0;
text-align: center;
}
display: inline-grid;
place-items: center;
border-radius: 8px;
background: color-mix(in srgb, var(--activity-color, var(--accent)) 24%, transparent);
border: 1px solid color-mix(in srgb, var(--activity-color, var(--accent)) 56%, transparent);
color: var(--text-primary);
font-size: 0.86rem;
font-weight: 800;
line-height: 1;
padding: 0;
text-align: center;
.calendar-day-tooltip-head .calendar-day-tooltip-code {
display: inline-grid;
place-items: center;
}
.calendar-day-tooltip-code span {
display: block;
transform: translateY(2px);
display: inline-flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: inherit;
font: inherit;
line-height: 1;
transform: none;
}
.calendar-day-tooltip-body {
@@ -1019,12 +1051,14 @@
}
.calendar-empty-day {
width: 18px;
min-width: 18px;
height: 19px;
height: var(--calendar-day-cell-size);
background: color-mix(in srgb, var(--bg-input) 65%, transparent);
}
.calendar-week-placeholder {
color: transparent;
}
.calendar-empty-semester {
flex: 1;
display: flex;
@@ -1198,6 +1232,8 @@
.calendar-semesters-grid {
grid-template-columns: 1fr;
grid-auto-rows: auto;
overflow-x: auto;
scrollbar-width: thin;
}
.calendar-semester-block {

View File

@@ -580,39 +580,56 @@ export async function initAcademicCalendar() {
function renderCourseSemesters(course, courseRows) {
const calendar = selectedEditorCalendar();
const semestersForCourse = semesterBlocksForCourse(calendar, course, courseRows);
const maxWeekColumns = Math.max(
...semestersForCourse.map(block => uniqueWeekNumbers(block.rows).length),
1
);
return semestersForCourse.map(block => `
<section class="calendar-semester-block">
<div class="calendar-semester-header">
<h4>${block.semesterNumber} семестр</h4>
<span>${escapeHtml(block.caption)}</span>
</div>
${renderSemesterTable(block.rows)}
${renderSemesterTable(block.rows, maxWeekColumns)}
</section>
`).join('');
}
function renderSemesterTable(rows) {
function renderSemesterTable(rows, targetWeekColumns = 0) {
if (!rows.length) {
return '<div class="calendar-empty-semester">Нет дат в этом семестре</div>';
}
const rowsByWeek = groupBy(rows, row => row.weekNumber);
const weeks = Array.from(rowsByWeek.keys()).sort((a, b) => Number(a) - Number(b));
const weekSlots = [...weeks];
while (weekSlots.length < targetWeekColumns) {
weekSlots.push(null);
}
const cellByWeekAndDay = new Map(rows.map(row => [`${row.weekNumber}:${row.dayOfWeek}`, row]));
return `
<div class="calendar-semester-table-wrap">
<table class="calendar-semester-table">
<table class="calendar-semester-table" style="--calendar-week-columns:${weekSlots.length}; --calendar-min-table-width:${32 + weekSlots.length * 22}px">
<colgroup>
<col class="calendar-day-head-col">
${weekSlots.map(() => '<col class="calendar-week-col">').join('')}
</colgroup>
<thead>
<tr>
<th class="calendar-day-head">День</th>
${weeks.map(week => `<th title="Неделя ${week} учебного года">${week}</th>`).join('')}
${weekSlots.map(week => week === null
? '<th class="calendar-week-placeholder" aria-hidden="true"></th>'
: `<th title="Неделя ${week} учебного года">${week}</th>`).join('')}
</tr>
</thead>
<tbody>
${DAY_OPTIONS.map(day => `
<tr>
<th class="calendar-day-head">${escapeHtml(day.shortLabel)}</th>
${weeks.map(week => {
${weekSlots.map(week => {
if (week === null) {
return '<td class="calendar-empty-day calendar-week-placeholder" aria-hidden="true"></td>';
}
const row = cellByWeekAndDay.get(`${week}:${day.value}`);
return row ? renderCalendarDay(row) : '<td class="calendar-empty-day"></td>';
}).join('')}
@@ -624,6 +641,10 @@ export async function initAcademicCalendar() {
`;
}
function uniqueWeekNumbers(rows) {
return Array.from(new Set(rows.map(row => row.weekNumber)));
}
function renderCalendarDay(row) {
const selected = row.activityTypeId || activityIdByCode(row.activityCode) || theoryActivityId();
const dayName = shortDayLabel(row.dayOfWeek);

View File

@@ -21,11 +21,15 @@ export async function initDepartmentWorkspace() {
const commentsSubjectInput = document.getElementById('department-comment-subject-id');
const commentsTextInput = document.getElementById('department-comment-text');
const commentsCloseButton = document.getElementById('department-comments-close');
let lastTeachers = null;
let lastWorkload = null;
let teachersErrorMessage = '';
let workloadErrorMessage = '';
setDefaultDates(startInput, endInput);
refreshButton?.addEventListener('click', loadWorkspace);
departmentSelect?.addEventListener('change', loadWorkspace);
startInput?.addEventListener('change', loadWorkload);
startInput?.addEventListener('change', handleStartDateChange);
endInput?.addEventListener('change', loadWorkload);
importForm?.addEventListener('submit', importSubjects);
commentsForm?.addEventListener('submit', saveComment);
@@ -53,6 +57,11 @@ export async function initDepartmentWorkspace() {
async function loadWorkspace() {
hideAlert('department-workspace-alert');
lastTeachers = null;
lastWorkload = null;
teachersErrorMessage = '';
workloadErrorMessage = '';
renderTeachers();
await Promise.all([loadSubjects(), loadTeachers(), loadWorkload()]);
}
@@ -172,25 +181,28 @@ export async function initDepartmentWorkspace() {
}
async function loadTeachers() {
const container = document.getElementById('department-teachers-list');
container.textContent = 'Загрузка...';
lastTeachers = null;
teachersErrorMessage = '';
renderTeachers();
try {
const teachers = await api.get(`/api/department/teachers?departmentId=${encodeURIComponent(selectedDepartmentId() || '')}`);
container.innerHTML = teachers.length
? teachers.map(teacher => `
<article class="metric-card">
<strong>${escapeHtml(teacher.fullName || teacher.username || '-')}</strong>
<span>${escapeHtml(teacher.jobTitle || 'Должность не указана')}</span>
</article>
`).join('')
: '<div class="loading-row">Преподаватели не найдены</div>';
lastTeachers = Array.isArray(teachers) ? teachers : [];
renderTeachers();
} catch (error) {
container.innerHTML = `<div class="loading-row">${escapeHtml(error.message || 'Ошибка загрузки преподавателей')}</div>`;
lastTeachers = [];
teachersErrorMessage = error.message || 'Ошибка загрузки преподавателей';
renderTeachers();
}
}
async function loadWorkload() {
const container = document.getElementById('department-workload-list');
if (!startInput.value || !endInput.value) {
lastWorkload = [];
workloadErrorMessage = 'Выберите начало и окончание периода';
renderTeachers();
return;
}
const params = new URLSearchParams({
startDate: startInput.value,
endDate: endInput.value
@@ -198,23 +210,84 @@ export async function initDepartmentWorkspace() {
if (selectedDepartmentId()) {
params.set('departmentId', selectedDepartmentId());
}
container.textContent = 'Загрузка...';
lastWorkload = null;
workloadErrorMessage = '';
renderTeachers();
try {
const workload = await api.get(`/api/workload/teachers?${params}`);
container.innerHTML = workload.length
? workload.map(item => `
<article class="metric-card">
<strong>${escapeHtml(item.name || '-')}</strong>
<span>${escapeHtml(String(item.academicHours ?? 0))} ак. ч.</span>
<small>${escapeHtml(String(item.lessonCount ?? 0))} занятий</small>
</article>
`).join('')
: '<div class="loading-row">Нагрузка не найдена</div>';
lastWorkload = Array.isArray(workload) ? workload : [];
renderTeachers();
} catch (error) {
container.innerHTML = `<div class="loading-row">${escapeHtml(error.message || 'Ошибка расчёта нагрузки')}</div>`;
lastWorkload = [];
workloadErrorMessage = error.message || 'Ошибка расчёта нагрузки';
renderTeachers();
}
}
async function handleStartDateChange() {
if (startInput.value && endInput.value && startInput.value > endInput.value) {
endInput.value = '';
}
await loadWorkload();
}
function renderTeachers() {
const container = document.getElementById('department-teachers-list');
if (!container) return;
if (lastTeachers === null || lastWorkload === null) {
container.textContent = 'Загрузка...';
return;
}
const teachers = Array.isArray(lastTeachers) ? lastTeachers : [];
const workload = Array.isArray(lastWorkload) ? lastWorkload : [];
const workloadByTeacher = new Map();
const usedWorkloadKeys = new Set();
workload
.filter(item => item && (item.id || item.name))
.forEach(item => {
workloadKeys(item).forEach(key => {
if (!workloadByTeacher.has(key)) {
workloadByTeacher.set(key, item);
}
});
});
const rows = teachers.map(teacher => {
const workloadItem = workloadKeys(teacher)
.map(key => workloadByTeacher.get(key))
.find(Boolean);
if (workloadItem) {
workloadKeys(workloadItem).forEach(key => usedWorkloadKeys.add(key));
}
return renderTeacherMetric({
name: teacher.fullName || teacher.username || '-',
meta: teacher.jobTitle || 'Должность не указана',
workload: workloadItem
}, workloadErrorMessage);
});
workload
.filter(item => item && (item.id || item.name))
.filter(item => workloadKeys(item).every(key => !usedWorkloadKeys.has(key)))
.forEach(item => {
rows.push(renderTeacherMetric({
name: item.name || '-',
meta: 'Есть занятия в расписании кафедры',
workload: item
}, workloadErrorMessage));
});
if (rows.length) {
container.innerHTML = rows.join('');
return;
}
container.innerHTML = `<div class="loading-row">${escapeHtml(teachersErrorMessage || 'Преподаватели не найдены')}</div>`;
}
function selectedDepartmentId() {
return role === ROLE_DEPARTMENT ? userDepartmentId : departmentSelect.value;
}
@@ -261,3 +334,36 @@ function subjectCountLabel(count) {
if ([2, 3, 4].includes(count % 10)) return `${count} дисциплины`;
return `${count} дисциплин`;
}
function renderTeacherMetric(item, workloadErrorMessage) {
return `
<article class="metric-card">
<strong>${escapeHtml(item.name || '-')}</strong>
<span>${escapeHtml(item.meta || 'Должность не указана')}</span>
<small>${escapeHtml(workloadLabel(item.workload, workloadErrorMessage))}</small>
</article>
`;
}
function workloadLabel(item, workloadErrorMessage) {
if (workloadErrorMessage) {
return workloadErrorMessage;
}
if (!item) {
return '0 ак. ч., 0 занятий за период';
}
const hours = String(item.academicHours ?? 0);
const lessons = String(item.lessonCount ?? 0);
return `${hours} ак. ч., ${lessons} занятий за период`;
}
function workloadKeys(item) {
return [
item.id == null ? null : `id:${item.id}`,
normalizeTeacherName(item.fullName || item.name || item.username)
].filter(Boolean);
}
function normalizeTeacherName(value) {
return value ? `name:${String(value).trim().toLowerCase()}` : null;
}

View File

@@ -44,7 +44,6 @@
<h2>Преподаватели и нагрузка</h2>
</div>
<div class="metric-grid" id="department-teachers-list">Загрузка...</div>
<div class="metric-grid department-workload-list" id="department-workload-list">Загрузка...</div>
</div>
</div>

View File

@@ -84,7 +84,7 @@
<div id="schedule-rule-slots" class="schedule-slots-list"></div>
</div>
<div class="form-row">
<div class="form-row schedule-rule-actions">
<button type="submit" class="btn-primary">Сохранить правило</button>
</div>
<div class="form-alert" id="schedule-rule-alert" role="alert"></div>