456 lines
20 KiB
JavaScript
456 lines
20 KiB
JavaScript
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');
|
||
}
|
||
}
|
||
});
|
||
|
||
} |