diff --git a/frontend/admin/js/views/department.js b/frontend/admin/js/views/department.js index 244b29d..f1011c7 100644 --- a/frontend/admin/js/views/department.js +++ b/frontend/admin/js/views/department.js @@ -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 = ''; } + // ===== Восстанавливаем ранее загруженные таблицы из 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 = `
@@ -86,11 +107,15 @@ export async function initDepartment() {
`; + 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 'Нет данных'; } return schedule.map(r => ` @@ -98,9 +123,9 @@ export async function initDepartment() { ${escapeHtml(r.specialityCode || '-')} ${(() => { 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}`; })()} ${escapeHtml(r.groupName || '-')} ${escapeHtml(r.subjectName || '-')} @@ -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 = '' + csSubjects.map(s => ``).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 = '' + csTeachers.map(t => ``).join(''); } else { - csTeacherSelect.innerHTML = ''; + csTeacherSelect.innerHTML = ''; } } 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 = 'Нет записей'; + preparedSchedulesTbody.innerHTML = 'Нет записей'; 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 = ` ${escapeHtml(periodDisplay)} ${escapeHtml(semLabel)} - ${s.groupSemester} ${escapeHtml(String(groupName))} ${escapeHtml(String(subjectName))} ${escapeHtml(lessonTypeName)} @@ -248,7 +313,7 @@ export async function initDepartment() { `; if (hasError) { row += ` - + ⚠ ${escapeHtml(s._errorMsg)} `; @@ -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) { diff --git a/frontend/admin/js/views/groups.js b/frontend/admin/js/views/groups.js index e702b88..ec57a24 100755 --- a/frontend/admin/js/views/groups.js +++ b/frontend/admin/js/views/groups.js @@ -17,7 +17,7 @@ export async function initGroups() { populateEfSelects(educationForms); await loadGroups(); } catch (e) { - groupsTbody.innerHTML = 'Ошибка загрузки данных'; + groupsTbody.innerHTML = 'Ошибка загрузки данных'; } } @@ -26,7 +26,7 @@ export async function initGroups() { allGroups = await api.get('/api/groups'); applyGroupFilter(); } catch (e) { - groupsTbody.innerHTML = 'Ошибка загрузки'; + groupsTbody.innerHTML = 'Ошибка загрузки'; } } @@ -61,7 +61,7 @@ export async function initGroups() { function renderGroups(groups) { if (!groups || !groups.length) { - groupsTbody.innerHTML = 'Нет групп'; + groupsTbody.innerHTML = 'Нет групп'; return; } groupsTbody.innerHTML = groups.map(g => ` @@ -72,6 +72,7 @@ export async function initGroups() { ${escapeHtml(g.educationFormName)} ${g.departmentId || '-'} ${g.course || '-'} + ${escapeHtml(g.specialityCode || '-')} `).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(); diff --git a/frontend/admin/views/department.html b/frontend/admin/views/department.html index d0b435e..11bf25b 100644 --- a/frontend/admin/views/department.html +++ b/frontend/admin/views/department.html @@ -30,11 +30,11 @@ @@ -63,9 +63,9 @@ @@ -83,11 +83,6 @@ -
- - -
-
+
+ + +
@@ -51,12 +55,13 @@ Форма обучения ID кафедры Курс + Код специальности Действия - Загрузка... + Загрузка...