Изменил страницу "Кафедра", добавлены изменения из задачи #54 в Vikunja

This commit is contained in:
2026-04-01 23:18:05 +03:00
parent 3cdb8614cb
commit c82e3feaed
4 changed files with 111 additions and 50 deletions

View File

@@ -1,6 +1,9 @@
import { api } from '../api.js';
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
// Ключ для хранения данных в sessionStorage
const STORAGE_KEY = 'department_schedule_blocks';
export async function initDepartment() {
const form = document.getElementById('department-schedule-form');
const departmentSelect = document.getElementById('filter-department');
@@ -17,6 +20,9 @@ export async function initDepartment() {
departmentSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
}
// ===== Восстанавливаем ранее загруженные таблицы из sessionStorage =====
restoreScheduleBlocks();
form.addEventListener('submit', async (e) => {
e.preventDefault();
hideAlert('schedule-form-alert');
@@ -39,17 +45,32 @@ export async function initDepartment() {
const semesterName = semesterType === 'spring' ? 'весенний' : (semesterType === 'autumn' ? 'осенний' : semesterType);
const periodName = period.replace('-', '/');
renderScheduleBlock(deptName, semesterName, periodName, data);
form.reset();
renderScheduleBlock(deptName, semesterName, periodName, data, departmentId, semesterType, period);
// НЕ сбрасываем форму — фильтры остаются заполненными (fix #3)
} catch (err) {
showAlert('schedule-form-alert', err.message || 'Ошибка загрузки данных', 'error');
}
});
function renderScheduleBlock(deptName, groupSemester, period, schedule) {
// ===== Уникальный ключ для таблицы по параметрам =====
function blockKey(departmentId, semesterType, period) {
return `${departmentId}_${semesterType}_${period}`;
}
// ===== Рендер блока таблицы (с дедупликацией — fix #6) =====
function renderScheduleBlock(deptName, semester, period, schedule, departmentId, semesterType, rawPeriod) {
const key = blockKey(departmentId, semesterType, rawPeriod);
// Удаляем ранее загруженный блок с тем же ключом
const existing = container.querySelector(`[data-block-key="${key}"]`);
if (existing) existing.remove();
const details = document.createElement('details');
details.className = 'table-item';
details.open = true;
details.setAttribute('data-block-key', key);
details.innerHTML = `
<summary>
<div class="chev" aria-hidden="true">
@@ -61,10 +82,10 @@ export async function initDepartment() {
<div class="title title-multiline">
<span class="title-main">Данные к составлению расписания</span>
<span class="title-sub">Кафедра: <b>${escapeHtml(deptName)}</b></span>
<span class="title-sub">Семестр: <b>${escapeHtml(groupSemester)}</b></span>
<span class="title-sub">Семестр: <b>${escapeHtml(semester)}</b></span>
<span class="title-sub">Уч. год: <b>${escapeHtml(period)}</b></span>
</div>
<div class="meta">${schedule ? schedule.length : 0} записей</div>
<div class="meta">${Array.isArray(schedule) ? schedule.length : 0} записей</div>
</summary>
<div class="content">
<table>
@@ -86,11 +107,15 @@ export async function initDepartment() {
</table>
</div>
`;
container.prepend(details);
// Сохраняем в sessionStorage
saveScheduleBlock(key, { deptName, semester, period, schedule, departmentId, semesterType, rawPeriod });
}
function renderRows(schedule) {
if (!schedule || schedule.length === 0) {
if (!Array.isArray(schedule) || schedule.length === 0) {
return '<tr><td colspan="8" class="loading-row">Нет данных</td></tr>';
}
return schedule.map(r => `
@@ -98,9 +123,9 @@ export async function initDepartment() {
<td>${escapeHtml(r.specialityCode || '-')}</td>
<td>${(() => {
const course = r.groupCourse || '-';
const groupSemester = r.groupSemester || '-';
if (course === '-' && groupSemester === '-') return '-';
return `${course} | ${groupSemester}`;
const semester = r.semester || '-';
if (course === '-' && semester === '-') return '-';
return `${course} | ${semester}`;
})()}</td>
<td>${escapeHtml(r.groupName || '-')}</td>
<td>${escapeHtml(r.subjectName || '-')}</td>
@@ -117,6 +142,32 @@ export async function initDepartment() {
`).join('');
}
// ===== Persistence: sessionStorage (fix #4) =====
function saveScheduleBlock(key, blockData) {
try {
const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
stored[key] = blockData;
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
} catch (e) {
console.warn('Ошибка сохранения в sessionStorage:', e);
}
}
function restoreScheduleBlocks() {
try {
const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
const keys = Object.keys(stored);
if (keys.length === 0) return;
keys.forEach(key => {
const b = stored[key];
renderScheduleBlock(b.deptName, b.semester, b.period, b.schedule, b.departmentId, b.semesterType, b.rawPeriod);
});
} catch (e) {
console.warn('Ошибка восстановления из sessionStorage:', e);
}
}
// =========================================================
// ЛОГИКА ДЛЯ ФУНКЦИОНАЛА "СОЗДАТЬ ЗАПИСЬ (К/Ф)"
// Два модальных окна поверх всего контента в одном оверлее
@@ -158,12 +209,28 @@ export async function initDepartment() {
csSubjectSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
csSubjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
// Загрузка преподавателей: сначала по кафедре, при ошибке — все преподаватели
csTeachers = [];
if (localDepartmentId) {
csTeachers = await api.get(`/api/users/teachers/${localDepartmentId}`);
try {
csTeachers = await api.get(`/api/users/teachers/${localDepartmentId}`);
} catch (e) {
console.warn('Не удалось загрузить преподавателей для кафедры, загружаем всех:', e);
}
}
// Фолбэк: загружаем всех преподавателей
if (!Array.isArray(csTeachers) || csTeachers.length === 0) {
try {
csTeachers = await api.get('/api/users/teachers');
} catch (e2) {
console.error('Ошибка загрузки всех преподавателей:', e2);
}
}
if (Array.isArray(csTeachers) && csTeachers.length > 0) {
csTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' +
csTeachers.map(t => `<option value="${t.id}">${escapeHtml(t.fullName || t.username)}</option>`).join('');
} else {
csTeacherSelect.innerHTML = '<option value="">Ошибка: Не найден ID кафедры</option>';
csTeacherSelect.innerHTML = '<option value="">Нет преподавателей</option>';
}
} catch (e) {
console.error('Ошибка загрузки справочников:', e);
@@ -175,7 +242,7 @@ export async function initDepartment() {
// ===== Открытие / Закрытие оверлея =====
function openOverlay() {
csOverlay.classList.add('open');
document.body.style.overflow = 'hidden'; // Предотвращаем скролл страницы
document.body.style.overflow = 'hidden';
}
function closeOverlay() {
@@ -204,7 +271,6 @@ export async function initDepartment() {
modalCreateScheduleClose.addEventListener('click', closeOverlay);
csOverlay.addEventListener('click', (e) => {
// Закрыть по клику на затемнённый фон (но не по клику на содержимое модалок)
if (e.target === csOverlay || e.target.classList.contains('cs-overlay-scroll')) {
closeOverlay();
}
@@ -216,10 +282,10 @@ export async function initDepartment() {
}
});
// ===== Рендер таблицы =====
// ===== Рендер таблицы подготовленных записей =====
function renderPreparedSchedules() {
if (preparedSchedules.length === 0) {
preparedSchedulesTbody.innerHTML = '<tr><td colspan="10" class="loading-row">Нет записей</td></tr>';
preparedSchedulesTbody.innerHTML = '<tr><td colspan="9" class="loading-row">Нет записей</td></tr>';
return;
}
preparedSchedulesTbody.innerHTML = preparedSchedules.map((s, index) => {
@@ -230,14 +296,13 @@ export async function initDepartment() {
const lessonTypeName = LESSON_TYPE_LABELS[s.lessonTypeId] || 'Неизвестно';
const semLabel = SEMESTER_LABELS[s.semesterType] || s.semesterType;
const periodDisplay = s.period.replace('-', '/');
const divText = s.division ? '✓' : '';
const divText = s.isDivision ? '✓' : '';
const hasError = !!s._errorMsg;
const rowStyle = hasError ? ' style="background: rgba(239, 68, 68, 0.08);"' : '';
let row = `
<tr${rowStyle}>
<td>${escapeHtml(periodDisplay)}</td>
<td>${escapeHtml(semLabel)}</td>
<td>${s.groupSemester}</td>
<td>${escapeHtml(String(groupName))}</td>
<td>${escapeHtml(String(subjectName))}</td>
<td>${escapeHtml(lessonTypeName)}</td>
@@ -248,7 +313,7 @@ export async function initDepartment() {
</tr>`;
if (hasError) {
row += `<tr style="background: rgba(239, 68, 68, 0.05);">
<td colspan="10" style="color: var(--error); font-size: 0.85rem; padding: 0.4rem 0.85rem;">
<td colspan="9" style="color: var(--error); font-size: 0.85rem; padding: 0.4rem 0.85rem;">
${escapeHtml(s._errorMsg)}
</td>
</tr>`;
@@ -268,8 +333,6 @@ export async function initDepartment() {
});
// ===== Очистка полей формы (частичная) =====
// НЕ очищаем select'ы — они остаются заполненными для удобства.
// Пользователь сам изменит нужные поля для следующей записи.
function clearFormFields() {
document.getElementById('cs-hours').value = '';
document.getElementById('cs-division').checked = false;
@@ -283,12 +346,11 @@ export async function initDepartment() {
const depId = csDepartmentIdInput.value;
const period = document.getElementById('cs-period').value;
const semesterType = document.querySelector('input[name="csSemesterType"]:checked')?.value;
const groupSemester = document.getElementById('cs-semester').value;
const groupId = csGroupSelect.value;
const subjectId = csSubjectSelect.value;
const lessonTypeId = document.getElementById('cs-lesson-type').value;
const hours = document.getElementById('cs-hours').value;
const division = document.getElementById('cs-division').checked;
const isDivision = document.getElementById('cs-division').checked;
const teacherId = csTeacherSelect.value;
if (!period || !semesterType || !groupId || !subjectId || !lessonTypeId || !hours || !teacherId) {
@@ -298,27 +360,25 @@ export async function initDepartment() {
const newRecord = {
departmentId: Number(depId),
groupSemester: Number(groupSemester),
groupId: Number(groupId),
subjectsId: Number(subjectId),
lessonTypeId: Number(lessonTypeId),
numberOfHours: Number(hours),
division: division,
isDivision: isDivision,
teacherId: Number(teacherId),
semesterType: semesterType,
period: period
};
// Проверка на дубликат в уже добавленных записях
// Проверка на дубликат
const isDuplicate = preparedSchedules.some(s =>
s.period === newRecord.period &&
s.semesterType === newRecord.semesterType &&
s.groupSemester === newRecord.groupSemester &&
s.groupId === newRecord.groupId &&
s.subjectsId === newRecord.subjectsId &&
s.lessonTypeId === newRecord.lessonTypeId &&
s.numberOfHours === newRecord.numberOfHours &&
s.division === newRecord.division &&
s.isDivision === newRecord.isDivision &&
s.teacherId === newRecord.teacherId
);
@@ -332,7 +392,7 @@ export async function initDepartment() {
clearFormFields();
showAlert('create-schedule-alert', 'Запись добавлена ✓', 'success');
setTimeout(() => hideAlert('create-schedule-alert'), 2000);
setTimeout(() => hideAlert('create-schedule-alert'), 4000); // fix #1: 4 секунды
renderPreparedSchedules();
updateTableVisibility();
@@ -360,7 +420,6 @@ export async function initDepartment() {
} catch (err) {
console.error('Ошибка сохранения записи:', err);
errors++;
// Помечаем запись как дубликат, если бэк вернул соответствующую ошибку
const isDuplicate = err.status === 409 ||
(err.message && err.message.toLowerCase().includes('уже существует'));
failedRecords.push({
@@ -382,7 +441,6 @@ export async function initDepartment() {
updateTableVisibility();
setTimeout(closeOverlay, 2000);
} else {
// Оставляем неудачные записи для повторной попытки / удаления
preparedSchedules = failedRecords;
renderPreparedSchedules();
if (saved > 0) {