Files
magistr/frontend/admin/js/views/department.js

456 lines
20 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
const container = document.getElementById('schedule-blocks-container');
let departments = [];
// Загрузка кафедр
try {
departments = await api.get('/api/departments');
departmentSelect.innerHTML = '<option value="">Выберите кафедру...</option>' +
departments.map(d => `<option value="${d.id}">${escapeHtml(d.departmentName || d.name)}</option>`).join('');
} catch (e) {
departmentSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
}
// ===== Восстанавливаем ранее загруженные таблицы из sessionStorage =====
restoreScheduleBlocks();
form.addEventListener('submit', async (e) => {
e.preventDefault();
hideAlert('schedule-form-alert');
const departmentId = departmentSelect.value;
const period = document.getElementById('filter-period').value;
const semesterType = document.querySelector('input[name="semesterType"]:checked')?.value;
if (!departmentId || !period || !semesterType) {
showAlert('schedule-form-alert', 'Заполните все поля', 'error');
return;
}
const deptName = departmentSelect.options[departmentSelect.selectedIndex].text;
try {
const params = new URLSearchParams({ departmentId, semesterType, period });
const data = await api.get(`/api/department/schedule?${params.toString()}`);
const semesterName = semesterType === 'spring' ? 'весенний' : (semesterType === 'autumn' ? 'осенний' : semesterType);
const periodName = period.replace('-', '/');
renderScheduleBlock(deptName, semesterName, periodName, data, departmentId, semesterType, period);
// НЕ сбрасываем форму — фильтры остаются заполненными (fix #3)
} catch (err) {
showAlert('schedule-form-alert', err.message || 'Ошибка загрузки данных', 'error');
}
});
// ===== Уникальный ключ для таблицы по параметрам =====
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">
<svg viewBox="0 0 20 20" class="chev-icon" focusable="false" aria-hidden="true">
<path d="M5.5 7.5L10 12l4.5-4.5" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<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(semester)}</b></span>
<span class="title-sub">Уч. год: <b>${escapeHtml(period)}</b></span>
</div>
<div class="meta">${Array.isArray(schedule) ? schedule.length : 0} записей</div>
</summary>
<div class="content">
<table>
<thead>
<tr>
<th>Специальность</th>
<th>Курс/семестр</th>
<th>Группа</th>
<th>Дисциплина</th>
<th>Вид занятий</th>
<th>Часов в неделю</th>
<th>Деление на подгруппы</th>
<th>Преподаватель</th>
</tr>
</thead>
<tbody>
${renderRows(schedule)}
</tbody>
</table>
</div>
`;
container.prepend(details);
// Сохраняем в sessionStorage
saveScheduleBlock(key, { deptName, semester, period, schedule, departmentId, semesterType, rawPeriod });
}
function renderRows(schedule) {
if (!Array.isArray(schedule) || schedule.length === 0) {
return '<tr><td colspan="8" class="loading-row">Нет данных</td></tr>';
}
return schedule.map(r => `
<tr>
<td>${escapeHtml(r.specialityCode || '-')}</td>
<td>${(() => {
const course = r.groupCourse || '-';
const semester = r.semester || '-';
if (course === '-' && semester === '-') return '-';
return `${course} | ${semester}`;
})()}</td>
<td>${escapeHtml(r.groupName || '-')}</td>
<td>${escapeHtml(r.subjectName || '-')}</td>
<td>${escapeHtml(r.lessonType || '-')}</td>
<td>${escapeHtml(r.numberOfHours || '-')}</td>
<td>${r.division === true ? '✓' : ''}</td>
<td>${(() => {
const jobTitle = r.teacherJobTitle || '-';
const teacherName = r.teacherName || '-';
if (jobTitle === '-' && teacherName === '-') return '-';
return `${jobTitle}, ${teacherName}`;
})()}</td>
</tr>
`).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);
}
}
// =========================================================
// ЛОГИКА ДЛЯ ФУНКЦИОНАЛА "СОЗДАТЬ ЗАПИСЬ (К/Ф)"
// Два модальных окна поверх всего контента в одном оверлее
// =========================================================
const btnCreateSchedule = document.getElementById('btn-create-schedule');
const csOverlay = document.getElementById('cs-overlay');
const modalCreateSchedule = document.getElementById('modal-create-schedule');
const modalCreateScheduleClose = document.getElementById('modal-create-schedule-close');
const formCreateSchedule = document.getElementById('create-schedule-form');
const modalViewSchedules = document.getElementById('modal-view-schedules');
const btnSaveSchedules = document.getElementById('btn-save-schedules');
const preparedSchedulesTbody = document.getElementById('prepared-schedules-tbody');
const csGroupSelect = document.getElementById('cs-group');
const csSubjectSelect = document.getElementById('cs-subject');
const csTeacherSelect = document.getElementById('cs-teacher');
const csDepartmentIdInput = document.getElementById('cs-department-id');
let preparedSchedules = [];
let csGroups = [];
let csSubjects = [];
let csTeachers = [];
const SEMESTER_LABELS = { autumn: 'Осенний', spring: 'Весенний' };
const LESSON_TYPE_LABELS = { 1: 'Лекция', 2: 'Практическая работа', 3: 'Лабораторная работа' };
const localDepartmentId = localStorage.getItem('departmentId');
// ===== Загрузка справочников =====
async function loadDictionariesForSchedule() {
try {
csGroups = await api.get('/api/groups');
csGroupSelect.innerHTML = '<option value="">Выберите группу</option>' +
csGroups.map(g => `<option value="${g.id}">${escapeHtml(g.name)}</option>`).join('');
csSubjects = await api.get('/api/subjects');
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="">Нет преподавателей</option>';
}
} catch (e) {
console.error('Ошибка загрузки справочников:', e);
}
}
loadDictionariesForSchedule();
// ===== Открытие / Закрытие оверлея =====
function openOverlay() {
csOverlay.classList.add('open');
document.body.style.overflow = 'hidden';
}
function closeOverlay() {
csOverlay.classList.remove('open');
document.body.style.overflow = '';
hideAlert('create-schedule-alert');
hideAlert('save-schedules-alert');
}
function updateTableVisibility() {
modalViewSchedules.style.display = preparedSchedules.length > 0 ? '' : 'none';
}
// ===== Кнопка «Создать запись» =====
btnCreateSchedule.addEventListener('click', () => {
if (localDepartmentId) {
csDepartmentIdInput.value = localDepartmentId;
} else {
showAlert('schedule-form-alert', 'Требуется перезайти (отсутствует ID кафедры)', 'error');
return;
}
openOverlay();
});
// ===== Закрытие =====
modalCreateScheduleClose.addEventListener('click', closeOverlay);
csOverlay.addEventListener('click', (e) => {
if (e.target === csOverlay || e.target.classList.contains('cs-overlay-scroll')) {
closeOverlay();
}
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && csOverlay.classList.contains('open')) {
closeOverlay();
}
});
// ===== Рендер таблицы подготовленных записей =====
function renderPreparedSchedules() {
if (preparedSchedules.length === 0) {
preparedSchedulesTbody.innerHTML = '<tr><td colspan="9" class="loading-row">Нет записей</td></tr>';
return;
}
preparedSchedulesTbody.innerHTML = preparedSchedules.map((s, index) => {
const groupName = csGroups.find(g => g.id == s.groupId)?.name || s.groupId;
const subjectName = csSubjects.find(sub => sub.id == s.subjectsId)?.name || s.subjectsId;
const teacherName = csTeachers.find(t => t.id == s.teacherId)?.fullName
|| csTeachers.find(t => t.id == s.teacherId)?.username || s.teacherId;
const lessonTypeName = LESSON_TYPE_LABELS[s.lessonTypeId] || 'Неизвестно';
const semLabel = SEMESTER_LABELS[s.semesterType] || s.semesterType;
const periodDisplay = s.period.replace('-', '/');
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>${escapeHtml(String(groupName))}</td>
<td>${escapeHtml(String(subjectName))}</td>
<td>${escapeHtml(lessonTypeName)}</td>
<td>${s.numberOfHours}</td>
<td>${divText}</td>
<td>${escapeHtml(String(teacherName))}</td>
<td><button type="button" class="btn-delete" data-index="${index}">Удалить</button></td>
</tr>`;
if (hasError) {
row += `<tr style="background: rgba(239, 68, 68, 0.05);">
<td colspan="9" style="color: var(--error); font-size: 0.85rem; padding: 0.4rem 0.85rem;">
${escapeHtml(s._errorMsg)}
</td>
</tr>`;
}
return row;
}).join('');
}
// ===== Удаление строки из таблицы =====
preparedSchedulesTbody.addEventListener('click', (e) => {
if (e.target.classList.contains('btn-delete')) {
const idx = parseInt(e.target.getAttribute('data-index'), 10);
preparedSchedules.splice(idx, 1);
renderPreparedSchedules();
updateTableVisibility();
}
});
// ===== Очистка полей формы (частичная) =====
function clearFormFields() {
document.getElementById('cs-hours').value = '';
document.getElementById('cs-division').checked = false;
}
// ===== Добавление записи в список =====
formCreateSchedule.addEventListener('submit', (e) => {
e.preventDefault();
hideAlert('create-schedule-alert');
const depId = csDepartmentIdInput.value;
const period = document.getElementById('cs-period').value;
const semesterType = document.querySelector('input[name="csSemesterType"]:checked')?.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 isDivision = document.getElementById('cs-division').checked;
const teacherId = csTeacherSelect.value;
if (!period || !semesterType || !groupId || !subjectId || !lessonTypeId || !hours || !teacherId) {
showAlert('create-schedule-alert', 'Заполните все обязательные поля', 'error');
return;
}
const newRecord = {
departmentId: Number(depId),
groupId: Number(groupId),
subjectsId: Number(subjectId),
lessonTypeId: Number(lessonTypeId),
numberOfHours: Number(hours),
isDivision: isDivision,
teacherId: Number(teacherId),
semesterType: semesterType,
period: period
};
// Проверка на дубликат
const isDuplicate = preparedSchedules.some(s =>
s.period === newRecord.period &&
s.semesterType === newRecord.semesterType &&
s.groupId === newRecord.groupId &&
s.subjectsId === newRecord.subjectsId &&
s.lessonTypeId === newRecord.lessonTypeId &&
s.numberOfHours === newRecord.numberOfHours &&
s.isDivision === newRecord.isDivision &&
s.teacherId === newRecord.teacherId
);
if (isDuplicate) {
showAlert('create-schedule-alert', 'Такая запись уже есть в списке', 'error');
return;
}
preparedSchedules.push(newRecord);
clearFormFields();
showAlert('create-schedule-alert', 'Запись добавлена ✓', 'success');
setTimeout(() => hideAlert('create-schedule-alert'), 4000); // fix #1: 4 секунды
renderPreparedSchedules();
updateTableVisibility();
});
// ===== Сохранение в БД =====
btnSaveSchedules.addEventListener('click', async () => {
if (preparedSchedules.length === 0) {
showAlert('save-schedules-alert', 'Нет записей для сохранения', 'error');
return;
}
btnSaveSchedules.disabled = true;
btnSaveSchedules.textContent = 'Сохранение...';
hideAlert('save-schedules-alert');
let errors = 0;
let saved = 0;
const failedRecords = [];
for (const record of preparedSchedules) {
try {
await api.post('/api/department/schedule/create', record);
saved++;
} catch (err) {
console.error('Ошибка сохранения записи:', err);
errors++;
const isDuplicate = err.status === 409 ||
(err.message && err.message.toLowerCase().includes('уже существует'));
failedRecords.push({
...record,
_errorMsg: isDuplicate
? 'Такая запись уже есть в базе данных'
: (err.message || 'Ошибка сохранения')
});
}
}
btnSaveSchedules.disabled = false;
btnSaveSchedules.textContent = 'Сохранить в БД';
if (errors === 0) {
showAlert('save-schedules-alert', `Все записи (${saved}) успешно сохранены!`, 'success');
preparedSchedules = [];
renderPreparedSchedules();
updateTableVisibility();
setTimeout(closeOverlay, 2000);
} else {
preparedSchedules = failedRecords;
renderPreparedSchedules();
if (saved > 0) {
showAlert('save-schedules-alert',
`Сохранено: ${saved}. Ошибок: ${errors}. Проблемные записи отмечены в таблице.`, 'error');
} else {
showAlert('save-schedules-alert',
`Не удалось сохранить. Ошибок: ${errors}. Проблемные записи отмечены в таблице.`, 'error');
}
}
});
}