Изменил страницу "Кафедра", добавлены изменения из задачи #54 в Vikunja
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user