Много разных изменений
This commit is contained in:
@@ -1,7 +1,15 @@
|
||||
import { api, getSession } from '../api.js';
|
||||
import {
|
||||
academicYearsFromSemesters,
|
||||
resolveAcademicPeriod,
|
||||
semestersForAcademicYear
|
||||
} from '../academic-period.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
import { preferredScheduleSemester } from '../schedule-period.js';
|
||||
import { canEditScheduleOverrides, createScheduleOverridePanel } from './schedule-override-panel.js';
|
||||
import { AsyncCombobox } from '../async-combobox.js';
|
||||
import { readUrlState, writeUrlState } from '../url-state.js';
|
||||
import { trackUxEvent } from '../../../telemetry.js';
|
||||
|
||||
const ROLE_DEPARTMENT = 'DEPARTMENT';
|
||||
const TARGET_GROUPS = 'groups';
|
||||
@@ -62,7 +70,18 @@ export async function initScheduleView() {
|
||||
const tables = document.getElementById('schedule-view-tables');
|
||||
const entityFields = Array.from(document.querySelectorAll('[data-schedule-view-target-field]'));
|
||||
const departmentSelect = document.getElementById('schedule-view-department');
|
||||
const academicYearSelect = document.getElementById('schedule-view-year');
|
||||
const semesterSelect = document.getElementById('schedule-view-semester');
|
||||
const restored = readUrlState({
|
||||
scheduleTarget: TARGET_GROUPS,
|
||||
scheduleEntity: '',
|
||||
scheduleDate: '',
|
||||
scheduleYear: '',
|
||||
scheduleSemester: '',
|
||||
scheduleSubject: '',
|
||||
scheduleLessonType: '',
|
||||
scheduleParity: ''
|
||||
});
|
||||
|
||||
const state = {
|
||||
dictionaries: {
|
||||
@@ -83,6 +102,10 @@ export async function initScheduleView() {
|
||||
const mobileMedia = window.matchMedia(MOBILE_QUERY);
|
||||
|
||||
setDefaultDate(dateInput);
|
||||
if (restored.scheduleDate) dateInput.value = restored.scheduleDate;
|
||||
if (targetSelect && role !== ROLE_DEPARTMENT) targetSelect.value = restored.scheduleTarget;
|
||||
restoreEntitySelect(restored.scheduleTarget, restored.scheduleEntity);
|
||||
const entityComboboxes = setupEntityComboboxes(state.dictionaries, role, userDepartmentId);
|
||||
configureDepartmentTarget();
|
||||
updatePeriodLabel();
|
||||
updateEntityFieldVisibility();
|
||||
@@ -92,6 +115,7 @@ export async function initScheduleView() {
|
||||
clearInactiveEntityValues();
|
||||
updateEntityFieldVisibility();
|
||||
resetResults(SCHEDULE_REFRESH_PROMPT);
|
||||
persistFilters();
|
||||
});
|
||||
loadButton?.addEventListener('click', loadData);
|
||||
previousPeriodButton?.addEventListener('click', () => movePeriod(-1));
|
||||
@@ -103,11 +127,22 @@ export async function initScheduleView() {
|
||||
dateInput?.addEventListener('change', () => {
|
||||
updatePeriodLabel();
|
||||
resetResults(SCHEDULE_REFRESH_PROMPT);
|
||||
persistFilters();
|
||||
});
|
||||
academicYearSelect?.addEventListener('change', () => {
|
||||
const yearSemesters = semestersForAcademicYear(state.dictionaries.semesters, academicYearSelect.value);
|
||||
const selected = resolveAcademicPeriod(yearSemesters, { date: dateInput.value });
|
||||
renderPeriodSelects(academicYearSelect.value, selected.semesterId);
|
||||
synchronizeDateWithSemester();
|
||||
updatePeriodLabel();
|
||||
resetResults(SCHEDULE_REFRESH_PROMPT);
|
||||
persistFilters();
|
||||
});
|
||||
semesterSelect?.addEventListener('change', () => {
|
||||
synchronizeDateWithSemester();
|
||||
updatePeriodLabel();
|
||||
resetResults(SCHEDULE_REFRESH_PROMPT);
|
||||
persistFilters();
|
||||
});
|
||||
resultPreviousButton?.addEventListener('click', () => moveResult(-1));
|
||||
resultNextButton?.addEventListener('click', () => moveResult(1));
|
||||
@@ -132,8 +167,10 @@ export async function initScheduleView() {
|
||||
}
|
||||
|
||||
try {
|
||||
state.dictionaries = await loadDictionaries(role, userDepartmentId);
|
||||
selectCurrentSemester();
|
||||
Object.assign(state.dictionaries, await loadDictionaries(role, userDepartmentId));
|
||||
mergeLoadedDictionaries(state.dictionaries, entityComboboxes);
|
||||
restoreSimpleFilters(restored);
|
||||
selectCurrentSemester(restored.scheduleYear, restored.scheduleSemester);
|
||||
overridePanel = createScheduleOverridePanel({
|
||||
role,
|
||||
dictionaries: state.dictionaries,
|
||||
@@ -149,6 +186,11 @@ export async function initScheduleView() {
|
||||
resetResults(message);
|
||||
}
|
||||
|
||||
['schedule-view-group', 'schedule-view-teacher', 'schedule-view-classroom', 'schedule-view-department',
|
||||
'schedule-view-subject', 'schedule-view-lesson-type', 'schedule-view-parity'].forEach(id => {
|
||||
document.getElementById(id)?.addEventListener('change', persistFilters);
|
||||
});
|
||||
|
||||
function configureDepartmentTarget() {
|
||||
if (role !== ROLE_DEPARTMENT) return;
|
||||
targetSelect.value = TARGET_DEPARTMENTS;
|
||||
@@ -187,8 +229,12 @@ export async function initScheduleView() {
|
||||
].forEach(([fieldTarget, id]) => {
|
||||
if (fieldTarget === target) return;
|
||||
if (role === ROLE_DEPARTMENT && id === 'schedule-view-department') return;
|
||||
const select = document.getElementById(id);
|
||||
if (select) select.value = '';
|
||||
if (entityComboboxes[fieldTarget]) {
|
||||
entityComboboxes[fieldTarget].clear();
|
||||
} else {
|
||||
const select = document.getElementById(id);
|
||||
if (select) select.value = '';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -255,6 +301,8 @@ export async function initScheduleView() {
|
||||
state.overrides = overrides;
|
||||
overridePanel.setRegistry(overrides, range);
|
||||
renderLessons(lessons, range, target, previousKey);
|
||||
persistFilters();
|
||||
trackUxEvent('расписание_найдено', { target, lessons: lessons.length });
|
||||
} catch (error) {
|
||||
const message = error.message || 'Ошибка загрузки расписания';
|
||||
showAlert('schedule-view-alert', message, 'error');
|
||||
@@ -894,17 +942,38 @@ export async function initScheduleView() {
|
||||
.find(semester => String(semester.id) === String(semesterId)) || null;
|
||||
}
|
||||
|
||||
function selectCurrentSemester() {
|
||||
if (!semesterSelect) return;
|
||||
const preferredSemester = preferredScheduleSemester(
|
||||
state.dictionaries.semesters,
|
||||
dateInput.value
|
||||
);
|
||||
semesterSelect.value = preferredSemester ? String(preferredSemester.id) : '';
|
||||
function selectCurrentSemester(academicYearId = '', semesterId = '') {
|
||||
if (!academicYearSelect || !semesterSelect) return;
|
||||
const selected = resolveAcademicPeriod(state.dictionaries.semesters, {
|
||||
academicYearId,
|
||||
semesterId,
|
||||
date: dateInput.value
|
||||
});
|
||||
renderPeriodSelects(selected.academicYearId, selected.semesterId);
|
||||
synchronizeDateWithSemester();
|
||||
updatePeriodLabel();
|
||||
}
|
||||
|
||||
function renderPeriodSelects(academicYearId, semesterId) {
|
||||
const years = academicYearsFromSemesters(state.dictionaries.semesters);
|
||||
const selectedYearId = String(academicYearId || years[0]?.id || '');
|
||||
academicYearSelect.innerHTML = years.length
|
||||
? years.map(year => `<option value="${escapeHtml(year.id)}">${escapeHtml(year.title)}</option>`).join('')
|
||||
: '<option value="">Учебные годы не настроены</option>';
|
||||
academicYearSelect.value = selectedYearId;
|
||||
|
||||
const yearSemesters = semestersForAcademicYear(state.dictionaries.semesters, selectedYearId);
|
||||
semesterSelect.innerHTML = yearSemesters.length
|
||||
? yearSemesters.map(semester => `
|
||||
<option value="${escapeHtml(String(semester.id))}">${escapeHtml(semesterOptionLabel(semester))}</option>
|
||||
`).join('')
|
||||
: '<option value="">Семестры не настроены</option>';
|
||||
const selectedSemester = yearSemesters.find(semester => String(semester.id) === String(semesterId))
|
||||
|| preferredScheduleSemester(yearSemesters, dateInput.value)
|
||||
|| yearSemesters[0];
|
||||
semesterSelect.value = String(selectedSemester?.id || '');
|
||||
}
|
||||
|
||||
function synchronizeDateWithSemester() {
|
||||
const semester = selectedSemester();
|
||||
if (!semester) {
|
||||
@@ -924,6 +993,26 @@ export async function initScheduleView() {
|
||||
if (!range) return 1;
|
||||
return isoDay(dateInput.value || range.startDate);
|
||||
}
|
||||
|
||||
function persistFilters() {
|
||||
const target = activeTarget();
|
||||
const entitySelectId = {
|
||||
groups: 'schedule-view-group',
|
||||
teachers: 'schedule-view-teacher',
|
||||
classrooms: 'schedule-view-classroom',
|
||||
departments: 'schedule-view-department'
|
||||
}[target];
|
||||
writeUrlState({
|
||||
scheduleTarget: role === ROLE_DEPARTMENT ? '' : target,
|
||||
scheduleEntity: entitySelectId ? valueOf(entitySelectId) : '',
|
||||
scheduleDate: dateInput.value,
|
||||
scheduleYear: academicYearSelect?.value || '',
|
||||
scheduleSemester: semesterSelect?.value || '',
|
||||
scheduleSubject: valueOf('schedule-view-subject'),
|
||||
scheduleLessonType: valueOf('schedule-view-lesson-type'),
|
||||
scheduleParity: valueOf('schedule-view-parity')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createEmptyOverridePanel() {
|
||||
@@ -935,37 +1024,111 @@ function createEmptyOverridePanel() {
|
||||
}
|
||||
|
||||
async function loadDictionaries(role, userDepartmentId) {
|
||||
const [groups, teachers, classrooms, departments, subjects, lessonTypes, semesters] = await Promise.all([
|
||||
api.get('/api/groups'),
|
||||
api.get('/api/users/teachers'),
|
||||
api.get('/api/classrooms'),
|
||||
const [departments, subjects, lessonTypes, semesters] = await Promise.all([
|
||||
api.get('/api/departments'),
|
||||
api.get('/api/subjects'),
|
||||
api.get('/api/lesson-types'),
|
||||
api.get('/api/schedule/semesters')
|
||||
]);
|
||||
|
||||
const visibleGroups = role === ROLE_DEPARTMENT && userDepartmentId
|
||||
? groups.filter(group => String(group.departmentId) === String(userDepartmentId))
|
||||
: groups;
|
||||
|
||||
fillSelect('schedule-view-group', visibleGroups, item => item.id, item => item.name, 'Все группы');
|
||||
fillSelect('schedule-view-teacher', teachers, item => item.id, item => item.fullName || item.username, 'Все преподаватели');
|
||||
fillSelect('schedule-view-classroom', classrooms, item => item.id, item => roomLabel(item), 'Все аудитории');
|
||||
fillSelect('schedule-view-department', departments, item => item.id, item => item.departmentName, 'Все кафедры');
|
||||
fillSelect('schedule-view-subject', subjects, item => item.id, item => item.name, 'Все дисциплины');
|
||||
fillSelect('schedule-view-lesson-type', lessonTypes, item => item.id, item => item.name, 'Все типы');
|
||||
fillSelect('schedule-view-semester', semesters, item => item.id, semesterOptionLabel, 'По текущей дате');
|
||||
|
||||
return {
|
||||
groups,
|
||||
teachers,
|
||||
classrooms,
|
||||
groups: [],
|
||||
teachers: [],
|
||||
classrooms: [],
|
||||
departments,
|
||||
semesters: semesters || []
|
||||
};
|
||||
}
|
||||
|
||||
function setupEntityComboboxes(dictionaries, role, userDepartmentId) {
|
||||
const definitions = {
|
||||
groups: {
|
||||
select: document.getElementById('schedule-view-group'),
|
||||
load: async (query, selectedId) => {
|
||||
const params = new URLSearchParams({ query, limit: '30' });
|
||||
if (selectedId) params.set('selectedId', selectedId);
|
||||
if (role === ROLE_DEPARTMENT && userDepartmentId) params.set('departmentId', userDepartmentId);
|
||||
return api.get(`/api/groups/options?${params}`);
|
||||
},
|
||||
label: item => item.name,
|
||||
description: item => [item.specialtyCode, item.specialtyProfileName, item.educationFormName].filter(Boolean).join(' · ')
|
||||
},
|
||||
teachers: {
|
||||
select: document.getElementById('schedule-view-teacher'),
|
||||
load: async (query, selectedId) => {
|
||||
const params = new URLSearchParams({ query, limit: '30' });
|
||||
if (selectedId) params.set('selectedId', selectedId);
|
||||
return api.get(`/api/users/teachers/options?${params}`);
|
||||
},
|
||||
label: item => item.fullName || item.username,
|
||||
description: item => [item.jobTitle, item.departmentName].filter(Boolean).join(' · ')
|
||||
},
|
||||
classrooms: {
|
||||
select: document.getElementById('schedule-view-classroom'),
|
||||
load: async (query, selectedId) => {
|
||||
const params = new URLSearchParams({ query, limit: '30' });
|
||||
if (selectedId) params.set('selectedId', selectedId);
|
||||
return api.get(`/api/classrooms/options?${params}`);
|
||||
},
|
||||
label: roomLabel,
|
||||
description: item => [item.capacity ? `${item.capacity} мест` : null, item.building].filter(Boolean).join(' · ')
|
||||
}
|
||||
};
|
||||
return Object.fromEntries(Object.entries(definitions).map(([key, definition]) => {
|
||||
const combo = new AsyncCombobox(definition.select, {
|
||||
placeholder: `Найдите ${key === 'groups' ? 'группу' : key === 'teachers' ? 'преподавателя' : 'аудиторию'}`,
|
||||
optionLabel: definition.label,
|
||||
optionDescription: definition.description,
|
||||
load: async (query, selectedId) => {
|
||||
const items = await definition.load(query, selectedId);
|
||||
mergeItems(dictionaries, key, items);
|
||||
return items;
|
||||
}
|
||||
});
|
||||
combo.preload('');
|
||||
return [key, combo];
|
||||
}));
|
||||
}
|
||||
|
||||
function mergeLoadedDictionaries(dictionaries, combos) {
|
||||
Object.keys(combos).forEach(key => mergeItems(dictionaries, key, combos[key].items || []));
|
||||
}
|
||||
|
||||
function mergeItems(dictionaries, key, items) {
|
||||
const merged = new Map((dictionaries[key] || []).map(item => [String(item.id), item]));
|
||||
(items || []).forEach(item => merged.set(String(item.id), item));
|
||||
dictionaries[key] = [...merged.values()];
|
||||
}
|
||||
|
||||
function restoreEntitySelect(target, value) {
|
||||
if (!value) return;
|
||||
const id = {
|
||||
groups: 'schedule-view-group',
|
||||
teachers: 'schedule-view-teacher',
|
||||
classrooms: 'schedule-view-classroom',
|
||||
departments: 'schedule-view-department'
|
||||
}[target];
|
||||
const select = document.getElementById(id);
|
||||
if (select) select.innerHTML = `<option value="${escapeHtml(value)}" selected>Загрузка...</option>`;
|
||||
}
|
||||
|
||||
function restoreSimpleFilters(restored) {
|
||||
[
|
||||
['schedule-view-subject', restored.scheduleSubject],
|
||||
['schedule-view-lesson-type', restored.scheduleLessonType],
|
||||
['schedule-view-parity', restored.scheduleParity]
|
||||
].forEach(([id, value]) => {
|
||||
const select = document.getElementById(id);
|
||||
if (select && value && Array.from(select.options).some(option => option.value === String(value))) {
|
||||
select.value = String(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function setDefaultDate(dateInput) {
|
||||
const today = new Date();
|
||||
dateInput.value = toIsoDate(today);
|
||||
|
||||
Reference in New Issue
Block a user