1 Задача

This commit is contained in:
dipatrik10
2026-08-06 01:10:33 +03:00
parent 28268a38c0
commit 5276729a69
33 changed files with 3569 additions and 19 deletions

View File

@@ -841,6 +841,77 @@ API возвращает `409 Conflict`; соседние интервалы и
}
```
### Отсутствия преподавателей и мастер замены
| Метод | URL | Назначение |
|-------|-----|------------|
| `GET` | `/api/teacher-absences?status=` | Доступный текущей роли реестр отсутствий |
| `POST` | `/api/teacher-absences` | Зарегистрировать отсутствие или отправить преподавательскую заявку |
| `POST` | `/api/teacher-absences/{id}/review` | Подтвердить или отклонить заявку преподавателя |
| `DELETE` | `/api/teacher-absences/{id}` | Отменить ожидающее или ещё не обработанное отсутствие |
| `GET` | `/api/teacher-absences/{id}/wizard` | Затронутые занятия, проверенные кандидаты и журнал решений |
| `POST` | `/api/teacher-absences/{id}/resolve` | Атомарно применить выбранные решения |
Доступ имеют `ADMIN`, `EDUCATION_OFFICE`, `DEPARTMENT` и `TEACHER`. Преподаватель видит и
создаёт только собственные заявки; они получают статус `PENDING`. Кафедра работает только с
преподавателями, назначенными ей на период отсутствия. Ответственный сотрудник создаёт
сразу согласованную запись `APPROVED`, а кафедра, учебный отдел или администратор могут
перевести преподавательскую заявку в `APPROVED` либо `REJECTED`.
```json
{
"teacherId": 15,
"startDate": "2026-09-10",
"endDate": "2026-09-12",
"reason": "Командировка"
}
```
Для роли `TEACHER` поле `teacherId` игнорируется в пользу ID текущего пользователя. Период
включительный, не длиннее 120 дней и не может пересекаться с другой активной записью того
же преподавателя.
`GET /{id}/wizard` ищет фактические занятия через `ScheduleQueryService`. Кандидат на замену
должен иметь связь с дисциплиной; если для этой связи настроены `teacher_lesson_types`, тип
занятия также обязан совпасть. Из списка исключаются архивные, занятые и отсутствующие
преподаватели. Варианты переноса выбираются среди ближайших учебных дат вне периода
отсутствия, аудитории — среди активных и свободных. Каждый кандидат заранее проходит тот же
валидатор `ScheduleOverrideService`, что и обычная разовая правка.
Пример группового подтверждения:
```json
{
"decisions": [
{
"baseRuleSlotId": 31,
"lessonDate": "2026-09-10",
"resolution": "REPLACE_TEACHER",
"newTeacherId": 28,
"comment": "Согласовано кафедрой"
},
{
"baseRuleSlotId": 44,
"lessonDate": "2026-09-11",
"resolution": "MOVE_TIME",
"targetLessonDate": "2026-09-14",
"newTimeSlotId": 3
},
{
"baseRuleSlotId": 52,
"lessonDate": "2026-09-12",
"resolution": "REJECT"
}
]
}
```
Допустимые решения: `REPLACE_TEACHER`, `MOVE_TIME`, `CHANGE_CLASSROOM`, `CANCEL`, `REJECT`.
Первые четыре создают обычный `schedule_override`; `REJECT` фиксирует осознанное отклонение
без изменения расписания. Весь пакет выполняется в одной транзакции: конфликт любого
выбранного действия возвращает `409` и откатывает остальные. Незаполненные занятия не
меняются. Когда обработаны все оставшиеся занятия, отсутствие получает статус `RESOLVED`.
## Загруженность
| Метод | URL | Назначение |

View File

@@ -312,6 +312,35 @@ constraint. При чтении API разворачивает периоды о
для backend-pod, поэтому два конкурентных переноса с разных исходных дат на одну целевую
дату и одинаковые ресурсы проверяются последовательно.
### Отсутствия преподавателей и мастер замены
Отсутствие хранится отдельно от базовых правил расписания и проходит жизненный цикл
`PENDING → APPROVED → RESOLVED`; отклонённые и отменённые записи получают соответственно
`REJECTED` и `CANCELLED`. Преподаватель создаёт заявку только для себя со статусом
`PENDING`. Администратор, учебный отдел и кафедра могут регистрировать согласованное
отсутствие сразу. Кафедра видит и подтверждает только преподавателей, назначенных ей на
выбранный период через `teacher_department_assignments`.
Для согласованного отсутствия `ScheduleQueryService` строит фактические занятия
преподавателя в пределах периода. Мастер предлагает не более пяти проверенных вариантов
каждого типа:
- преподавателей со связью по дисциплине и допустимому типу занятия;
- ближайшие учебные даты вне периода отсутствия и эффективные временные слоты;
- активные свободные аудитории;
- отмену занятия или явное отклонение предложений.
Если у связи преподавателя с дисциплиной нет настроенных строк `teacher_lesson_types`, она
считается разрешающей все типы; при наличии настроек требуется точное совпадение типа.
Кандидаты-замены исключаются, если архивированы, заняты или сами отсутствуют в дату пары.
Каждый вариант до показа проходит read-only проверку `ScheduleOverrideService`, а выбранный
пакет повторно валидируется и сохраняется в одной транзакции. Поэтому время, аудитория,
группы и подгруппы проверяются тем же механизмом, что и ручные точечные изменения.
Применённые действия создают обычные `schedule_overrides`; базовое правило семестра не
изменяется. Таблица `teacher_absence_decisions` фиксирует как применённые, так и отклонённые
решения. Незаполненные строки мастер не меняет, и к ним можно вернуться позже.
---
## Привязка преподаватель ↔ дисциплина
@@ -335,6 +364,9 @@ constraint. При чтении API разворачивает периоды о
- **Вместимость:** Суммарная численность всех групп в слоте не должна превышать вместимость аудитории
### Управление инцидентами
- Регистрация отсутствия преподавателя (болезнь, командировка) с указанием периода
- Автоматическая подсветка конфликтующих пар (Red Zone)
- Resolution Wizard: предложение замены преподавателя или переноса занятия
Регистрация отсутствий и Resolution Wizard реализованы. Дальнейшее развитие этого контура:
- автоматическая рассылка уведомлений группам и преподавателям;
- пакетная обработка нескольких одновременных отсутствий;
- ранжирование вариантов по окнам и дополнительной нагрузке.

View File

@@ -334,6 +334,34 @@ erDiagram
TEXT comment
}
teacher_absences {
BIGSERIAL id PK
BIGINT teacher_id FK
DATE start_date
DATE end_date
TEXT reason
VARCHAR status
BIGINT requested_by FK
BIGINT reviewed_by FK
TEXT review_comment
TIMESTAMPTZ reviewed_at
TIMESTAMPTZ created_at
TIMESTAMPTZ updated_at
}
teacher_absence_decisions {
BIGSERIAL id PK
BIGINT absence_id FK
BIGINT base_rule_slot_id FK
DATE lesson_date
VARCHAR resolution
VARCHAR decision_status
BIGINT schedule_override_id FK
TEXT comment
BIGINT decided_by FK
TIMESTAMPTZ decided_at
}
schedule_rule_slot_subgroups {
BIGINT schedule_rule_slot_id FK,PK
BIGINT subgroup_id FK,PK
@@ -385,6 +413,11 @@ erDiagram
time_slots ||--o{ schedule_overrides : "new_time_slot_id"
classrooms ||--o{ schedule_overrides : "new_classroom_id"
users ||--o{ schedule_overrides : "new_teacher_id"
users ||--o{ teacher_absences : "teacher_id/requested_by/reviewed_by"
teacher_absences ||--o{ teacher_absence_decisions : "absence_id"
schedule_rule_slots ||--o{ teacher_absence_decisions : "base_rule_slot_id"
schedule_overrides ||--o{ teacher_absence_decisions : "schedule_override_id"
users ||--o{ teacher_absence_decisions : "decided_by"
time_slot_scopes ||--o{ time_slots : "time_slot_scope_id"
time_slot_scopes ||--o{ time_slot_date_assignments : "time_slot_scope_id"
time_slots ||--o{ schedule_rule_slots : "time_slot_id"
@@ -905,6 +938,44 @@ V1 добавляет `uq_schedule_rule_slots_exact_payload`: в одном пр
lifecycle ресурсов, эффективная сетка целевого дня, реальность изменения и ресурсные
конфликты проверяются транзакционным сервисом, а не SQL CHECK.
#### `teacher_absences` — Отсутствия преподавателей
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID отсутствия |
| `teacher_id` | BIGINT FK → users | Отсутствующий преподаватель |
| `start_date`, `end_date` | DATE | Включительные границы периода |
| `reason` | TEXT | Причина длиной 1–2000 символов |
| `status` | VARCHAR(20) | `PENDING`, `APPROVED`, `REJECTED`, `RESOLVED`, `CANCELLED` |
| `requested_by` | BIGINT FK → users | Автор регистрации или заявки |
| `reviewed_by` | BIGINT FK → users | Подтвердивший или отклонивший сотрудник |
| `review_comment` | TEXT | Комментарий согласования |
| `reviewed_at` | TIMESTAMPTZ | Время согласования |
| `created_at`, `updated_at` | TIMESTAMPTZ | Аудит жизненного цикла |
CHECK запрещает обратный период, пустую или слишком длинную причину и неизвестный статус.
Индекс `(teacher_id, start_date, end_date)` ускоряет поиск пересекающихся отсутствий и
проверку кандидатов мастера.
#### `teacher_absence_decisions` — Журнал мастера замены
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID записи журнала |
| `absence_id` | BIGINT FK → teacher_absences | Инцидент отсутствия |
| `base_rule_slot_id` | BIGINT FK → schedule_rule_slots | Базовый слот занятия |
| `lesson_date` | DATE | Исходная дата занятия |
| `resolution` | VARCHAR(30) | Замена, перенос, аудитория, отмена или отклонение |
| `decision_status` | VARCHAR(20) | `APPLIED` либо `REJECTED` |
| `schedule_override_id` | BIGINT FK → schedule_overrides | Созданная разовая правка, если есть |
| `comment` | TEXT | Комментарий ответственного сотрудника |
| `decided_by` | BIGINT FK → users | Автор решения |
| `decided_at` | TIMESTAMPTZ | Время решения |
Уникальность `(absence_id, base_rule_slot_id, lesson_date)` не позволяет повторно обработать
одно занятие одного инцидента. При удалении override ссылка обнуляется, но аудиторская запись
сохраняется.
---
## Flyway миграции
@@ -924,12 +995,15 @@ lifecycle ресурсов, эффективная сетка целевого
| Файл | Описание |
|------|----------|
| `V1__init.sql` | Полная baseline-схема: справочники, роли, refresh-сессии JWT, PostgreSQL rate limit и аудит входа, lifecycle-поля, история кафедр, календарные графики с интервальным хранением активностей и нумерацией недель `понедельник–воскресенье`, динамическое расписание, точечные изменения с переносом даты, seed, CHECK/UNIQUE/GiST-ограничения, конкурентно безопасные триггеры и комментарии |
| `V2__teacher_absences_and_replacement_wizard.sql` | Реестр отсутствий преподавателей, статусы согласования и журнал применённых/отклонённых решений со ссылками на обычные `schedule_overrides` |
### Этап разработки
По прямому решению владельца проекта разработческие миграции V2–V7 объединены в baseline
`V1`. Интервальное хранение активностей и правильная нумерация недель календарного графика
также входят непосредственно в V1.
Исторические разработческие миграции V2–V7 по прямому решению владельца проекта были
объединены в baseline `V1`. После фиксации baseline нумерация начата заново: текущая `V2`
добавляет отсутствия и мастер замены, не изменяя контрольную сумму `V1`.
Интервальное хранение активностей и правильная нумерация недель календарного графика входят
непосредственно в V1.
Перед применением этой редакции требуется полностью пустая tenant-схема.
### Полный сброс БД (локально)

View File

@@ -53,6 +53,10 @@
## 1. Отсутствия преподавателей и мастер замены
**Статус: реализовано в MVP.** Реестр, согласование преподавательских заявок, поиск
затронутых занятий, проверенные варианты, групповое применение `schedule_overrides` и журнал
решений доступны в системе.
### Проблема и пользователи
Болезнь, командировка или другое временное отсутствие преподавателя затрагивает сразу
@@ -60,9 +64,8 @@
доступность ресурсов и создать каждое точечное изменение. Функция нужна учебному отделу,
кафедрам и преподавателям.
Регистрация отсутствий и мастер разрешения инцидентов уже обозначены как планируемые
бизнес-правила в [BUSINESS_LOGIC.md](BUSINESS_LOGIC.md), но отдельной модели и рабочего
сценария пока нет. Существующая Red Zone уже выявляет конфликты сформированного расписания;
Регистрация отсутствий и мастер разрешения инцидентов реализуют правила из
[BUSINESS_LOGIC.md](BUSINESS_LOGIC.md). Существующая Red Zone уже выявляет конфликты сформированного расписания;
новизна этого направления заключается в реестре причин, массовом поиске затронутых занятий
и управляемом подборе решений.
@@ -504,4 +507,4 @@ Red Zone уже выявляет накладки и превышение вме
**Ожидаемый результат:** новый самостоятельный контур учебного процесса на основе уже
существующих справочников и календаря. **Сложность:** L. **Демонстрационный эффект:**
максимальный.
максимальный.

View File

@@ -34,6 +34,7 @@ frontend/
│ ├── dashboard-conflicts.test.mjs # Регрессии дат и состояний проверки конфликтов
│ ├── schedule-overrides.test.mjs # Действия, роли, недельный выбор и подбор времени разовой правки
│ ├── schedule-view-semesters.test.mjs # Выбор семестра и расчёт двухнедельного диапазона просмотра
│ ├── teacher-absences.test.mjs # Payload мастера замены и доступность вкладки по ролям
│ └── security-policy.test.mjs # Web Storage, XSS, пароли, язык, CSP и Dockerfile
├── index.html # 🔐 Страница авторизации (общая)
├── script.js # Логика авторизации
@@ -49,7 +50,8 @@ frontend/
│ │ ├── components.css # Кнопки, таблицы, карточки, формы
│ │ ├── modals.css # Модальные окна
│ │ ├── auditorium-workload.css # Таблицы расписаний и загруженности
│ │ └── departments-data.css # Стили создания кафедры/специальности
│ │ ├── departments-data.css # Стили создания кафедры/специальности
│ │ └── teacher-absences.css # Реестр инцидентов и полноэкранный мастер замены
│ ├── js/
│ │ ├── main.js # Инициализация, маршрутизация, навигация
│ │ ├── role-capabilities.js # Единая матрица вкладок admin/settings по ролям
@@ -68,6 +70,7 @@ frontend/
│ │ ├── department-workspace.js # Кабинет кафедры в общей панели
│ │ ├── schedule-view.js # Просмотр расписаний и запуск разовой правки из карточки
│ │ ├── schedule-override-panel.js # Боковая панель и реестр разовых изменений
│ │ ├── teacher-absences.js # Реестр, согласование и групповой мастер замены
│ │ ├── schedule.js # Конструктор правил расписания
│ │ ├── academic-calendar-grid.js # Расчёт ISO-недели дневной сетки
│ │ ├── academic-calendar-title.js # Название из кода, профиля, формы и года
@@ -83,6 +86,7 @@ frontend/
│ │ ├── university-structure.html
│ │ ├── department-workspace.html
│ │ ├── schedule-view.html
│ │ ├── teacher-absences.html
│ │ ├── schedule.html
│ │ ├── academic-calendar.html
│ │ └── auditorium-workload.html
@@ -142,8 +146,8 @@ frontend/
| Роль | Доступные вкладки |
|------|-------------------|
| `ADMIN` | Все вкладки |
| `EDUCATION_OFFICE` | Просмотр расписаний, конструктор расписания, календарный график, загруженность и аудитории; в settings — временные слоты и формы обучения |
| `DEPARTMENT` | Кабинет кафедры, просмотр расписаний |
| `EDUCATION_OFFICE` | Просмотр и конструктор расписаний, отсутствия и замены, календарный график, загруженность и аудитории; в settings — временные слоты и формы обучения |
| `DEPARTMENT` | Кабинет кафедры, просмотр расписаний, отсутствия и подтверждение заявок своих преподавателей |
| `SCHEDULE_VIEWER` | Только просмотр расписаний |
Пути `/department/` и `/edu-office/` оставлены как входные redirect-страницы в общую панель. Отдельные кабинеты не дублируют UI админ-панели.
@@ -166,6 +170,7 @@ frontend/
| `university-structure` | Кафедры, специальности и профили обучения; профили доступны отдельной внутренней вкладкой и через кнопку специальности | `/api/departments`, `/api/specialties`, `/api/specialties/{id}/profiles` |
| `department-workspace` | Кабинет кафедры: дисциплины, импорт, комментарии, преподаватели, привязка преподавателей, заявки на новых преподавателей и нагрузка | `/api/department/*`, `/api/department/teacher-requests`, `/api/workload/teachers` |
| `schedule-view` | Просмотр расписаний: семестр выбирается в дополнительных фильтрах, для учебного периода строится двухнедельный диапазон, найденные расписания выбираются в переключателе, а `ADMIN` и `EDUCATION_OFFICE` редактируют конкретное занятие в боковой панели без изменения правила | `/api/schedule/semesters`, `/api/schedule/search`, `/api/edu-office/schedule/overrides`, `/api/admin/time-slots/effective` |
| `teacher-absences` | Реестр отсутствий, подтверждение преподавательских заявок, проверенные варианты замен/переносов и журнал решений | `/api/teacher-absences`, `/api/users/teachers` |
| `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/academic-calendars/{id}/subjects`, `/api/admin/calendar/activity-types`, `/api/specialties`, `/api/specialties/{id}/profiles`, `/api/education-forms`, `/api/subjects` |
| `auditorium-workload` | Динамическая загруженность аудиторий, преподавателей и кафедр: сводная матрица по дате или совмещённая таблица выбранной сущности по чётной/нечётной неделе | `/api/classrooms`, `/api/users/teachers`, `/api/departments`, `/api/admin/time-slots`, `/api/equipments`, `/api/groups`, `/api/schedule`, `/api/admin/calendar/years` |
@@ -181,6 +186,11 @@ frontend/
- Вкладка `teacher-requests` показывает pending-заявки кафедр на создание преподавателей. Администратор может скорректировать кафедру, логин, ФИО и должность, задать пароль минимум 8 символов в скрытом поле с `autocomplete="new-password"`, затем одобрить заявку через `/api/teacher-requests/{id}/approve` или отклонить её через `/api/teacher-requests/{id}/reject`. Для роли `ADMIN` счётчик pending-заявок выводится в пункте меню «Заявки» и рядом с заголовком страницы, чтобы очередь была видна без открытия вкладки.
- Вкладка `department-workspace` в блоке преподавателей объединяет данные `/api/department/teachers` и `/api/workload/teachers`: каждый преподаватель показывается одной карточкой с должностью и нагрузкой за выбранный период, преподаватели без занятий получают нулевую нагрузку, а преподаватели из расписания добавляются без дублей. Если дата начала периода выбрана позже даты окончания, поле окончания очищается, а расчёт нагрузки ждёт корректный период.
- Вкладка `department-workspace` позволяет кафедре добавить существующего активного преподавателя на свою кафедру через `/api/department/teachers/{teacherId}/assignments`, отправить заявку на нового преподавателя через `/api/department/teacher-requests` и видеть статусы собственных заявок в таблице.
- Вкладка `teacher-absences` доступна администратору, учебному отделу и кафедре. Верхний
командный блок показывает очередь инцидентов, форма регистрирует преподавателя, период и
причину, а реестр разделяет статусы согласования. Полноэкранный мастер выводит каждое
затронутое занятие отдельной строкой и предлагает только кандидатов, уже проверенных
backend. Пустая строка не отправляется; выбранные решения применяются одним пакетом.
- Компоновка `department-workspace` использует собственные CSS-сетки `department-workspace-filter-grid` и `department-workspace-actions-grid`: фильтры периода отделены от сеток расписания, загрузка дисциплин занимает широкую колонку, формы преподавателей выравниваются справа, а списки и таблицы идут полноширинными блоками ниже.
- Вкладка `schedule-view` показывает найденные занятия в режиме одной активной таблицы. Пользователь выбирает, что смотреть: группу, преподавателя, аудиторию или кафедру; основная кнопка `Показать` расположена в заголовке блока параметров, а пустое состояние таблицы с подсказкой об обновлении содержит дополнительную кнопку `Показать расписание`. В дополнительных фильтрах доступен семестр из справочника `/api/schedule/semesters`, предназначенного только для чтения. Для текущего семестра сохраняется текущая двухнедельная точка просмотра, а при выборе другого семестра диапазон начинается с понедельника его первой недели. Frontend запрашивает две недели и собирает найденные расписания в переключатель результатов. На странице не выводится стек таблиц: виден один выбранный результат, а остальные доступны через чипы и кнопки предыдущего/следующего расписания; чипы результатов переносятся и отделены от счётчика стабильным отступом. Для режима кафедры и роли `DEPARTMENT` расписание ограничивается кафедрой пользователя; преподавательские и студенческие отдельные страницы пока остаются самостоятельными. Таблица строится как строки пар и столбцы дней недели. Нечётная неделя отображается в верхней половине ячейки, чётная — в нижней, а одинаковые занятия в обе недели схлопываются в цельную ячейку. Бейдж диапазона недель скрывается для занятий на весь семестр, а для занятий до конца семестра показывает только неделю начала в формате `(с 5 нед.)`. На мобильной ширине вместо широкой недельной матрицы показывается один день активного расписания с переключателем дней.
- Для `ADMIN` и `EDUCATION_OFFICE` карточка занятия содержит кнопку `Изменить`, а уже изменённая пара — индикатор разовой правки. Справа открывается полупрозрачная боковая панель с размытием содержимого под ней; внешний затемнённый слой также размывает страницу, а на мобильном устройстве панель занимает весь экран. Режим `Редактирование` сравнивает `Было по правилу / Станет`, позволяет изменить дату, эффективный временной слот, преподавателя, аудиторию, формат и комментарий, отменить занятие или удалить override через `Вернуть по правилу`. Селект аудитории получает записи из `/api/classrooms`, но показывает только поле `name`, без корпуса и этажа. По умолчанию выводятся семь дней исходной недели; кнопка `Выбрать другую дату` раскрывает календарь всего семестра, где неучебные даты отключены. После смены даты загружается эффективная сетка дня: сначала выбирается тот же ID слота, затем совпадающий интервал, иначе требуется ручной выбор. Дата или время формируют `MOVE`, а только преподаватель, аудитория или формат — `REPLACE`.
@@ -325,6 +335,9 @@ if (!session || !AUTHORIZED_ROLES.includes(session.role)) {
- выбор даты через `input[type="date"]`;
- запрос `GET /api/schedule?teacherId={userId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}`;
- отображение дисциплины, времени, типа занятия, лабораторных подгрупп, аудитории и всех групп правила.
- форма собственной заявки на отсутствие через `POST /api/teacher-absences`;
- список статусов заявок и отмена ещё не подтверждённой записи через
`DELETE /api/teacher-absences/{id}`.
Если refresh-cookie недействительна или роль не `TEACHER`, страница возвращает пользователя на вход.