исправил просмотр расписаний
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
| **Администратор** | `ADMIN` | Полный доступ: пользователи, справочники, тенанты, роли, архивирование и восстановление. |
|
| **Администратор** | `ADMIN` | Полный доступ: пользователи, справочники, тенанты, роли, архивирование и восстановление. |
|
||||||
| **Учебный отдел** | `EDUCATION_OFFICE` | Редактирование расписания, точечные переносы/замены/отмены, временные слоты, аудитории, загруженность. |
|
| **Учебный отдел** | `EDUCATION_OFFICE` | Редактирование расписания, точечные переносы/замены/отмены, временные слоты, аудитории, загруженность. |
|
||||||
| **Кафедра** | `DEPARTMENT` | Дисциплины своей кафедры, загрузка дисциплин, привязки преподавателей, комментарии, расписание и нагрузка кафедры. |
|
| **Кафедра** | `DEPARTMENT` | Дисциплины своей кафедры, загрузка дисциплин, привязки преподавателей, комментарии, расписание и нагрузка кафедры. |
|
||||||
| **Просмотр расписаний** | `SCHEDULE_VIEWER` | Read-only просмотр расписаний по группам, преподавателям, аудиториям и кафедрам в совмещённых таблицах чётной/нечётной недели с разрезом по группам, преподавателям или аудиториям. |
|
| **Просмотр расписаний** | `SCHEDULE_VIEWER` | Read-only просмотр расписаний по группам, преподавателям, аудиториям и кафедрам в режиме одной активной совмещённой таблицы чётной/нечётной недели. |
|
||||||
| **Преподаватель** | `TEACHER` | Просмотр своего расписания. В перспективе — подача заявок на перенос. |
|
| **Преподаватель** | `TEACHER` | Просмотр своего расписания. В перспективе — подача заявок на перенос. |
|
||||||
| **Студент** | `STUDENT` | Только просмотр расписания (Read-only). |
|
| **Студент** | `STUDENT` | Только просмотр расписания (Read-only). |
|
||||||
|
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ frontend/
|
|||||||
| `classrooms` | Аудитории | `/api/classrooms` |
|
| `classrooms` | Аудитории | `/api/classrooms` |
|
||||||
| `subjects` | Дисциплины | `/api/subjects` |
|
| `subjects` | Дисциплины | `/api/subjects` |
|
||||||
| `department-workspace` | Кабинет кафедры: дисциплины, импорт, комментарии, преподаватели и нагрузка | `/api/department/*`, `/api/workload/teachers` |
|
| `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` |
|
| `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` |
|
| `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` |
|
| `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`.
|
- Вкладка `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`.
|
- Вкладка `auditorium-workload` стала общей вкладкой `Загруженность`: в поле «Что смотреть» выбираются аудитории, преподаватели или кафедры. Сводная матрица по выбранной дате использует одинаковую структуру: строки — выбранный тип сущности, столбцы — эффективные временные слоты дня из `/api/admin/time-slots/effective`, занятость собирается из динамического расписания `/api/schedule` по группам. Кафедральная матрица группирует занятия по кафедре преподавателя. Для аудиторий доступны фильтры корпуса, вместимости и оборудования. В поле «Отображение» можно выбрать конкретную аудиторию, преподавателя или кафедру; тогда сводная матрица заменяется одной таблицей по дням недели и времени для двухнедельного периода от выбранной даты. Таблица выбранной сущности растягивается до нижней части экрана. Ячейка делится вертикально только если верхняя и нижняя недели отличаются: нечётная неделя отображается сверху, чётная — снизу. Если состояние или занятие одинаковое, ячейка остаётся цельной. Чётность берётся из расписания, а для свободных дней рассчитывается по семестрам из `/api/admin/calendar/years`.
|
||||||
- Вкладка `profiles` выделена под профили обучения: администратор выбирает специальность, создаёт профиль, редактирует описание и удаляет неиспользуемые профили.
|
- Вкладка `profiles` выделена под профили обучения: администратор выбирает специальность, создаёт профиль, редактирует описание и удаляет неиспользуемые профили.
|
||||||
- Вкладка `schedule` не обращается к старым `lessons` API. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы, отдельные часы и недели начала для лекций, лабораторных и практик, а также набор базовых слотов. Группы выбираются через выпадающий мультиселект. Поле подгруппы появляется только при выборе лабораторной работы; для лекций и практик оно не отображается. Если в правиле выбрана одна группа, селект подгруппы содержит пункт `Вся группа`; если выбрано несколько групп, лабораторный слот показывает мультиселект подгрупп, чтобы выбрать разные подгруппы разных групп. Из календарной системы здесь используется список семестров для выбора периода действия правила.
|
- Вкладка `schedule` не обращается к старым `lessons` API. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы, отдельные часы и недели начала для лекций, лабораторных и практик, а также набор базовых слотов. Группы выбираются через выпадающий мультиселект. Поле подгруппы появляется только при выборе лабораторной работы; для лекций и практик оно не отображается. Если в правиле выбрана одна группа, селект подгруппы содержит пункт `Вся группа`; если выбрано несколько групп, лабораторный слот показывает мультиселект подгрупп, чтобы выбрать разные подгруппы разных групп. Из календарной системы здесь используется список семестров для выбора периода действия правила.
|
||||||
|
|||||||
@@ -569,19 +569,205 @@
|
|||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule-view-table-stack {
|
.schedule-view-shell {
|
||||||
display: flex;
|
min-height: calc(100vh - 8.5rem);
|
||||||
flex-direction: column;
|
display: grid;
|
||||||
|
grid-template-rows: auto auto minmax(420px, 1fr);
|
||||||
gap: 1rem;
|
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 {
|
.schedule-view-week-panel {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.schedule-view-grid-container {
|
.schedule-view-grid-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
max-height: none;
|
max-height: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,11 +786,80 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.schedule-view-state {
|
.schedule-view-state {
|
||||||
|
min-height: 100%;
|
||||||
border: 1px solid var(--bg-card-border);
|
border: 1px solid var(--bg-card-border);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--bg-input);
|
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 для контейнера сетки */
|
/* Настройка scrollbar для контейнера сетки */
|
||||||
.workload-grid-container::-webkit-scrollbar {
|
.workload-grid-container::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
@@ -621,6 +876,12 @@
|
|||||||
background: rgba(0, 0, 0, 0.15);
|
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) {
|
@media (max-width: 768px) {
|
||||||
.workload-target-control,
|
.workload-target-control,
|
||||||
.workload-display-control {
|
.workload-display-control {
|
||||||
@@ -638,4 +899,54 @@
|
|||||||
.room-grid-container {
|
.room-grid-container {
|
||||||
height: 520px;
|
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';
|
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||||
|
|
||||||
const ROLE_DEPARTMENT = 'DEPARTMENT';
|
const ROLE_DEPARTMENT = 'DEPARTMENT';
|
||||||
|
const TARGET_GROUPS = 'groups';
|
||||||
|
const TARGET_TEACHERS = 'teachers';
|
||||||
|
const TARGET_CLASSROOMS = 'classrooms';
|
||||||
|
const TARGET_DEPARTMENTS = 'departments';
|
||||||
const PARITY_LABELS = {
|
const PARITY_LABELS = {
|
||||||
BOTH: 'каждая',
|
BOTH: 'каждая',
|
||||||
ODD: 'нечётная',
|
ODD: 'нечётная',
|
||||||
@@ -30,104 +34,418 @@ const CELL_PARITY_LAYOUT = [
|
|||||||
{ parity: 'ODD', label: 'Нечётная' },
|
{ parity: 'ODD', label: 'Нечётная' },
|
||||||
{ parity: 'EVEN', label: 'Чётная' }
|
{ parity: 'EVEN', label: 'Чётная' }
|
||||||
];
|
];
|
||||||
const SPLIT_AUTO = 'auto';
|
const MOBILE_QUERY = '(max-width: 760px)';
|
||||||
const SPLIT_GROUPS = 'groups';
|
|
||||||
const SPLIT_TEACHERS = 'teachers';
|
|
||||||
const SPLIT_CLASSROOMS = 'classrooms';
|
|
||||||
|
|
||||||
export async function initScheduleView() {
|
export async function initScheduleView() {
|
||||||
const role = localStorage.getItem('role');
|
const role = localStorage.getItem('role');
|
||||||
const userDepartmentId = localStorage.getItem('departmentId');
|
const userDepartmentId = normalizeId(localStorage.getItem('departmentId'));
|
||||||
const dateInput = document.getElementById('schedule-view-date');
|
const dateInput = document.getElementById('schedule-view-date');
|
||||||
|
const targetSelect = document.getElementById('schedule-view-target');
|
||||||
const loadButton = document.getElementById('schedule-view-load');
|
const loadButton = document.getElementById('schedule-view-load');
|
||||||
const departmentField = document.getElementById('schedule-view-department-field');
|
const previousPeriodButton = document.getElementById('schedule-view-prev-period');
|
||||||
const departmentSelect = document.getElementById('schedule-view-department');
|
const todayButton = document.getElementById('schedule-view-today');
|
||||||
const tables = document.getElementById('schedule-view-tables');
|
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 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);
|
setDefaultDate(dateInput);
|
||||||
|
configureDepartmentTarget();
|
||||||
|
updatePeriodLabel();
|
||||||
|
updateEntityFieldVisibility();
|
||||||
|
renderResultNavigation();
|
||||||
|
|
||||||
|
targetSelect?.addEventListener('change', () => {
|
||||||
|
clearInactiveEntityValues();
|
||||||
|
updateEntityFieldVisibility();
|
||||||
|
resetResults('Нажмите «Показать», чтобы обновить расписание');
|
||||||
|
});
|
||||||
loadButton?.addEventListener('click', loadData);
|
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 {
|
try {
|
||||||
await loadDictionaries(role, userDepartmentId);
|
state.dictionaries = await loadDictionaries(role, userDepartmentId);
|
||||||
if (role === ROLE_DEPARTMENT) {
|
configureDepartmentAccess();
|
||||||
departmentField.hidden = true;
|
updateEntityFieldVisibility();
|
||||||
departmentSelect.value = userDepartmentId || '';
|
|
||||||
}
|
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (error) {
|
} 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() {
|
async function loadData() {
|
||||||
hideAlert('schedule-view-alert');
|
hideAlert('schedule-view-alert');
|
||||||
setTableState('Загрузка...');
|
|
||||||
countLabel.textContent = 'Загрузка...';
|
|
||||||
|
|
||||||
if (!dateInput.value) {
|
if (!dateInput.value) {
|
||||||
const message = 'Выберите дату в периоде';
|
const message = 'Выберите дату в периоде';
|
||||||
showAlert('schedule-view-alert', message, 'error');
|
showAlert('schedule-view-alert', message, 'error');
|
||||||
setTableState(message);
|
resetResults(message);
|
||||||
countLabel.textContent = '0 занятий';
|
|
||||||
return;
|
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 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({
|
const params = new URLSearchParams({
|
||||||
startDate: range.startDate,
|
startDate: range.startDate,
|
||||||
endDate: range.endDate
|
endDate: range.endDate
|
||||||
});
|
});
|
||||||
addOptional(params, 'groupId', valueOf('schedule-view-group'));
|
applyTargetFilter(params, target);
|
||||||
addOptional(params, 'teacherId', valueOf('schedule-view-teacher'));
|
|
||||||
addOptional(params, 'classroomId', valueOf('schedule-view-classroom'));
|
|
||||||
addOptional(params, 'departmentId', role === ROLE_DEPARTMENT ? userDepartmentId : departmentSelect.value);
|
|
||||||
addOptional(params, 'subjectId', valueOf('schedule-view-subject'));
|
addOptional(params, 'subjectId', valueOf('schedule-view-subject'));
|
||||||
addOptional(params, 'lessonTypeId', valueOf('schedule-view-lesson-type'));
|
addOptional(params, 'lessonTypeId', valueOf('schedule-view-lesson-type'));
|
||||||
addOptional(params, 'parity', valueOf('schedule-view-parity'));
|
addOptional(params, 'parity', valueOf('schedule-view-parity'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const lessons = await api.get(`/api/schedule/search?${params}`);
|
const lessons = await api.get(`/api/schedule/search?${params}`);
|
||||||
renderLessons(lessons, range);
|
renderLessons(lessons, range, target, previousKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error.message || 'Ошибка загрузки расписания';
|
const message = error.message || 'Ошибка загрузки расписания';
|
||||||
showAlert('schedule-view-alert', message, 'error');
|
showAlert('schedule-view-alert', message, 'error');
|
||||||
setTableState(message);
|
resetResults(message);
|
||||||
countLabel.textContent = '0 занятий';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderLessons(lessons, range) {
|
function applyTargetFilter(params, target) {
|
||||||
countLabel.textContent = lessonCountLabel(lessons.length);
|
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) {
|
if (!lessons.length) {
|
||||||
|
state.sections = [];
|
||||||
|
state.activeSectionIndex = 0;
|
||||||
|
renderResultNavigation();
|
||||||
|
tableTitle.textContent = 'Расписание';
|
||||||
|
tableCaption.textContent = periodCaption(range);
|
||||||
setTableState('Занятия не найдены');
|
setTableState('Занятия не найдены');
|
||||||
|
countLabel.textContent = '0 занятий';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const sections = buildScheduleSections(lessons);
|
|
||||||
|
const sections = buildScheduleSections(lessons, splitTypeForTarget(target), buildSectionContext(target));
|
||||||
if (!sections.length) {
|
if (!sections.length) {
|
||||||
setTableState('Нет данных для выбранного разреза таблиц');
|
state.sections = [];
|
||||||
|
state.activeSectionIndex = 0;
|
||||||
|
renderResultNavigation();
|
||||||
|
tableTitle.textContent = 'Расписание';
|
||||||
|
tableCaption.textContent = periodCaption(range);
|
||||||
|
setTableState('Нет данных для выбранного режима просмотра');
|
||||||
|
countLabel.textContent = lessonCountLabel(lessons.length);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tableCount = sections.reduce((count, section) =>
|
|
||||||
count + buildCombinedWeekBlocks(range.startDate, range.endDate, section.lessons).length, 0
|
state.sections = sections;
|
||||||
);
|
state.activeSectionIndex = Math.max(0, sections.findIndex(section => section.key === previousKey));
|
||||||
if (!tableCount) {
|
if (state.activeSectionIndex < 0) {
|
||||||
setTableState('Выберите корректный период');
|
state.activeSectionIndex = 0;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
countLabel.textContent = `${lessonCountLabel(lessons.length)}, ${tableCountLabel(tableCount)}`;
|
state.mobileDay = preferredMobileDay(range);
|
||||||
tables.innerHTML = sections
|
renderResultNavigation();
|
||||||
.map(section => renderScheduleSection(section, range))
|
renderActiveSection();
|
||||||
.join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 slots = collectSlots(section.lessons);
|
||||||
const lessonsByDateSlot = groupLessonsByDateSlot(section.lessons);
|
const lessonsByDateSlot = groupLessonsByDateSlot(section.lessons);
|
||||||
const blocks = buildCombinedWeekBlocks(range.startDate, range.endDate, 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))
|
.map(block => renderCombinedWeekTable(block, slots, lessonsByDateSlot, section))
|
||||||
.join('');
|
.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) {
|
function renderCombinedWeekTable(block, slots, lessonsByDateSlot, section) {
|
||||||
const dayHeaders = DAY_ORDER.map(day => `
|
const dayHeaders = DAY_ORDER.map(day => `
|
||||||
<th class="room-day-cell">
|
<th class="room-day-cell">
|
||||||
@@ -152,8 +470,8 @@ export async function initScheduleView() {
|
|||||||
return `
|
return `
|
||||||
<section class="schedule-view-week-panel">
|
<section class="schedule-view-week-panel">
|
||||||
<div class="room-week-heading">
|
<div class="room-week-heading">
|
||||||
<h3>${escapeHtml(section.title)} · ${escapeHtml(block.label)}</h3>
|
<h3>${escapeHtml(section.title)}</h3>
|
||||||
<span>${escapeHtml(`${lessonCountLabel(section.lessons.length)}; ${combinedWeekCaption(block)}`)}</span>
|
<span>${escapeHtml(combinedWeekCaption(block))}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="workload-grid-container schedule-view-grid-container">
|
<div class="workload-grid-container schedule-view-grid-container">
|
||||||
<table class="workload-table room-week-table schedule-view-matrix-table">
|
<table class="workload-table room-week-table schedule-view-matrix-table">
|
||||||
@@ -175,20 +493,26 @@ export async function initScheduleView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderCombinedSlotCell(datesByParity, slot, lessonsByDateSlot) {
|
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,
|
parity,
|
||||||
label,
|
label,
|
||||||
state: scheduleSlotState(datesByParity[parity], slot, lessonsByDateSlot)
|
state: scheduleSlotState(datesByParity[parity], slot, lessonsByDateSlot)
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSlotContent(states) {
|
||||||
if (slotStatesEqual(states[0].state, states[1].state)) {
|
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);
|
||||||
}
|
}
|
||||||
|
return states.map(({ parity, label, state }) =>
|
||||||
const halves = states.map(({ parity, label, state }) =>
|
|
||||||
renderSlotHalf(state, parity, label)
|
renderSlotHalf(state, parity, label)
|
||||||
).join('');
|
).join('');
|
||||||
return `<td class="room-combined-cell">${halves}</td>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleSlotState(date, slot, lessonsByDateSlot) {
|
function scheduleSlotState(date, slot, lessonsByDateSlot) {
|
||||||
@@ -298,6 +622,38 @@ export async function initScheduleView() {
|
|||||||
if (!tables) return;
|
if (!tables) return;
|
||||||
tables.innerHTML = `<div class="loading-row schedule-view-state">${escapeHtml(message)}</div>`;
|
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) {
|
async function loadDictionaries(role, userDepartmentId) {
|
||||||
@@ -310,16 +666,23 @@ async function loadDictionaries(role, userDepartmentId) {
|
|||||||
api.get('/api/lesson-types')
|
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-teacher', teachers, item => item.id, item => item.fullName || item.username, 'Все преподаватели');
|
||||||
fillSelect('schedule-view-classroom', classrooms, item => item.id, item => roomLabel(item), 'Все аудитории');
|
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-department', departments, item => item.id, item => item.departmentName, 'Все кафедры');
|
||||||
fillSelect('schedule-view-subject', subjects, item => item.id, item => item.name, 'Все дисциплины');
|
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-lesson-type', lessonTypes, item => item.id, item => item.name, 'Все типы');
|
||||||
|
|
||||||
if (role === ROLE_DEPARTMENT && userDepartmentId) {
|
return {
|
||||||
document.getElementById('schedule-view-department').value = userDepartmentId;
|
groups,
|
||||||
}
|
teachers,
|
||||||
|
classrooms,
|
||||||
|
departments
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDefaultDate(dateInput) {
|
function setDefaultDate(dateInput) {
|
||||||
@@ -342,6 +705,11 @@ function valueOf(id) {
|
|||||||
return document.getElementById(id)?.value || '';
|
return document.getElementById(id)?.value || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeId(value) {
|
||||||
|
if (!value || value === 'null' || value === 'undefined') return '';
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
function roomLabel(room) {
|
function roomLabel(room) {
|
||||||
return [room.name, room.building, room.floor ? `${room.floor} этаж` : null].filter(Boolean).join(' · ');
|
return [room.name, room.building, room.floor ? `${room.floor} этаж` : null].filter(Boolean).join(' · ');
|
||||||
}
|
}
|
||||||
@@ -385,12 +753,11 @@ function groupLessonsByDateSlot(lessons) {
|
|||||||
return grouped;
|
return grouped;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildScheduleSections(lessons) {
|
function buildScheduleSections(lessons, splitType, context) {
|
||||||
const splitType = resolveSplitType();
|
|
||||||
const sections = new Map();
|
const sections = new Map();
|
||||||
|
|
||||||
lessons.forEach(lesson => {
|
lessons.forEach(lesson => {
|
||||||
splitEntitiesForLesson(lesson, splitType).forEach(entity => {
|
splitEntitiesForLesson(lesson, splitType, context).forEach(entity => {
|
||||||
if (!sections.has(entity.key)) {
|
if (!sections.has(entity.key)) {
|
||||||
sections.set(entity.key, {
|
sections.set(entity.key, {
|
||||||
key: entity.key,
|
key: entity.key,
|
||||||
@@ -410,45 +777,42 @@ function buildScheduleSections(lessons) {
|
|||||||
.sort((a, b) => naturalCompare(a.title, b.title));
|
.sort((a, b) => naturalCompare(a.title, b.title));
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveSplitType() {
|
function splitEntitiesForLesson(lesson, splitType, context) {
|
||||||
const requested = valueOf('schedule-view-split');
|
if (splitType === TARGET_TEACHERS) {
|
||||||
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) {
|
|
||||||
return [singleEntity('teacher', lesson.teacherId, lesson.teacherName, 'Преподаватель не указан')];
|
return [singleEntity('teacher', lesson.teacherId, lesson.teacherName, 'Преподаватель не указан')];
|
||||||
}
|
}
|
||||||
if (splitType === SPLIT_CLASSROOMS) {
|
if (splitType === TARGET_CLASSROOMS) {
|
||||||
return [singleEntity('classroom', lesson.classroomId, lesson.classroomName, 'Аудитория не указана')];
|
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) {
|
function singleEntity(prefix, id, name, fallback) {
|
||||||
const title = name || fallback;
|
const title = name || fallback;
|
||||||
return {
|
return {
|
||||||
|
id,
|
||||||
key: `${prefix}:${id ?? title}`,
|
key: `${prefix}:${id ?? title}`,
|
||||||
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 safeIds = Array.isArray(ids) ? ids : [];
|
||||||
const safeNames = Array.isArray(names) ? names : [];
|
const safeNames = Array.isArray(names) ? names : [];
|
||||||
if (!safeIds.length && !safeNames.length) {
|
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 maxLength = Math.max(safeIds.length, safeNames.length);
|
||||||
const entities = [];
|
const entities = [];
|
||||||
for (let index = 0; index < maxLength; index += 1) {
|
for (let index = 0; index < maxLength; index += 1) {
|
||||||
const id = safeIds[index] ?? null;
|
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));
|
entities.push(singleEntity(prefix, id, name, fallback));
|
||||||
}
|
}
|
||||||
return entities;
|
return entities;
|
||||||
@@ -540,6 +904,19 @@ function combinedWeekCaption(block) {
|
|||||||
.join('; ');
|
.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) {
|
function lessonSlotKey(lesson) {
|
||||||
const value = lesson.timeSlotOrder ?? lesson.timeSlotId ?? `${lesson.startTime || ''}-${lesson.endTime || ''}`;
|
const value = lesson.timeSlotOrder ?? lesson.timeSlotId ?? `${lesson.startTime || ''}-${lesson.endTime || ''}`;
|
||||||
return value ? String(value) : 'unknown';
|
return value ? String(value) : 'unknown';
|
||||||
@@ -625,12 +1002,12 @@ function lessonCountLabel(count) {
|
|||||||
return `${count} занятий`;
|
return `${count} занятий`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function tableCountLabel(count) {
|
function sectionCountLabel(count) {
|
||||||
const tail = count % 100;
|
const tail = count % 100;
|
||||||
if (tail >= 11 && tail <= 14) return `${count} таблиц`;
|
if (tail >= 11 && tail <= 14) return `${count} расписаний`;
|
||||||
if (count % 10 === 1) return `${count} таблица`;
|
if (count % 10 === 1) return `${count} расписание`;
|
||||||
if ([2, 3, 4].includes(count % 10)) return `${count} таблицы`;
|
if ([2, 3, 4].includes(count % 10)) return `${count} расписания`;
|
||||||
return `${count} таблиц`;
|
return `${count} расписаний`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareSlots(a, b) {
|
function compareSlots(a, b) {
|
||||||
|
|||||||
@@ -1,79 +1,122 @@
|
|||||||
<div class="card">
|
<section class="schedule-view-shell">
|
||||||
<div class="card-header-row">
|
<div class="card schedule-view-control-card">
|
||||||
<h2>Фильтры расписания</h2>
|
<div class="schedule-view-control-header">
|
||||||
<button type="button" class="btn-primary" id="schedule-view-load">Показать</button>
|
<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>
|
||||||
|
|
||||||
<div class="schedule-view-filter-grid">
|
<div class="card schedule-view-results-card">
|
||||||
<div class="form-group">
|
<div class="schedule-view-results-header">
|
||||||
<label for="schedule-view-date">Дата в периоде</label>
|
<div>
|
||||||
<input type="date" id="schedule-view-date">
|
<h2>Найденные расписания</h2>
|
||||||
</div>
|
<span class="muted" id="schedule-view-count">0 занятий</span>
|
||||||
<div class="form-group">
|
</div>
|
||||||
<label for="schedule-view-group">Группа</label>
|
<div class="schedule-view-result-actions">
|
||||||
<select id="schedule-view-group" data-native-select="true">
|
<button type="button" class="btn-secondary" id="schedule-view-result-prev" aria-label="Предыдущее расписание">‹</button>
|
||||||
<option value="">Все группы</option>
|
<span id="schedule-view-active-label">Нет выбранного расписания</span>
|
||||||
</select>
|
<button type="button" class="btn-secondary" id="schedule-view-result-next" aria-label="Следующее расписание">›</button>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
<div class="schedule-view-result-tabs" id="schedule-view-result-tabs"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-alert" id="schedule-view-alert" role="alert"></div>
|
<div class="card schedule-view-table-card">
|
||||||
</div>
|
<div class="schedule-view-table-header">
|
||||||
|
<div>
|
||||||
<div class="card">
|
<h2 id="schedule-view-table-title">Расписание</h2>
|
||||||
<div class="card-header-row">
|
<span class="muted" id="schedule-view-table-caption">Выберите параметры и нажмите «Показать»</span>
|
||||||
<h2>Таблицы расписания</h2>
|
</div>
|
||||||
<span class="muted" id="schedule-view-count">0 занятий</span>
|
<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>
|
||||||
<div class="schedule-view-table-stack" id="schedule-view-tables">
|
</section>
|
||||||
<div class="loading-row schedule-view-state">Загрузка...</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user