This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { api, getSession } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
import { canEditScheduleOverrides, createScheduleOverridePanel } from './schedule-override-panel.js';
|
||||
|
||||
const ROLE_DEPARTMENT = 'DEPARTMENT';
|
||||
const TARGET_GROUPS = 'groups';
|
||||
@@ -59,6 +60,7 @@ 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 semesterSelect = document.getElementById('schedule-view-semester');
|
||||
|
||||
const state = {
|
||||
dictionaries: {
|
||||
@@ -71,8 +73,11 @@ export async function initScheduleView() {
|
||||
activeSectionIndex: 0,
|
||||
mobileDay: 1,
|
||||
range: null,
|
||||
totalLessons: 0
|
||||
totalLessons: 0,
|
||||
lessons: [],
|
||||
overrides: []
|
||||
};
|
||||
let overridePanel = createEmptyOverridePanel();
|
||||
const mobileMedia = window.matchMedia(MOBILE_QUERY);
|
||||
|
||||
setDefaultDate(dateInput);
|
||||
@@ -97,8 +102,22 @@ export async function initScheduleView() {
|
||||
updatePeriodLabel();
|
||||
resetResults('Нажмите «Показать», чтобы обновить расписание');
|
||||
});
|
||||
semesterSelect?.addEventListener('change', () => {
|
||||
synchronizeDateWithSemester();
|
||||
updatePeriodLabel();
|
||||
resetResults('Нажмите «Показать», чтобы обновить расписание');
|
||||
});
|
||||
resultPreviousButton?.addEventListener('click', () => moveResult(-1));
|
||||
resultNextButton?.addEventListener('click', () => moveResult(1));
|
||||
tables?.addEventListener('click', event => {
|
||||
const button = event.target.closest('[data-schedule-override-edit]');
|
||||
if (!button) return;
|
||||
const lesson = state.lessons.find(item =>
|
||||
String(item.scheduleRuleSlotId) === button.dataset.baseRuleSlotId
|
||||
&& (item.originalLessonDate || item.date) === button.dataset.lessonDate
|
||||
);
|
||||
if (lesson) overridePanel.openLesson(lesson);
|
||||
});
|
||||
if (typeof mobileMedia.addEventListener === 'function') {
|
||||
mobileMedia.addEventListener('change', renderActiveSection);
|
||||
} else if (typeof mobileMedia.addListener === 'function') {
|
||||
@@ -107,6 +126,13 @@ export async function initScheduleView() {
|
||||
|
||||
try {
|
||||
state.dictionaries = await loadDictionaries(role, userDepartmentId);
|
||||
selectCurrentSemester();
|
||||
overridePanel = createScheduleOverridePanel({
|
||||
role,
|
||||
dictionaries: state.dictionaries,
|
||||
getRange: () => state.range,
|
||||
onChanged: loadData
|
||||
});
|
||||
configureDepartmentAccess();
|
||||
updateEntityFieldVisibility();
|
||||
await loadData();
|
||||
@@ -192,7 +218,7 @@ export async function initScheduleView() {
|
||||
}
|
||||
|
||||
const target = activeTarget();
|
||||
const range = twoWeekRange(dateInput.value);
|
||||
const range = scheduleRangeForSemester(dateInput.value, selectedSemester());
|
||||
const previousKey = state.sections[state.activeSectionIndex]?.key;
|
||||
state.range = range;
|
||||
updatePeriodLabel(range);
|
||||
@@ -213,7 +239,14 @@ export async function initScheduleView() {
|
||||
addOptional(params, 'parity', valueOf('schedule-view-parity'));
|
||||
|
||||
try {
|
||||
const lessons = await api.get(`/api/schedule/search?${params}`);
|
||||
const [lessons, overrides] = await Promise.all([
|
||||
api.get(`/api/schedule/search?${params}`),
|
||||
canEditScheduleOverrides(role)
|
||||
? api.get(`/api/edu-office/schedule/overrides?startDate=${range.startDate}&endDate=${range.endDate}`)
|
||||
: Promise.resolve([])
|
||||
]);
|
||||
state.overrides = overrides;
|
||||
overridePanel.setRegistry(overrides, range);
|
||||
renderLessons(lessons, range, target, previousKey);
|
||||
} catch (error) {
|
||||
const message = error.message || 'Ошибка загрузки расписания';
|
||||
@@ -245,6 +278,7 @@ export async function initScheduleView() {
|
||||
}
|
||||
|
||||
function renderLessons(lessons, range, target, previousKey) {
|
||||
state.lessons = lessons;
|
||||
state.totalLessons = lessons.length;
|
||||
if (!lessons.length) {
|
||||
state.sections = [];
|
||||
@@ -581,6 +615,9 @@ export async function initScheduleView() {
|
||||
lesson.classroomId || lesson.classroomName || '',
|
||||
lesson.lessonTypeId || lesson.lessonTypeName || '',
|
||||
lesson.lessonFormat || '',
|
||||
lesson.scheduleOverrideId || '',
|
||||
lesson.overrideAction || '',
|
||||
lesson.originalLessonDate || '',
|
||||
valueListSignature(lesson.groupIds, lesson.groupNames),
|
||||
valueListSignature(lesson.subgroupIds, lesson.subgroupNames)
|
||||
].join(':');
|
||||
@@ -672,19 +709,36 @@ export async function initScheduleView() {
|
||||
|
||||
const weeksText = getLessonWeeksText(lesson);
|
||||
const weeksBadge = weeksText ? `<span class="lesson-weeks-badge">${escapeHtml(weeksText)}</span>` : '';
|
||||
const sourceDate = lesson.originalLessonDate || lesson.date;
|
||||
const overrideBadge = lesson.scheduleOverrideId
|
||||
? '<span class="schedule-override-badge">Разовая правка</span>'
|
||||
: '';
|
||||
const editButton = canEditScheduleOverrides(role) && lesson.scheduleRuleSlotId && sourceDate
|
||||
? `
|
||||
<button type="button"
|
||||
class="schedule-override-edit"
|
||||
data-schedule-override-edit
|
||||
data-base-rule-slot-id="${escapeHtml(lesson.scheduleRuleSlotId)}"
|
||||
data-lesson-date="${escapeHtml(sourceDate)}">
|
||||
Изменить
|
||||
</button>
|
||||
`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="lesson-card schedule-view-lesson-card">
|
||||
<div class="lesson-subject" style="display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem;">
|
||||
<div class="lesson-subject schedule-view-lesson-title" style="display: flex; justify-content: space-between; align-items: baseline; gap: 0.5rem;">
|
||||
<span>${escapeHtml(lesson.subjectName || 'Без дисциплины')}</span>
|
||||
${weeksBadge}
|
||||
</div>
|
||||
${overrideBadge}
|
||||
<div class="lesson-group">${escapeHtml((lesson.groupNames || []).join(', ') || 'Группа не указана')}</div>
|
||||
${subgroupText ? `<div class="lesson-subgroup">${escapeHtml(subgroupText)}</div>` : ''}
|
||||
<div class="lesson-teacher">${escapeHtml(lesson.teacherName || 'Преподаватель не указан')}</div>
|
||||
<div class="lesson-meta">${escapeHtml(lesson.classroomName || 'Аудитория не указана')}</div>
|
||||
${meta ? `<div class="lesson-meta">${escapeHtml(meta)}</div>` : ''}
|
||||
${lesson.scheduleRuleSlotId ? `<div class="lesson-meta">ID слота: ${escapeHtml(lesson.scheduleRuleSlotId)}</div>` : ''}
|
||||
${editButton}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -699,6 +753,7 @@ export async function initScheduleView() {
|
||||
state.activeSectionIndex = 0;
|
||||
state.mobileDay = 1;
|
||||
state.totalLessons = 0;
|
||||
state.lessons = [];
|
||||
renderResultNavigation();
|
||||
tableTitle.textContent = 'Расписание';
|
||||
tableCaption.textContent = message;
|
||||
@@ -714,7 +769,36 @@ export async function initScheduleView() {
|
||||
periodLabel.textContent = 'Период не выбран';
|
||||
return;
|
||||
}
|
||||
periodLabel.textContent = periodCaption(range || twoWeekRange(dateInput.value));
|
||||
periodLabel.textContent = periodCaption(
|
||||
range || scheduleRangeForSemester(dateInput.value, selectedSemester()),
|
||||
selectedSemester()
|
||||
);
|
||||
}
|
||||
|
||||
function selectedSemester() {
|
||||
const semesterId = semesterSelect?.value;
|
||||
if (!semesterId) return null;
|
||||
return (state.dictionaries.semesters || [])
|
||||
.find(semester => String(semester.id) === String(semesterId)) || null;
|
||||
}
|
||||
|
||||
function selectCurrentSemester() {
|
||||
if (!semesterSelect) return;
|
||||
const currentSemester = findSemesterForDate(state.dictionaries.semesters, dateInput.value);
|
||||
semesterSelect.value = currentSemester ? String(currentSemester.id) : '';
|
||||
synchronizeDateWithSemester();
|
||||
updatePeriodLabel();
|
||||
}
|
||||
|
||||
function synchronizeDateWithSemester() {
|
||||
const semester = selectedSemester();
|
||||
if (!semester) {
|
||||
dateInput.value = toIsoDate(new Date());
|
||||
return;
|
||||
}
|
||||
if (!dateWithinSemester(dateInput.value, semester)) {
|
||||
dateInput.value = semester.startDate;
|
||||
}
|
||||
}
|
||||
|
||||
function groupNameById(id) {
|
||||
@@ -727,15 +811,23 @@ export async function initScheduleView() {
|
||||
}
|
||||
}
|
||||
|
||||
function createEmptyOverridePanel() {
|
||||
return {
|
||||
setRegistry() {},
|
||||
openLesson() {},
|
||||
openRegistry() {}
|
||||
};
|
||||
}
|
||||
|
||||
async function loadDictionaries(role, userDepartmentId) {
|
||||
const [groups, teachers, classrooms, departments, subjects, lessonTypes, academicYears] = await Promise.all([
|
||||
const [groups, teachers, classrooms, departments, subjects, lessonTypes, semesters] = await Promise.all([
|
||||
api.get('/api/groups'),
|
||||
api.get('/api/users/teachers'),
|
||||
api.get('/api/classrooms'),
|
||||
api.get('/api/departments'),
|
||||
api.get('/api/subjects'),
|
||||
api.get('/api/lesson-types'),
|
||||
api.get('/api/admin/calendar/years').catch(() => [])
|
||||
api.get('/api/schedule/semesters')
|
||||
]);
|
||||
|
||||
const visibleGroups = role === ROLE_DEPARTMENT && userDepartmentId
|
||||
@@ -748,13 +840,14 @@ async function loadDictionaries(role, userDepartmentId) {
|
||||
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,
|
||||
departments,
|
||||
semesters: (academicYears || []).flatMap(year => year.semesters || [])
|
||||
semesters: semesters || []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -975,8 +1068,43 @@ function mobileDayCaption(datesByParity) {
|
||||
return 'Нечётная / Чётная';
|
||||
}
|
||||
|
||||
function periodCaption(range) {
|
||||
return 'Учебное расписание';
|
||||
function periodCaption(range, semester = null) {
|
||||
if (!range) return 'Учебное расписание';
|
||||
const rangeLabel = `${formatDate(range.startDate)} — ${formatDate(range.endDate)}`;
|
||||
return semester ? `${semesterOptionLabel(semester)} · ${rangeLabel}` : rangeLabel;
|
||||
}
|
||||
|
||||
export function semesterOptionLabel(semester) {
|
||||
const type = String(semester?.semesterType || '').toLowerCase();
|
||||
const typeLabel = type === 'autumn'
|
||||
? 'Осенний семестр'
|
||||
: (type === 'spring' ? 'Весенний семестр' : 'Семестр');
|
||||
const academicYear = semester?.academicYearTitle || 'Учебный год';
|
||||
const dates = semester?.startDate && semester?.endDate
|
||||
? ` · ${formatDate(semester.startDate)} — ${formatDate(semester.endDate)}`
|
||||
: '';
|
||||
return `${academicYear} · ${typeLabel}${dates}`;
|
||||
}
|
||||
|
||||
export function findSemesterForDate(semesters, date) {
|
||||
return (semesters || []).find(semester => dateWithinSemester(date, semester)) || null;
|
||||
}
|
||||
|
||||
export function scheduleRangeForSemester(selectedDate, semester) {
|
||||
const anchorDate = semester && !dateWithinSemester(selectedDate, semester)
|
||||
? semester.startDate
|
||||
: selectedDate;
|
||||
return twoWeekRange(anchorDate);
|
||||
}
|
||||
|
||||
function dateWithinSemester(date, semester) {
|
||||
return Boolean(
|
||||
date
|
||||
&& semester?.startDate
|
||||
&& semester?.endDate
|
||||
&& date >= semester.startDate
|
||||
&& date <= semester.endDate
|
||||
);
|
||||
}
|
||||
|
||||
function lessonSlotKey(lesson) {
|
||||
|
||||
Reference in New Issue
Block a user