исправил просмотр расписаний
This commit is contained in:
@@ -2,6 +2,10 @@ import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
|
||||
const ROLE_DEPARTMENT = 'DEPARTMENT';
|
||||
const TARGET_GROUPS = 'groups';
|
||||
const TARGET_TEACHERS = 'teachers';
|
||||
const TARGET_CLASSROOMS = 'classrooms';
|
||||
const TARGET_DEPARTMENTS = 'departments';
|
||||
const PARITY_LABELS = {
|
||||
BOTH: 'каждая',
|
||||
ODD: 'нечётная',
|
||||
@@ -30,104 +34,418 @@ const CELL_PARITY_LAYOUT = [
|
||||
{ parity: 'ODD', label: 'Нечётная' },
|
||||
{ parity: 'EVEN', label: 'Чётная' }
|
||||
];
|
||||
const SPLIT_AUTO = 'auto';
|
||||
const SPLIT_GROUPS = 'groups';
|
||||
const SPLIT_TEACHERS = 'teachers';
|
||||
const SPLIT_CLASSROOMS = 'classrooms';
|
||||
const MOBILE_QUERY = '(max-width: 760px)';
|
||||
|
||||
export async function initScheduleView() {
|
||||
const role = localStorage.getItem('role');
|
||||
const userDepartmentId = localStorage.getItem('departmentId');
|
||||
const userDepartmentId = normalizeId(localStorage.getItem('departmentId'));
|
||||
const dateInput = document.getElementById('schedule-view-date');
|
||||
const targetSelect = document.getElementById('schedule-view-target');
|
||||
const loadButton = document.getElementById('schedule-view-load');
|
||||
const departmentField = document.getElementById('schedule-view-department-field');
|
||||
const departmentSelect = document.getElementById('schedule-view-department');
|
||||
const tables = document.getElementById('schedule-view-tables');
|
||||
const previousPeriodButton = document.getElementById('schedule-view-prev-period');
|
||||
const todayButton = document.getElementById('schedule-view-today');
|
||||
const nextPeriodButton = document.getElementById('schedule-view-next-period');
|
||||
const resultPreviousButton = document.getElementById('schedule-view-result-prev');
|
||||
const resultNextButton = document.getElementById('schedule-view-result-next');
|
||||
const resultTabs = document.getElementById('schedule-view-result-tabs');
|
||||
const activeLabel = document.getElementById('schedule-view-active-label');
|
||||
const countLabel = document.getElementById('schedule-view-count');
|
||||
const periodLabel = document.getElementById('schedule-view-period-label');
|
||||
const tableTitle = document.getElementById('schedule-view-table-title');
|
||||
const tableCaption = document.getElementById('schedule-view-table-caption');
|
||||
const dayTabs = document.getElementById('schedule-view-day-tabs');
|
||||
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 state = {
|
||||
dictionaries: {
|
||||
groups: [],
|
||||
teachers: [],
|
||||
classrooms: [],
|
||||
departments: []
|
||||
},
|
||||
sections: [],
|
||||
activeSectionIndex: 0,
|
||||
mobileDay: 1,
|
||||
range: null,
|
||||
totalLessons: 0
|
||||
};
|
||||
const mobileMedia = window.matchMedia(MOBILE_QUERY);
|
||||
|
||||
setDefaultDate(dateInput);
|
||||
configureDepartmentTarget();
|
||||
updatePeriodLabel();
|
||||
updateEntityFieldVisibility();
|
||||
renderResultNavigation();
|
||||
|
||||
targetSelect?.addEventListener('change', () => {
|
||||
clearInactiveEntityValues();
|
||||
updateEntityFieldVisibility();
|
||||
resetResults('Нажмите «Показать», чтобы обновить расписание');
|
||||
});
|
||||
loadButton?.addEventListener('click', loadData);
|
||||
previousPeriodButton?.addEventListener('click', () => movePeriod(-1));
|
||||
todayButton?.addEventListener('click', () => {
|
||||
dateInput.value = toIsoDate(new Date());
|
||||
loadData();
|
||||
});
|
||||
nextPeriodButton?.addEventListener('click', () => movePeriod(1));
|
||||
dateInput?.addEventListener('change', () => {
|
||||
updatePeriodLabel();
|
||||
resetResults('Нажмите «Показать», чтобы обновить расписание');
|
||||
});
|
||||
resultPreviousButton?.addEventListener('click', () => moveResult(-1));
|
||||
resultNextButton?.addEventListener('click', () => moveResult(1));
|
||||
if (typeof mobileMedia.addEventListener === 'function') {
|
||||
mobileMedia.addEventListener('change', renderActiveSection);
|
||||
} else if (typeof mobileMedia.addListener === 'function') {
|
||||
mobileMedia.addListener(renderActiveSection);
|
||||
}
|
||||
|
||||
try {
|
||||
await loadDictionaries(role, userDepartmentId);
|
||||
if (role === ROLE_DEPARTMENT) {
|
||||
departmentField.hidden = true;
|
||||
departmentSelect.value = userDepartmentId || '';
|
||||
}
|
||||
state.dictionaries = await loadDictionaries(role, userDepartmentId);
|
||||
configureDepartmentAccess();
|
||||
updateEntityFieldVisibility();
|
||||
await loadData();
|
||||
} catch (error) {
|
||||
setTableState(error.message || 'Ошибка загрузки справочников');
|
||||
const message = error.message || 'Ошибка загрузки справочников';
|
||||
showAlert('schedule-view-alert', message, 'error');
|
||||
resetResults(message);
|
||||
}
|
||||
|
||||
function configureDepartmentTarget() {
|
||||
if (role !== ROLE_DEPARTMENT) return;
|
||||
targetSelect.value = TARGET_DEPARTMENTS;
|
||||
targetSelect.disabled = true;
|
||||
}
|
||||
|
||||
function configureDepartmentAccess() {
|
||||
if (role !== ROLE_DEPARTMENT) return;
|
||||
targetSelect.value = TARGET_DEPARTMENTS;
|
||||
targetSelect.disabled = true;
|
||||
if (departmentSelect) {
|
||||
departmentSelect.value = userDepartmentId || '';
|
||||
departmentSelect.disabled = true;
|
||||
if (userDepartmentId && departmentSelect.value !== userDepartmentId) {
|
||||
departmentSelect.insertAdjacentHTML('afterbegin',
|
||||
`<option value="${escapeHtml(userDepartmentId)}">Своя кафедра</option>`);
|
||||
departmentSelect.value = userDepartmentId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateEntityFieldVisibility() {
|
||||
const target = activeTarget();
|
||||
entityFields.forEach(field => {
|
||||
field.hidden = field.dataset.scheduleViewTargetField !== target;
|
||||
});
|
||||
}
|
||||
|
||||
function clearInactiveEntityValues() {
|
||||
const target = activeTarget();
|
||||
[
|
||||
[TARGET_GROUPS, 'schedule-view-group'],
|
||||
[TARGET_TEACHERS, 'schedule-view-teacher'],
|
||||
[TARGET_CLASSROOMS, 'schedule-view-classroom'],
|
||||
[TARGET_DEPARTMENTS, 'schedule-view-department']
|
||||
].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 = '';
|
||||
});
|
||||
}
|
||||
|
||||
function activeTarget() {
|
||||
if (role === ROLE_DEPARTMENT) return TARGET_DEPARTMENTS;
|
||||
return targetSelect?.value || TARGET_GROUPS;
|
||||
}
|
||||
|
||||
async function movePeriod(direction) {
|
||||
if (!dateInput.value) {
|
||||
dateInput.value = toIsoDate(new Date());
|
||||
}
|
||||
const range = twoWeekRange(dateInput.value);
|
||||
const nextDate = addDays(parseIsoDate(range.startDate), direction * 14);
|
||||
dateInput.value = toIsoDate(nextDate);
|
||||
await loadData();
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
hideAlert('schedule-view-alert');
|
||||
setTableState('Загрузка...');
|
||||
countLabel.textContent = 'Загрузка...';
|
||||
|
||||
if (!dateInput.value) {
|
||||
const message = 'Выберите дату в периоде';
|
||||
showAlert('schedule-view-alert', message, 'error');
|
||||
setTableState(message);
|
||||
countLabel.textContent = '0 занятий';
|
||||
resetResults(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (role === ROLE_DEPARTMENT && !userDepartmentId) {
|
||||
const message = 'Не удалось определить кафедру пользователя';
|
||||
showAlert('schedule-view-alert', message, 'error');
|
||||
resetResults(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const target = activeTarget();
|
||||
const range = twoWeekRange(dateInput.value);
|
||||
const previousKey = state.sections[state.activeSectionIndex]?.key;
|
||||
state.range = range;
|
||||
updatePeriodLabel(range);
|
||||
setTableState('Загрузка...');
|
||||
setActiveSummary('Загрузка расписаний...');
|
||||
countLabel.textContent = 'Загрузка...';
|
||||
resultTabs.innerHTML = '';
|
||||
dayTabs.hidden = true;
|
||||
dayTabs.innerHTML = '';
|
||||
|
||||
const params = new URLSearchParams({
|
||||
startDate: range.startDate,
|
||||
endDate: range.endDate
|
||||
});
|
||||
addOptional(params, 'groupId', valueOf('schedule-view-group'));
|
||||
addOptional(params, 'teacherId', valueOf('schedule-view-teacher'));
|
||||
addOptional(params, 'classroomId', valueOf('schedule-view-classroom'));
|
||||
addOptional(params, 'departmentId', role === ROLE_DEPARTMENT ? userDepartmentId : departmentSelect.value);
|
||||
applyTargetFilter(params, target);
|
||||
addOptional(params, 'subjectId', valueOf('schedule-view-subject'));
|
||||
addOptional(params, 'lessonTypeId', valueOf('schedule-view-lesson-type'));
|
||||
addOptional(params, 'parity', valueOf('schedule-view-parity'));
|
||||
|
||||
try {
|
||||
const lessons = await api.get(`/api/schedule/search?${params}`);
|
||||
renderLessons(lessons, range);
|
||||
renderLessons(lessons, range, target, previousKey);
|
||||
} catch (error) {
|
||||
const message = error.message || 'Ошибка загрузки расписания';
|
||||
showAlert('schedule-view-alert', message, 'error');
|
||||
setTableState(message);
|
||||
countLabel.textContent = '0 занятий';
|
||||
resetResults(message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderLessons(lessons, range) {
|
||||
countLabel.textContent = lessonCountLabel(lessons.length);
|
||||
function applyTargetFilter(params, target) {
|
||||
if (role === ROLE_DEPARTMENT) {
|
||||
params.set('departmentId', userDepartmentId);
|
||||
return;
|
||||
}
|
||||
if (target === TARGET_GROUPS) {
|
||||
addOptional(params, 'groupId', valueOf('schedule-view-group'));
|
||||
return;
|
||||
}
|
||||
if (target === TARGET_TEACHERS) {
|
||||
addOptional(params, 'teacherId', valueOf('schedule-view-teacher'));
|
||||
return;
|
||||
}
|
||||
if (target === TARGET_CLASSROOMS) {
|
||||
addOptional(params, 'classroomId', valueOf('schedule-view-classroom'));
|
||||
return;
|
||||
}
|
||||
if (target === TARGET_DEPARTMENTS) {
|
||||
addOptional(params, 'departmentId', valueOf('schedule-view-department'));
|
||||
}
|
||||
}
|
||||
|
||||
function renderLessons(lessons, range, target, previousKey) {
|
||||
state.totalLessons = lessons.length;
|
||||
if (!lessons.length) {
|
||||
state.sections = [];
|
||||
state.activeSectionIndex = 0;
|
||||
renderResultNavigation();
|
||||
tableTitle.textContent = 'Расписание';
|
||||
tableCaption.textContent = periodCaption(range);
|
||||
setTableState('Занятия не найдены');
|
||||
countLabel.textContent = '0 занятий';
|
||||
return;
|
||||
}
|
||||
const sections = buildScheduleSections(lessons);
|
||||
|
||||
const sections = buildScheduleSections(lessons, splitTypeForTarget(target), buildSectionContext(target));
|
||||
if (!sections.length) {
|
||||
setTableState('Нет данных для выбранного разреза таблиц');
|
||||
state.sections = [];
|
||||
state.activeSectionIndex = 0;
|
||||
renderResultNavigation();
|
||||
tableTitle.textContent = 'Расписание';
|
||||
tableCaption.textContent = periodCaption(range);
|
||||
setTableState('Нет данных для выбранного режима просмотра');
|
||||
countLabel.textContent = lessonCountLabel(lessons.length);
|
||||
return;
|
||||
}
|
||||
const tableCount = sections.reduce((count, section) =>
|
||||
count + buildCombinedWeekBlocks(range.startDate, range.endDate, section.lessons).length, 0
|
||||
);
|
||||
if (!tableCount) {
|
||||
setTableState('Выберите корректный период');
|
||||
return;
|
||||
|
||||
state.sections = sections;
|
||||
state.activeSectionIndex = Math.max(0, sections.findIndex(section => section.key === previousKey));
|
||||
if (state.activeSectionIndex < 0) {
|
||||
state.activeSectionIndex = 0;
|
||||
}
|
||||
countLabel.textContent = `${lessonCountLabel(lessons.length)}, ${tableCountLabel(tableCount)}`;
|
||||
tables.innerHTML = sections
|
||||
.map(section => renderScheduleSection(section, range))
|
||||
.join('');
|
||||
state.mobileDay = preferredMobileDay(range);
|
||||
renderResultNavigation();
|
||||
renderActiveSection();
|
||||
}
|
||||
|
||||
function renderScheduleSection(section, range) {
|
||||
function buildSectionContext(target) {
|
||||
const selectedGroupId = target === TARGET_GROUPS ? normalizeId(valueOf('schedule-view-group')) : '';
|
||||
const selectedDepartmentId = role === ROLE_DEPARTMENT
|
||||
? userDepartmentId
|
||||
: (target === TARGET_DEPARTMENTS ? normalizeId(valueOf('schedule-view-department')) : '');
|
||||
const allowedGroupIds = new Set();
|
||||
if (selectedGroupId) {
|
||||
allowedGroupIds.add(selectedGroupId);
|
||||
} else if (selectedDepartmentId) {
|
||||
state.dictionaries.groups
|
||||
.filter(group => String(group.departmentId) === String(selectedDepartmentId))
|
||||
.forEach(group => allowedGroupIds.add(String(group.id)));
|
||||
}
|
||||
return {
|
||||
selectedGroupId,
|
||||
selectedGroupName: selectedGroupId ? groupNameById(selectedGroupId) : '',
|
||||
allowedGroupIds,
|
||||
groupNamesById: new Map(state.dictionaries.groups.map(group => [String(group.id), group.name]))
|
||||
};
|
||||
}
|
||||
|
||||
function splitTypeForTarget(target) {
|
||||
if (target === TARGET_TEACHERS) return TARGET_TEACHERS;
|
||||
if (target === TARGET_CLASSROOMS) return TARGET_CLASSROOMS;
|
||||
return TARGET_GROUPS;
|
||||
}
|
||||
|
||||
function renderResultNavigation() {
|
||||
const section = activeSection();
|
||||
const totalSections = state.sections.length;
|
||||
countLabel.textContent = state.totalLessons
|
||||
? `${lessonCountLabel(state.totalLessons)}, ${sectionCountLabel(totalSections)}`
|
||||
: '0 занятий';
|
||||
|
||||
resultPreviousButton.disabled = totalSections < 2;
|
||||
resultNextButton.disabled = totalSections < 2;
|
||||
|
||||
if (!section) {
|
||||
setActiveSummary('Нет выбранного расписания');
|
||||
resultTabs.innerHTML = '<span class="schedule-view-empty-result">Ничего не найдено</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveSummary(`${section.title} · ${lessonCountLabel(section.lessons.length)} · ${state.activeSectionIndex + 1} из ${totalSections}`);
|
||||
resultTabs.innerHTML = state.sections.map((item, index) => `
|
||||
<button type="button"
|
||||
class="schedule-view-result-tab${index === state.activeSectionIndex ? ' active' : ''}"
|
||||
data-schedule-view-section="${index}">
|
||||
<span>${escapeHtml(item.title)}</span>
|
||||
<small>${escapeHtml(lessonCountLabel(item.lessons.length))}</small>
|
||||
</button>
|
||||
`).join('');
|
||||
resultTabs.querySelectorAll('[data-schedule-view-section]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
state.activeSectionIndex = Number(button.dataset.scheduleViewSection);
|
||||
state.mobileDay = preferredMobileDay(state.range);
|
||||
renderResultNavigation();
|
||||
renderActiveSection();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setActiveSummary(text) {
|
||||
activeLabel.textContent = text;
|
||||
}
|
||||
|
||||
function moveResult(direction) {
|
||||
if (state.sections.length < 2) return;
|
||||
state.activeSectionIndex = (state.activeSectionIndex + direction + state.sections.length) % state.sections.length;
|
||||
state.mobileDay = preferredMobileDay(state.range);
|
||||
renderResultNavigation();
|
||||
renderActiveSection();
|
||||
}
|
||||
|
||||
function activeSection() {
|
||||
return state.sections[state.activeSectionIndex] || null;
|
||||
}
|
||||
|
||||
function renderActiveSection() {
|
||||
const section = activeSection();
|
||||
if (!section || !state.range) {
|
||||
dayTabs.hidden = true;
|
||||
dayTabs.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
tableTitle.textContent = section.title;
|
||||
tableCaption.textContent = `${lessonCountLabel(section.lessons.length)} · ${periodCaption(state.range)}`;
|
||||
|
||||
if (mobileMedia.matches) {
|
||||
renderMobileDayTabs();
|
||||
renderMobileSchedule(section, state.range);
|
||||
return;
|
||||
}
|
||||
|
||||
dayTabs.hidden = true;
|
||||
dayTabs.innerHTML = '';
|
||||
renderDesktopSchedule(section, state.range);
|
||||
}
|
||||
|
||||
function renderDesktopSchedule(section, range) {
|
||||
const slots = collectSlots(section.lessons);
|
||||
const lessonsByDateSlot = groupLessonsByDateSlot(section.lessons);
|
||||
const blocks = buildCombinedWeekBlocks(range.startDate, range.endDate, section.lessons);
|
||||
return blocks
|
||||
if (!blocks.length || !slots.length) {
|
||||
setTableState('Занятия не найдены');
|
||||
return;
|
||||
}
|
||||
tables.innerHTML = blocks
|
||||
.map(block => renderCombinedWeekTable(block, slots, lessonsByDateSlot, section))
|
||||
.join('');
|
||||
}
|
||||
|
||||
function renderMobileDayTabs() {
|
||||
dayTabs.hidden = false;
|
||||
dayTabs.innerHTML = DAY_ORDER.map(day => `
|
||||
<button type="button"
|
||||
class="schedule-view-day-tab${day === state.mobileDay ? ' active' : ''}"
|
||||
data-schedule-view-day="${day}">
|
||||
${escapeHtml(dayShort(day))}
|
||||
</button>
|
||||
`).join('');
|
||||
dayTabs.querySelectorAll('[data-schedule-view-day]').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
state.mobileDay = Number(button.dataset.scheduleViewDay);
|
||||
renderActiveSection();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderMobileSchedule(section, range) {
|
||||
const slots = collectSlots(section.lessons);
|
||||
const lessonsByDateSlot = groupLessonsByDateSlot(section.lessons);
|
||||
const blocks = buildCombinedWeekBlocks(range.startDate, range.endDate, section.lessons);
|
||||
if (!blocks.length || !slots.length) {
|
||||
setTableState('Занятия не найдены');
|
||||
return;
|
||||
}
|
||||
tables.innerHTML = blocks.map(block => renderMobileDayBlock(block, slots, lessonsByDateSlot)).join('');
|
||||
}
|
||||
|
||||
function renderMobileDayBlock(block, slots, lessonsByDateSlot) {
|
||||
const dayDates = block.datesByDay[state.mobileDay] || {};
|
||||
const rows = slots.map(slot => {
|
||||
const states = slotStatesForDates(dayDates, slot, lessonsByDateSlot);
|
||||
return `
|
||||
<article class="schedule-view-mobile-slot">
|
||||
<div class="schedule-view-mobile-slot-time">
|
||||
<strong>${escapeHtml(slot.orderLabel)}</strong>
|
||||
<span>${escapeHtml(slot.timeLabel)}</span>
|
||||
</div>
|
||||
<div class="schedule-view-mobile-slot-content">
|
||||
${renderSlotContent(states)}
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<section class="schedule-view-mobile-day">
|
||||
<div class="schedule-view-mobile-day-heading">
|
||||
<strong>${escapeHtml(DAY_FULL_LABELS[state.mobileDay] || dayShort(state.mobileDay))}</strong>
|
||||
<span>${escapeHtml(mobileDayCaption(dayDates))}</span>
|
||||
</div>
|
||||
${rows}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderCombinedWeekTable(block, slots, lessonsByDateSlot, section) {
|
||||
const dayHeaders = DAY_ORDER.map(day => `
|
||||
<th class="room-day-cell">
|
||||
@@ -152,8 +470,8 @@ export async function initScheduleView() {
|
||||
return `
|
||||
<section class="schedule-view-week-panel">
|
||||
<div class="room-week-heading">
|
||||
<h3>${escapeHtml(section.title)} · ${escapeHtml(block.label)}</h3>
|
||||
<span>${escapeHtml(`${lessonCountLabel(section.lessons.length)}; ${combinedWeekCaption(block)}`)}</span>
|
||||
<h3>${escapeHtml(section.title)}</h3>
|
||||
<span>${escapeHtml(combinedWeekCaption(block))}</span>
|
||||
</div>
|
||||
<div class="workload-grid-container schedule-view-grid-container">
|
||||
<table class="workload-table room-week-table schedule-view-matrix-table">
|
||||
@@ -175,20 +493,26 @@ export async function initScheduleView() {
|
||||
}
|
||||
|
||||
function renderCombinedSlotCell(datesByParity, slot, lessonsByDateSlot) {
|
||||
const states = CELL_PARITY_LAYOUT.map(({ parity, label }) => ({
|
||||
const states = slotStatesForDates(datesByParity, slot, lessonsByDateSlot);
|
||||
const singleCellClass = slotStatesEqual(states[0].state, states[1].state) ? ' room-single-cell' : '';
|
||||
return `<td class="room-combined-cell${singleCellClass}">${renderSlotContent(states)}</td>`;
|
||||
}
|
||||
|
||||
function slotStatesForDates(datesByParity, slot, lessonsByDateSlot) {
|
||||
return CELL_PARITY_LAYOUT.map(({ parity, label }) => ({
|
||||
parity,
|
||||
label,
|
||||
state: scheduleSlotState(datesByParity[parity], slot, lessonsByDateSlot)
|
||||
}));
|
||||
}
|
||||
|
||||
function renderSlotContent(states) {
|
||||
if (slotStatesEqual(states[0].state, states[1].state)) {
|
||||
return `<td class="room-combined-cell room-single-cell">${renderUnifiedSlot(states[0].state)}</td>`;
|
||||
return renderUnifiedSlot(states[0].state);
|
||||
}
|
||||
|
||||
const halves = states.map(({ parity, label, state }) =>
|
||||
return states.map(({ parity, label, state }) =>
|
||||
renderSlotHalf(state, parity, label)
|
||||
).join('');
|
||||
return `<td class="room-combined-cell">${halves}</td>`;
|
||||
}
|
||||
|
||||
function scheduleSlotState(date, slot, lessonsByDateSlot) {
|
||||
@@ -298,6 +622,38 @@ export async function initScheduleView() {
|
||||
if (!tables) return;
|
||||
tables.innerHTML = `<div class="loading-row schedule-view-state">${escapeHtml(message)}</div>`;
|
||||
}
|
||||
|
||||
function resetResults(message) {
|
||||
state.sections = [];
|
||||
state.activeSectionIndex = 0;
|
||||
state.mobileDay = 1;
|
||||
state.totalLessons = 0;
|
||||
renderResultNavigation();
|
||||
tableTitle.textContent = 'Расписание';
|
||||
tableCaption.textContent = message;
|
||||
setTableState(message);
|
||||
countLabel.textContent = '0 занятий';
|
||||
dayTabs.hidden = true;
|
||||
dayTabs.innerHTML = '';
|
||||
}
|
||||
|
||||
function updatePeriodLabel(range) {
|
||||
if (!periodLabel) return;
|
||||
if (!dateInput.value) {
|
||||
periodLabel.textContent = 'Период не выбран';
|
||||
return;
|
||||
}
|
||||
periodLabel.textContent = periodCaption(range || twoWeekRange(dateInput.value));
|
||||
}
|
||||
|
||||
function groupNameById(id) {
|
||||
return state.dictionaries.groups.find(group => String(group.id) === String(id))?.name || `Группа ${id}`;
|
||||
}
|
||||
|
||||
function preferredMobileDay(range) {
|
||||
if (!range) return 1;
|
||||
return isoDay(dateInput.value || range.startDate);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDictionaries(role, userDepartmentId) {
|
||||
@@ -310,16 +666,23 @@ async function loadDictionaries(role, userDepartmentId) {
|
||||
api.get('/api/lesson-types')
|
||||
]);
|
||||
|
||||
fillSelect('schedule-view-group', groups, item => item.id, item => item.name, 'Все группы');
|
||||
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, 'Все типы');
|
||||
|
||||
if (role === ROLE_DEPARTMENT && userDepartmentId) {
|
||||
document.getElementById('schedule-view-department').value = userDepartmentId;
|
||||
}
|
||||
return {
|
||||
groups,
|
||||
teachers,
|
||||
classrooms,
|
||||
departments
|
||||
};
|
||||
}
|
||||
|
||||
function setDefaultDate(dateInput) {
|
||||
@@ -342,6 +705,11 @@ function valueOf(id) {
|
||||
return document.getElementById(id)?.value || '';
|
||||
}
|
||||
|
||||
function normalizeId(value) {
|
||||
if (!value || value === 'null' || value === 'undefined') return '';
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function roomLabel(room) {
|
||||
return [room.name, room.building, room.floor ? `${room.floor} этаж` : null].filter(Boolean).join(' · ');
|
||||
}
|
||||
@@ -385,12 +753,11 @@ function groupLessonsByDateSlot(lessons) {
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function buildScheduleSections(lessons) {
|
||||
const splitType = resolveSplitType();
|
||||
function buildScheduleSections(lessons, splitType, context) {
|
||||
const sections = new Map();
|
||||
|
||||
lessons.forEach(lesson => {
|
||||
splitEntitiesForLesson(lesson, splitType).forEach(entity => {
|
||||
splitEntitiesForLesson(lesson, splitType, context).forEach(entity => {
|
||||
if (!sections.has(entity.key)) {
|
||||
sections.set(entity.key, {
|
||||
key: entity.key,
|
||||
@@ -410,45 +777,42 @@ function buildScheduleSections(lessons) {
|
||||
.sort((a, b) => naturalCompare(a.title, b.title));
|
||||
}
|
||||
|
||||
function resolveSplitType() {
|
||||
const requested = valueOf('schedule-view-split');
|
||||
if ([SPLIT_GROUPS, SPLIT_TEACHERS, SPLIT_CLASSROOMS].includes(requested)) {
|
||||
return requested;
|
||||
}
|
||||
if (valueOf('schedule-view-teacher')) return SPLIT_TEACHERS;
|
||||
if (valueOf('schedule-view-classroom')) return SPLIT_CLASSROOMS;
|
||||
return SPLIT_GROUPS;
|
||||
}
|
||||
|
||||
function splitEntitiesForLesson(lesson, splitType) {
|
||||
if (splitType === SPLIT_TEACHERS) {
|
||||
function splitEntitiesForLesson(lesson, splitType, context) {
|
||||
if (splitType === TARGET_TEACHERS) {
|
||||
return [singleEntity('teacher', lesson.teacherId, lesson.teacherName, 'Преподаватель не указан')];
|
||||
}
|
||||
if (splitType === SPLIT_CLASSROOMS) {
|
||||
if (splitType === TARGET_CLASSROOMS) {
|
||||
return [singleEntity('classroom', lesson.classroomId, lesson.classroomName, 'Аудитория не указана')];
|
||||
}
|
||||
return listEntities('group', lesson.groupIds, lesson.groupNames, 'Группа не указана');
|
||||
if (context.selectedGroupId) {
|
||||
return [singleEntity('group', context.selectedGroupId, context.selectedGroupName, 'Группа не указана')];
|
||||
}
|
||||
return listEntities('group', lesson.groupIds, lesson.groupNames, 'Группа не указана', context.allowedGroupIds, context.groupNamesById);
|
||||
}
|
||||
|
||||
function singleEntity(prefix, id, name, fallback) {
|
||||
const title = name || fallback;
|
||||
return {
|
||||
id,
|
||||
key: `${prefix}:${id ?? title}`,
|
||||
title
|
||||
};
|
||||
}
|
||||
|
||||
function listEntities(prefix, ids = [], names = [], fallback) {
|
||||
function listEntities(prefix, ids = [], names = [], fallback, allowedIds = new Set(), namesById = new Map()) {
|
||||
const safeIds = Array.isArray(ids) ? ids : [];
|
||||
const safeNames = Array.isArray(names) ? names : [];
|
||||
if (!safeIds.length && !safeNames.length) {
|
||||
return [singleEntity(prefix, null, fallback, fallback)];
|
||||
return allowedIds.size ? [] : [singleEntity(prefix, null, fallback, fallback)];
|
||||
}
|
||||
const maxLength = Math.max(safeIds.length, safeNames.length);
|
||||
const entities = [];
|
||||
for (let index = 0; index < maxLength; index += 1) {
|
||||
const id = safeIds[index] ?? null;
|
||||
const name = safeNames[index] || (id == null ? fallback : `Группа ${id}`);
|
||||
if (allowedIds.size && (id == null || !allowedIds.has(String(id)))) {
|
||||
continue;
|
||||
}
|
||||
const name = namesById.get(String(id)) || safeNames[index] || (id == null ? fallback : `Группа ${id}`);
|
||||
entities.push(singleEntity(prefix, id, name, fallback));
|
||||
}
|
||||
return entities;
|
||||
@@ -540,6 +904,19 @@ function combinedWeekCaption(block) {
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
function mobileDayCaption(datesByParity) {
|
||||
return CELL_PARITY_LAYOUT
|
||||
.map(({ parity, label }) => {
|
||||
const date = datesByParity?.[parity];
|
||||
return `${label}: ${date ? formatDate(date) : 'даты нет'}`;
|
||||
})
|
||||
.join(' · ');
|
||||
}
|
||||
|
||||
function periodCaption(range) {
|
||||
return `${formatDate(range.startDate)} - ${formatDate(range.endDate)}`;
|
||||
}
|
||||
|
||||
function lessonSlotKey(lesson) {
|
||||
const value = lesson.timeSlotOrder ?? lesson.timeSlotId ?? `${lesson.startTime || ''}-${lesson.endTime || ''}`;
|
||||
return value ? String(value) : 'unknown';
|
||||
@@ -625,12 +1002,12 @@ function lessonCountLabel(count) {
|
||||
return `${count} занятий`;
|
||||
}
|
||||
|
||||
function tableCountLabel(count) {
|
||||
function sectionCountLabel(count) {
|
||||
const tail = count % 100;
|
||||
if (tail >= 11 && tail <= 14) return `${count} таблиц`;
|
||||
if (count % 10 === 1) return `${count} таблица`;
|
||||
if ([2, 3, 4].includes(count % 10)) return `${count} таблицы`;
|
||||
return `${count} таблиц`;
|
||||
if (tail >= 11 && tail <= 14) return `${count} расписаний`;
|
||||
if (count % 10 === 1) return `${count} расписание`;
|
||||
if ([2, 3, 4].includes(count % 10)) return `${count} расписания`;
|
||||
return `${count} расписаний`;
|
||||
}
|
||||
|
||||
function compareSlots(a, b) {
|
||||
|
||||
Reference in New Issue
Block a user