Изменил страницу "Кафедра", добавлены изменения из задачи #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) {
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) {

View File

@@ -17,7 +17,7 @@ export async function initGroups() {
populateEfSelects(educationForms);
await loadGroups();
} catch (e) {
groupsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Ошибка загрузки данных</td></tr>';
groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Ошибка загрузки данных</td></tr>';
}
}
@@ -26,7 +26,7 @@ export async function initGroups() {
allGroups = await api.get('/api/groups');
applyGroupFilter();
} catch (e) {
groupsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Ошибка загрузки</td></tr>';
groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Ошибка загрузки</td></tr>';
}
}
@@ -61,7 +61,7 @@ export async function initGroups() {
function renderGroups(groups) {
if (!groups || !groups.length) {
groupsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Нет групп</td></tr>';
groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Нет групп</td></tr>';
return;
}
groupsTbody.innerHTML = groups.map(g => `
@@ -72,6 +72,7 @@ export async function initGroups() {
<td><span class="badge badge-ef">${escapeHtml(g.educationFormName)}</span></td>
<td>${g.departmentId || '-'}</td>
<td>${g.course || '-'}</td>
<td>${escapeHtml(g.specialityCode || '-')}</td>
<td><button class="btn-delete" data-id="${g.id}">Удалить</button></td>
</tr>`).join('');
}
@@ -83,13 +84,15 @@ export async function initGroups() {
const groupSize = document.getElementById('new-group-size').value;
const educationFormId = newGroupEfSelect.value;
const departmentId = document.getElementById('new-group-department').value;
const yearStartStudy = document.getElementById('new-group-yearStartStudy').value;
const course = document.getElementById('new-group-course').value;
const specialityCode = document.getElementById('new-group-speciality-code').value.trim();
if (!name) { showAlert('create-group-alert', 'Введите название группы', 'error'); return; }
if (!groupSize) { showAlert('create-group-alert', 'Введите размер группы', 'error'); return; }
if (!educationFormId) { showAlert('create-group-alert', 'Выберите форму обучения', 'error'); return; }
if (!departmentId) { showAlert('create-group-alert', 'Введите ID кафедры', 'error'); return; }
if (!yearStartStudy) { showAlert('create-group-alert', 'Введите курс', 'error'); return; }
if (!course) { showAlert('create-group-alert', 'Введите курс', 'error'); return; }
if (!specialityCode) { showAlert('create-group-alert', 'Введите код специальности', 'error'); return; }
try {
const data = await api.post('/api/groups', {
@@ -97,7 +100,8 @@ export async function initGroups() {
groupSize: Number(groupSize),
educationFormId: Number(educationFormId),
departmentId: Number(departmentId),
yearStartStudy: Number(yearStartStudy)
course: Number(course),
specialityCode: specialityCode
});
showAlert('create-group-alert', `Группа "${escapeHtml(data.name || name)}" создана`, 'success');
createGroupForm.reset();

View File

@@ -30,11 +30,11 @@
<label for="filter-period">Учебный год</label>
<select id="filter-period" required>
<option value="">Выберите...</option>
<option value="2022-2023">2022/2023</option>
<option value="2023-2024">2023/2024</option>
<option value="2024-2025">2024/2025</option>
<option value="2025-2026">2025/2026</option>
<option value="2026-2027">2026/2027</option>
<option value="2025-2026">2025/2026</option>
<option value="2024-2025">2024/2025</option>
<option value="2023-2024">2023/2024</option>
<option value="2022-2023">2022/2023</option>
</select>
</div>
@@ -63,9 +63,9 @@
<label for="cs-period">Учебный год</label>
<select id="cs-period" required>
<option value="">Выберите...</option>
<option value="2024-2025">2024/2025</option>
<option value="2025-2026">2025/2026</option>
<option value="2026-2027">2026/2027</option>
<option value="2025-2026">2025/2026</option>
<option value="2024-2025">2024/2025</option>
</select>
</div>
@@ -83,11 +83,6 @@
</div>
</div>
<div class="form-group" style="flex: 1 1 150px;">
<label for="cs-semester">Курс/Семестр (номер)</label>
<input type="number" id="cs-semester" required min="1" max="12" placeholder="Например: 1">
</div>
<div class="form-group" style="flex: 1 1 180px;">
<label for="cs-group">Группа</label>
<select id="cs-group" required>
@@ -159,7 +154,6 @@
<tr>
<th>Уч. год</th>
<th>Семестр</th>
<th></th>
<th>Группа</th>
<th>Дисциплина</th>
<th>Вид</th>
@@ -171,7 +165,7 @@
</thead>
<tbody id="prepared-schedules-tbody">
<tr>
<td colspan="10" class="loading-row">Нет записей</td>
<td colspan="9" class="loading-row">Нет записей</td>
</tr>
</tbody>
</table>

View File

@@ -25,6 +25,10 @@
<label for="new-group-yearStartStudy">Год начала обучения</label>
<input type="number" id="new-group-yearStartStudy" required pattern="^20\d{2}$" maxlength="3" placeholder="2026">
</div>
<div class="form-group">
<label for="new-group-speciality-code">Код специальности</label>
<input type="text" id="new-group-speciality-code" placeholder="09.03.01" required>
</div>
<button type="submit" class="btn-primary">Создать</button>
</div>
<div class="form-alert" id="create-group-alert" role="alert"></div>
@@ -51,12 +55,13 @@
<th>Форма обучения</th>
<th>ID кафедры</th>
<th>Курс</th>
<th>Код специальности</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="groups-tbody">
<tr>
<td colspan="7" class="loading-row">Загрузка...</td>
<td colspan="8" class="loading-row">Загрузка...</td>
</tr>
</tbody>
</table>