исправил просмотр расписаний
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
| **Администратор** | `ADMIN` | Полный доступ: пользователи, справочники, тенанты, роли, архивирование и восстановление. |
|
||||
| **Учебный отдел** | `EDUCATION_OFFICE` | Редактирование расписания, точечные переносы/замены/отмены, временные слоты, аудитории, загруженность. |
|
||||
| **Кафедра** | `DEPARTMENT` | Дисциплины своей кафедры, загрузка дисциплин, привязки преподавателей, комментарии, расписание и нагрузка кафедры. |
|
||||
| **Просмотр расписаний** | `SCHEDULE_VIEWER` | Read-only просмотр расписаний по группам, преподавателям, аудиториям и кафедрам в совмещённых таблицах чётной/нечётной недели с разрезом по группам, преподавателям или аудиториям. |
|
||||
| **Просмотр расписаний** | `SCHEDULE_VIEWER` | Read-only просмотр расписаний по группам, преподавателям, аудиториям и кафедрам в режиме одной активной совмещённой таблицы чётной/нечётной недели. |
|
||||
| **Преподаватель** | `TEACHER` | Просмотр своего расписания. В перспективе — подача заявок на перенос. |
|
||||
| **Студент** | `STUDENT` | Только просмотр расписания (Read-only). |
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ frontend/
|
||||
| `classrooms` | Аудитории | `/api/classrooms` |
|
||||
| `subjects` | Дисциплины | `/api/subjects` |
|
||||
| `department-workspace` | Кабинет кафедры: дисциплины, импорт, комментарии, преподаватели и нагрузка | `/api/department/*`, `/api/workload/teachers` |
|
||||
| `schedule-view` | Read-only просмотр расписаний: по одной выбранной дате строится двухнедельный диапазон и результат разбивается на отдельные совмещённые таблицы чётной/нечётной недели по группам, преподавателям или аудиториям | `/api/schedule/search` |
|
||||
| `schedule-view` | Read-only просмотр расписаний: по одной выбранной дате строится двухнедельный диапазон, найденные расписания выбираются в переключателе, а на экране отображается одна активная совмещённая таблица чётной/нечётной недели | `/api/schedule/search` |
|
||||
| `schedule` | Конструктор правил динамического расписания | `/api/admin/schedule-rules`, `/api/admin/time-slots`, `/api/admin/calendar/years`, `/api/lesson-types`, `/api/subgroups` |
|
||||
| `academic-calendar` | Учебные годы, семестры, создание календарных графиков и Excel-подобный редактор дневной сетки | `/api/admin/calendar`, `/api/admin/academic-calendars`, `/api/admin/calendar/activity-types`, `/api/education-forms` |
|
||||
| `auditorium-workload` | Динамическая загруженность аудиторий, преподавателей и кафедр: сводная матрица по дате или совмещённая таблица выбранной сущности по чётной/нечётной неделе | `/api/classrooms`, `/api/users/teachers`, `/api/departments`, `/api/admin/time-slots`, `/api/equipments`, `/api/groups`, `/api/schedule`, `/api/admin/calendar/years` |
|
||||
@@ -147,7 +147,7 @@ frontend/
|
||||
### Особенности админских вкладок
|
||||
|
||||
- Вкладка `groups` загружает кафедры, специальности, профили, учебные годы и календарные графики. Группа создаётся через `/api/groups` с `specialtyId` и `specialtyProfileId`, блок подгрупп использует `/api/subgroups` и `/api/groups/{id}/subgroups`, а блок назначений использует `/api/groups/{id}/calendar-assignments`.
|
||||
- Вкладка `schedule-view` показывает найденные занятия только в табличной матрице. Пользователь выбирает одну дату в периоде; frontend сам запрашивает двухнедельный диапазон от понедельника выбранной недели, чтобы обе половины совмещённой таблицы были заполнены одним периодом. Чтобы ячейка вроде «понедельник, 1 пара» не смешивала занятия разных групп и аудиторий, результат разбивается на отдельные таблицы. Поле «Таблицы по» поддерживает авто-режим, группы, преподавателей и аудитории; авто-режим выбирает преподавателей при фильтре преподавателя, аудитории при фильтре аудитории, иначе группы. Каждая таблица строится двухнедельными блоками: строки — пары, столбцы — дни недели. Нечётная неделя отображается в верхней половине ячейки, чётная — в нижней, а одинаковые занятия в обе недели схлопываются в цельную ячейку. Внутри ячеек используются те же карточки занятий, что и в таблицах загруженности.
|
||||
- Вкладка `schedule-view` показывает найденные занятия в режиме одной активной таблицы. Пользователь выбирает, что смотреть: группу, преподавателя, аудиторию или кафедру; frontend запрашивает двухнедельный диапазон от понедельника выбранной даты и собирает найденные расписания в переключатель результатов. На странице не выводится стек таблиц: виден один выбранный результат, а остальные доступны через чипы и кнопки предыдущего/следующего расписания. Для режима кафедры и роли `DEPARTMENT` расписание ограничивается кафедрой пользователя; преподавательские и студенческие отдельные страницы пока остаются самостоятельными. Таблица строится как строки пар и столбцы дней недели. Нечётная неделя отображается в верхней половине ячейки, чётная — в нижней, а одинаковые занятия в обе недели схлопываются в цельную ячейку. На мобильной ширине вместо широкой недельной матрицы показывается один день активного расписания с переключателем дней.
|
||||
- Вкладка `auditorium-workload` стала общей вкладкой `Загруженность`: в поле «Что смотреть» выбираются аудитории, преподаватели или кафедры. Сводная матрица по выбранной дате использует одинаковую структуру: строки — выбранный тип сущности, столбцы — эффективные временные слоты дня из `/api/admin/time-slots/effective`, занятость собирается из динамического расписания `/api/schedule` по группам. Кафедральная матрица группирует занятия по кафедре преподавателя. Для аудиторий доступны фильтры корпуса, вместимости и оборудования. В поле «Отображение» можно выбрать конкретную аудиторию, преподавателя или кафедру; тогда сводная матрица заменяется одной таблицей по дням недели и времени для двухнедельного периода от выбранной даты. Таблица выбранной сущности растягивается до нижней части экрана. Ячейка делится вертикально только если верхняя и нижняя недели отличаются: нечётная неделя отображается сверху, чётная — снизу. Если состояние или занятие одинаковое, ячейка остаётся цельной. Чётность берётся из расписания, а для свободных дней рассчитывается по семестрам из `/api/admin/calendar/years`.
|
||||
- Вкладка `profiles` выделена под профили обучения: администратор выбирает специальность, создаёт профиль, редактирует описание и удаляет неиспользуемые профили.
|
||||
- Вкладка `schedule` не обращается к старым `lessons` API. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы, отдельные часы и недели начала для лекций, лабораторных и практик, а также набор базовых слотов. Группы выбираются через выпадающий мультиселект. Поле подгруппы появляется только при выборе лабораторной работы; для лекций и практик оно не отображается. Если в правиле выбрана одна группа, селект подгруппы содержит пункт `Вся группа`; если выбрано несколько групп, лабораторный слот показывает мультиселект подгрупп, чтобы выбрать разные подгруппы разных групп. Из календарной системы здесь используется список семестров для выбора периода действия правила.
|
||||
|
||||
@@ -569,19 +569,205 @@
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.schedule-view-table-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.schedule-view-shell {
|
||||
min-height: calc(100vh - 8.5rem);
|
||||
display: grid;
|
||||
grid-template-rows: auto auto minmax(420px, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.schedule-view-control-card,
|
||||
.schedule-view-results-card,
|
||||
.schedule-view-table-card {
|
||||
animation-duration: 0.28s;
|
||||
}
|
||||
|
||||
.schedule-view-control-header,
|
||||
.schedule-view-results-header,
|
||||
.schedule-view-table-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.schedule-view-control-header h2,
|
||||
.schedule-view-results-header h2,
|
||||
.schedule-view-table-header h2 {
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.schedule-view-control-grid,
|
||||
.schedule-view-extra-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.schedule-view-control-grid {
|
||||
grid-template-columns: minmax(160px, 0.8fr) minmax(220px, 1.25fr) minmax(170px, 0.8fr) minmax(210px, 0.9fr);
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.schedule-view-extra-grid {
|
||||
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||
margin-top: 0.9rem;
|
||||
}
|
||||
|
||||
.schedule-view-period-control {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.schedule-view-period-buttons,
|
||||
.schedule-view-result-actions,
|
||||
.schedule-view-day-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.schedule-view-shell .btn-secondary {
|
||||
min-height: 42px;
|
||||
padding: 0.65rem 0.9rem;
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition), border-color var(--transition), color var(--transition), transform var(--transition);
|
||||
}
|
||||
|
||||
.schedule-view-shell .btn-secondary:hover:not(:disabled) {
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--accent);
|
||||
color: var(--accent-hover);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.schedule-view-shell .btn-secondary:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.schedule-view-extra-filters {
|
||||
margin-top: 0.9rem;
|
||||
border-top: 1px solid var(--bg-card-border);
|
||||
padding-top: 0.85rem;
|
||||
}
|
||||
|
||||
.schedule-view-extra-filters summary {
|
||||
width: fit-content;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.schedule-view-result-actions {
|
||||
min-height: 42px;
|
||||
justify-content: flex-end;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.88rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.schedule-view-result-tabs {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
margin-top: 1rem;
|
||||
padding-bottom: 0.1rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.schedule-view-result-tab {
|
||||
flex: 0 0 auto;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.18rem;
|
||||
min-width: 150px;
|
||||
max-width: 240px;
|
||||
padding: 0.65rem 0.85rem;
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-primary);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition), border-color var(--transition), transform var(--transition);
|
||||
}
|
||||
|
||||
.schedule-view-result-tab span,
|
||||
.schedule-view-result-tab small {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.schedule-view-result-tab small {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.schedule-view-result-tab:hover,
|
||||
.schedule-view-result-tab.active {
|
||||
background: color-mix(in srgb, var(--accent) 12%, var(--bg-input));
|
||||
border-color: var(--accent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.schedule-view-empty-result {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.schedule-view-table-card {
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.schedule-view-table-header {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.schedule-view-table-stage {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.schedule-view-week-panel {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.schedule-view-grid-container {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
@@ -600,11 +786,80 @@
|
||||
}
|
||||
|
||||
.schedule-view-state {
|
||||
min-height: 100%;
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.schedule-view-day-tabs {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.schedule-view-day-tab {
|
||||
min-width: 42px;
|
||||
min-height: 34px;
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
color: var(--text-secondary);
|
||||
font: inherit;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.schedule-view-day-tab.active {
|
||||
border-color: var(--accent);
|
||||
background: color-mix(in srgb, var(--accent) 15%, var(--bg-input));
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.schedule-view-mobile-day {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.schedule-view-mobile-day-heading,
|
||||
.schedule-view-mobile-slot {
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg-input);
|
||||
}
|
||||
|
||||
.schedule-view-mobile-day-heading {
|
||||
padding: 0.8rem 0.9rem;
|
||||
}
|
||||
|
||||
.schedule-view-mobile-day-heading strong,
|
||||
.schedule-view-mobile-day-heading span,
|
||||
.schedule-view-mobile-slot-time strong,
|
||||
.schedule-view-mobile-slot-time span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.schedule-view-mobile-day-heading span,
|
||||
.schedule-view-mobile-slot-time span {
|
||||
margin-top: 0.25rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.schedule-view-mobile-slot {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.schedule-view-mobile-slot-time {
|
||||
padding: 0.7rem 0.85rem;
|
||||
border-bottom: 1px solid var(--bg-card-border);
|
||||
}
|
||||
|
||||
.schedule-view-mobile-slot-content .room-week-unified {
|
||||
min-height: 124px;
|
||||
}
|
||||
|
||||
/* Настройка scrollbar для контейнера сетки */
|
||||
.workload-grid-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
@@ -621,6 +876,12 @@
|
||||
background: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.schedule-view-control-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.workload-target-control,
|
||||
.workload-display-control {
|
||||
@@ -638,4 +899,54 @@
|
||||
.room-grid-container {
|
||||
height: 520px;
|
||||
}
|
||||
|
||||
.schedule-view-shell {
|
||||
min-height: auto;
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
|
||||
.schedule-view-control-header,
|
||||
.schedule-view-results-header,
|
||||
.schedule-view-table-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.schedule-view-control-grid,
|
||||
.schedule-view-extra-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.schedule-view-period-buttons,
|
||||
.schedule-view-result-actions,
|
||||
.schedule-view-day-tabs {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.schedule-view-period-buttons .btn-secondary,
|
||||
.schedule-view-result-actions .btn-secondary {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.schedule-view-result-actions {
|
||||
justify-content: stretch;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.schedule-view-result-actions span {
|
||||
flex: 2;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.schedule-view-result-tab {
|
||||
min-width: 72%;
|
||||
}
|
||||
|
||||
.schedule-view-table-card {
|
||||
min-height: 520px;
|
||||
}
|
||||
|
||||
.schedule-view-day-tabs {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,79 +1,122 @@
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Фильтры расписания</h2>
|
||||
<button type="button" class="btn-primary" id="schedule-view-load">Показать</button>
|
||||
<section class="schedule-view-shell">
|
||||
<div class="card schedule-view-control-card">
|
||||
<div class="schedule-view-control-header">
|
||||
<div>
|
||||
<h2>Расписания</h2>
|
||||
<span class="muted" id="schedule-view-period-label">Период не выбран</span>
|
||||
</div>
|
||||
<button type="button" class="btn-primary" id="schedule-view-load">Показать</button>
|
||||
</div>
|
||||
|
||||
<div class="schedule-view-control-grid">
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-target">Что смотреть</label>
|
||||
<select id="schedule-view-target" data-native-select="true">
|
||||
<option value="groups">Группа</option>
|
||||
<option value="teachers">Преподаватель</option>
|
||||
<option value="classrooms">Аудитория</option>
|
||||
<option value="departments">Кафедра</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group schedule-view-entity-field" data-schedule-view-target-field="groups">
|
||||
<label for="schedule-view-group">Группа</label>
|
||||
<select id="schedule-view-group" data-native-select="true">
|
||||
<option value="">Все группы</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group schedule-view-entity-field" data-schedule-view-target-field="teachers" hidden>
|
||||
<label for="schedule-view-teacher">Преподаватель</label>
|
||||
<select id="schedule-view-teacher" data-native-select="true">
|
||||
<option value="">Все преподаватели</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group schedule-view-entity-field" data-schedule-view-target-field="classrooms" hidden>
|
||||
<label for="schedule-view-classroom">Аудитория</label>
|
||||
<select id="schedule-view-classroom" data-native-select="true">
|
||||
<option value="">Все аудитории</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group schedule-view-entity-field" data-schedule-view-target-field="departments" hidden>
|
||||
<label for="schedule-view-department">Кафедра</label>
|
||||
<select id="schedule-view-department" data-native-select="true">
|
||||
<option value="">Все кафедры</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-date">Дата в периоде</label>
|
||||
<input type="date" id="schedule-view-date">
|
||||
</div>
|
||||
|
||||
<div class="form-group schedule-view-period-control">
|
||||
<span class="form-label">Переход</span>
|
||||
<div class="schedule-view-period-buttons">
|
||||
<button type="button" class="btn-secondary" id="schedule-view-prev-period" aria-label="Предыдущий период">‹</button>
|
||||
<button type="button" class="btn-secondary" id="schedule-view-today">Сегодня</button>
|
||||
<button type="button" class="btn-secondary" id="schedule-view-next-period" aria-label="Следующий период">›</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details class="schedule-view-extra-filters">
|
||||
<summary>Дополнительные фильтры</summary>
|
||||
<div class="schedule-view-extra-grid">
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-subject">Дисциплина</label>
|
||||
<select id="schedule-view-subject" data-native-select="true">
|
||||
<option value="">Все дисциплины</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-lesson-type">Тип занятия</label>
|
||||
<select id="schedule-view-lesson-type" data-native-select="true">
|
||||
<option value="">Все типы</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-parity">Неделя</label>
|
||||
<select id="schedule-view-parity" data-native-select="true">
|
||||
<option value="">Любая</option>
|
||||
<option value="BOTH">Каждая</option>
|
||||
<option value="ODD">Нечётная</option>
|
||||
<option value="EVEN">Чётная</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div class="form-alert" id="schedule-view-alert" role="alert"></div>
|
||||
</div>
|
||||
|
||||
<div class="schedule-view-filter-grid">
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-date">Дата в периоде</label>
|
||||
<input type="date" id="schedule-view-date">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-group">Группа</label>
|
||||
<select id="schedule-view-group" data-native-select="true">
|
||||
<option value="">Все группы</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-teacher">Преподаватель</label>
|
||||
<select id="schedule-view-teacher" data-native-select="true">
|
||||
<option value="">Все преподаватели</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-classroom">Аудитория</label>
|
||||
<select id="schedule-view-classroom" data-native-select="true">
|
||||
<option value="">Все аудитории</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="schedule-view-department-field">
|
||||
<label for="schedule-view-department">Кафедра</label>
|
||||
<select id="schedule-view-department" data-native-select="true">
|
||||
<option value="">Все кафедры</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-subject">Дисциплина</label>
|
||||
<select id="schedule-view-subject" data-native-select="true">
|
||||
<option value="">Все дисциплины</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-lesson-type">Тип занятия</label>
|
||||
<select id="schedule-view-lesson-type" data-native-select="true">
|
||||
<option value="">Все типы</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-parity">Неделя</label>
|
||||
<select id="schedule-view-parity" data-native-select="true">
|
||||
<option value="">Любая</option>
|
||||
<option value="BOTH">Каждая</option>
|
||||
<option value="ODD">Нечётная</option>
|
||||
<option value="EVEN">Чётная</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="schedule-view-split">Таблицы по</label>
|
||||
<select id="schedule-view-split" data-native-select="true">
|
||||
<option value="auto">Автоматически</option>
|
||||
<option value="groups">Группам</option>
|
||||
<option value="teachers">Преподавателям</option>
|
||||
<option value="classrooms">Аудиториям</option>
|
||||
</select>
|
||||
<div class="card schedule-view-results-card">
|
||||
<div class="schedule-view-results-header">
|
||||
<div>
|
||||
<h2>Найденные расписания</h2>
|
||||
<span class="muted" id="schedule-view-count">0 занятий</span>
|
||||
</div>
|
||||
<div class="schedule-view-result-actions">
|
||||
<button type="button" class="btn-secondary" id="schedule-view-result-prev" aria-label="Предыдущее расписание">‹</button>
|
||||
<span id="schedule-view-active-label">Нет выбранного расписания</span>
|
||||
<button type="button" class="btn-secondary" id="schedule-view-result-next" aria-label="Следующее расписание">›</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-view-result-tabs" id="schedule-view-result-tabs"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-alert" id="schedule-view-alert" role="alert"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Таблицы расписания</h2>
|
||||
<span class="muted" id="schedule-view-count">0 занятий</span>
|
||||
<div class="card schedule-view-table-card">
|
||||
<div class="schedule-view-table-header">
|
||||
<div>
|
||||
<h2 id="schedule-view-table-title">Расписание</h2>
|
||||
<span class="muted" id="schedule-view-table-caption">Выберите параметры и нажмите «Показать»</span>
|
||||
</div>
|
||||
<div class="schedule-view-day-tabs" id="schedule-view-day-tabs" hidden></div>
|
||||
</div>
|
||||
<div class="schedule-view-table-stage" id="schedule-view-tables">
|
||||
<div class="loading-row schedule-view-state">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="schedule-view-table-stack" id="schedule-view-tables">
|
||||
<div class="loading-row schedule-view-state">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user