задачи 2 и 8

This commit is contained in:
Zuev
2026-08-11 18:18:08 +03:00
parent 519864b962
commit 18a97b293a
59 changed files with 6237 additions and 276 deletions

View File

@@ -623,6 +623,7 @@ GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
```json
{
"scheduleVersionId": 7,
"subjectId": 1,
"semesterId": 1,
"lectureAcademicHours": 32,
@@ -652,7 +653,7 @@ CRUD доступен по:
| Метод | URL | Назначение |
|-------|-----|------------|
| `GET` | `/api/admin/schedule-rules` | Список правил, фильтры `semesterId`, `groupId` |
| `GET` | `/api/admin/schedule-rules` | Список правил, фильтры `semesterId`, `groupId`, `versionId` |
| `GET` | `/api/admin/schedule-rules/{id}` | Одно правило |
| `POST` | `/api/admin/schedule-rules` | Создать правило |
| `PUT` | `/api/admin/schedule-rules/{id}` | Обновить правило |
@@ -660,6 +661,11 @@ CRUD доступен по:
`timeSlotId` должен ссылаться на базовый слот (`scopeApplyMode = DEFAULT`). Субботняя и ручные сетки не выбираются в правиле напрямую.
Создание, изменение и архивирование правил разрешены только внутри версии со статусом
`DRAFT`; `scheduleVersionId` обязателен в payload. Версия должна относиться к указанному
семестру. Без `versionId` список возвращает только правила текущей опубликованной версии,
а конструктор всегда передаёт ID выбранного черновика.
`subgroupIds` можно передавать только для лабораторного слота. Каждая подгруппа должна относиться к одной из групп правила. Если лабораторная проводится у нескольких групп одновременно, в одном слоте можно передать разные подгруппы этих групп, например `[10, 22]`. Для совместимости одиночный `subgroupId` тоже принимается, но новый формат — `subgroupIds`. Для лекций и практик оба поля должны быть пустыми, иначе API вернёт ошибку валидации. В одном слоте нельзя выбрать больше одной подгруппы одной и той же группы.
Часы и недели начала задаются отдельно для лекций, лабораторных и практик. Каждый лимит
@@ -841,6 +847,112 @@ API возвращает `409 Conflict`; соседние интервалы и
}
```
### Версии и публикация расписания
| Метод | URL | Назначение |
|-------|-----|------------|
| `GET` | `/api/edu-office/schedule/versions?semesterId=` | Версии семестра с авторами, датами и количеством правил |
| `GET` | `/api/edu-office/schedule/versions/{id}` | Одна версия |
| `POST` | `/api/edu-office/schedule/versions` | Создать пустой черновик или копию выбранной версии |
| `GET` | `/api/edu-office/schedule/versions/{id}/validate` | Полная проверка правил черновика |
| `GET` | `/api/edu-office/schedule/versions/{id}/diff?baseVersionId=` | Сравнение правил и сформированных занятий |
| `POST` | `/api/edu-office/schedule/versions/{id}/publish` | Атомарно опубликовать проверенный черновик |
| `POST` | `/api/edu-office/schedule/versions/{id}/restore` | Восстановить ранее опубликованную архивную версию |
| `GET` | `/api/edu-office/schedule/versions/history?semesterId=` | Хронология создания, публикации, архивации и восстановления |
Доступ имеют только `ADMIN` и `EDUCATION_OFFICE`. Для семестра допускается ровно одна
версия со статусом `PUBLISHED`; остальные имеют статус `DRAFT` или `ARCHIVED`. Создание
копии переносит правила, группы, слоты и подгруппы, сохраняя `versionGroupId` для diff.
```json
{
"semesterId": 3,
"name": "Расписание после распределения аудиторий",
"basedOnVersionId": 11
}
```
Публикация и восстановление принимают обязательную причину:
```json
{ "reason": "Согласовано учебным отделом 11.08.2026" }
```
Перед публикацией backend блокирует версию, повторно проверяет весь набор активных правил,
архивирует текущую публикацию и переводит черновик в `PUBLISHED` одной транзакцией. При
ошибке ни один статус не меняется. Обычные endpoints просмотра и генерации выбирают только
опубликованную версию; overrides архивных версий не попадают в актуальное расписание и
операционный реестр.
### Анализ качества расписания
| Метод | URL | Назначение |
|-------|-----|------------|
| `GET` | `/api/edu-office/schedule/quality?semesterId=&versionId=` | Итоговая оценка, метрики и объяснимый список проблем выбранной версии |
| `GET` | `/api/edu-office/schedule/quality/recommendations?semesterId=&versionId=&scheduleRuleSlotId=&lessonDate=` | Проверенные локальные варианты улучшения опубликованного занятия |
Доступ имеют только `ADMIN` и `EDUCATION_OFFICE`. `versionId` необязателен: без него
анализируется текущая публикация. Для опубликованной версии анализатор рассматривает
фактическое расписание всего семестра, включая уже применённые `schedule_overrides`, и учитывает окна
групп и преподавателей, перегруженные дни, вместимость и избыточный размер аудитории,
согласованные пожелания и недоступность преподавателей, а также равномерность нагрузки.
Длинный семестр загружается внутренними интервалами не более 120 дней, поэтому публичный
запрос не требует диапазона дат.
Черновик и архивная версия генерируются напрямую по собственным правилам, без подмешивания
override текущей публикации. Для них доступны оценка, метрики и проблемы, но не локальные
рекомендации: исправления черновика выполняются в конструкторе до публикации, архив остаётся
только для чтения.
```json
{
"semesterId": 3,
"semesterLabel": "2026/2027 · осенний семестр",
"scheduleVersionId": 12,
"scheduleVersionNumber": 2,
"scheduleVersionName": "Расписание после распределения аудиторий",
"scheduleVersionStatus": "DRAFT",
"score": 84,
"rawPenalty": 17,
"lessonCount": 36,
"totalProblemCount": 5,
"problemsTruncated": false,
"metrics": [
{
"code": "GROUP_GAPS",
"label": "Окна у групп",
"value": 3,
"unit": "окон",
"penalty": 6,
"explanation": "Промежутки между занятиями одной группы"
}
],
"problems": [
{
"id": "GROUP_GAP:2026-09-14:31:18",
"type": "GROUP_GAP",
"severity": "MEDIUM",
"title": "Окно в расписании группы",
"penalty": 2,
"scheduleRuleSlotId": 31,
"lessonDate": "2026-09-14",
"optimizable": true
}
]
}
```
`score` находится в диапазоне от 0 до 100 и нормализует сумму объяснимых штрафов
относительно числа занятий. Поле `totalProblemCount` содержит полный размер результата,
а массив `problems` ограничен 300 элементами; `problemsTruncated` сообщает об усечении.
Рекомендации подбирают другой эффективный временной слот того же учебного дня либо
подходящую активную аудиторию. Занятия, уже закреплённые ручным override, исключаются.
Каждый кандидат до выдачи проходит `ScheduleOverrideService`, содержит готовый payload
`override`, ожидаемые `scoreDelta`, `penaltyDelta`, улучшения и компромиссы. API ничего не
изменяет автоматически: после подтверждения пользователь применяет payload обычным
`POST /api/edu-office/schedule/overrides`.
### Отсутствия преподавателей и мастер замены
| Метод | URL | Назначение |

View File

@@ -265,8 +265,32 @@ constraint. При чтении API разворачивает периоды о
- **Жизненный цикл:** архивные преподаватели, аудитории, группы и дисциплины не принимаются в новых правилах.
- **Правило расписания:** `status=ARCHIVED` или дата вне `valid_from` / `valid_to` исключают правило из генерации.
- **Доступность аудитории:** `is_available=false` запрещает новые назначения, но не удаляет историю.
- **Конфликты слотов:** сначала попарно проверяются слоты самого нового payload, включая точные дубли, затем — активные правила того же семестра. Конфликт возникает при пересечении дня, базового временного слота, чётности (`BOTH` пересекается с любой чётностью) и активных недель слота, если совпадает преподаватель, аудитория или учебная группа. `ODD` и `EVEN` между собой не конфликтуют. Активные недели считаются из лимита часов типа занятия, недели начала, чётности и порядка слотов внутри правила; например, занятие на 1-3 неделях не конфликтует с тем же ресурсом с 4 недели. Для лабораторных занятий разные подгруппы одной группы могут идти параллельно, но занятие для всей группы конфликтует с любой её подгруппой. Backend возвращает `409 Conflict`; `conflictRule` присутствует только для конфликта с сохранённым правилом, а внутренний конфликт описывается полями и русскими причинами без искусственной записи.
- **Конкурентная запись:** публичные методы `ScheduleRuleService` являются транзакционными. Создание сначала блокирует строку семестра, а update блокирует правило и старый/новый семестры в стабильном порядке, поэтому два backend-pod не могут одновременно пройти проверку одного семестра по устаревшему снимку.
- **Конфликты слотов:** сначала попарно проверяются слоты самого нового payload, включая точные дубли, затем — активные правила той же версии. Конфликт возникает при пересечении дня, базового временного слота, чётности (`BOTH` пересекается с любой чётностью) и активных недель слота, если совпадает преподаватель, аудитория или учебная группа. `ODD` и `EVEN` между собой не конфликтуют. Активные недели считаются из лимита часов типа занятия, недели начала, чётности и порядка слотов внутри правила; например, занятие на 1-3 неделях не конфликтует с тем же ресурсом с 4 недели. Для лабораторных занятий разные подгруппы одной группы могут идти параллельно, но занятие для всей группы конфликтует с любой её подгруппой. Backend возвращает `409 Conflict`; `conflictRule` присутствует только для конфликта с сохранённым правилом, а внутренний конфликт описывается полями и русскими причинами без искусственной записи.
- **Конкурентная запись:** публичные методы `ScheduleRuleService` являются транзакционными. Создание блокирует строку семестра и выбранную версию, а update — правило, версию и старый/новый семестры в стабильном порядке. Публикация блокирует версию до завершения полной проверки, поэтому другой backend-pod не может дописать правило после валидации черновика.
### Черновики, версии и публикация
Правила каждого семестра принадлежат явной версии расписания. Жизненный цикл версии:
1. `DRAFT` создаётся пустым или как полная копия выбранной версии.
2. Конструктор добавляет, изменяет и архивирует правила только в выбранном черновике.
3. Полная проверка выявляет внутренние конфликты правил; diff сопоставляет правила по
стабильному `version_group_id` и отдельно сравнивает сформированные занятия семестра.
4. Публикация требует причины и в одной транзакции архивирует прежнюю публикацию, затем
переводит проверенный черновик в `PUBLISHED`.
5. Ранее опубликованная версия получает `ARCHIVED` и может быть восстановлена такой же
атомарной операцией с обязательной причиной.
На уровне БД частичный уникальный индекс допускает только одну `PUBLISHED`-версию на
семестр. Блокировки версии и набора версий семестра не позволяют публикации пересечься с
редактированием черновика или конкурентной публикацией. Каждое создание, архивирование,
публикация и восстановление записывается в неизменяемый журнал с автором, временем и
причиной.
Обычная генерация для студентов, преподавателей и кабинетов просмотра всегда выбирает
только опубликованные правила. Точечные изменения привязаны к слотам конкретной версии:
после новой публикации overrides прежней версии сохраняются как аудит, но не влияют на
актуальное расписание и не показываются в его операционном реестре.
### Точечные изменения расписания
@@ -386,6 +410,41 @@ constraint. При чтении API разворачивает периоды о
отозвать только собственную ожидающую заявку. История содержит автора, время, статус и
комментарий каждого перехода.
### Анализ качества и локальная оптимизация расписания
Анализатор качества работает без собственной таблицы и не меняет правила расписания.
Пользователь выбирает конкретную версию семестра. Для `PUBLISHED` он строит фактические
занятия через `ScheduleQueryService`, поэтому в расчёт входят переносы, замены и отмены из
`schedule_overrides`. `DRAFT` и `ARCHIVED` генерируются напрямую по собственным правилам,
без точечных изменений текущей публикации. Семестр загружается частями не более 120 дней,
но оценка рассчитывается единообразно по всему периоду.
Итоговая оценка от 0 до 100 формируется из объяснимых штрафов:
- окна групп и преподавателей между занятиями одного дня;
- пятая и последующие пары группы или преподавателя за день;
- нехватка мест и заметно избыточная вместимость аудитории;
- занятие в подтверждённый строго недоступный или нежелательный интервал;
- занятие вне предпочтительного интервала преподавателя на выбранный день недели;
- нарушение пожеланий `NO_GAPS` и `CONSECUTIVE`.
Неравномерность дневной нагрузки выводится отдельной диагностической метрикой в процентах
и не добавляет скрытого штрафа к итоговой оценке.
В ответе сохраняются исходный штраф, вклад каждого критерия и конкретные проблемы с
датой, занятием и затронутой сущностью. Это делает оценку воспроизводимой и позволяет
фильтровать проблемы по типу и серьёзности.
Для проблемы опубликованной версии помощник перебирает другие слоты эффективной сетки того же дня и
активные аудитории достаточной вместимости. Каждый вариант повторно проходит
`ScheduleOverrideService`, затем анализатор моделирует его влияние на общую оценку и
показывает улучшения и компромиссы. Занятия с ручным override считаются закреплёнными и не
получают рекомендаций. Применение возможно только после подтверждения пользователя через
обычный механизм `schedule_overrides`; автоматической публикации и полного solver в MVP
нет. Для черновика доступны те же оценка, метрики и объяснимые проблемы, но рекомендации
не создаются: пользователь исправляет правила в изолированном конструкторе и повторяет
проверку до публикации. Архив анализируется только для чтения.
## Привязка преподаватель ↔ дисциплина
Связь Many-to-Many через таблицу `teacher_subjects`:

View File

@@ -286,10 +286,37 @@ erDiagram
BIGINT calendar_id FK
}
schedule_versions {
BIGSERIAL id PK
BIGINT semester_id FK
INT version_number
VARCHAR name
VARCHAR status
BIGINT based_on_version_id FK
BIGINT restored_from_version_id FK
TEXT change_reason
BIGINT created_by FK
TIMESTAMPTZ created_at
BIGINT published_by FK
TIMESTAMPTZ published_at
TIMESTAMPTZ archived_at
}
schedule_version_history {
BIGSERIAL id PK
BIGINT version_id FK
VARCHAR action
BIGINT actor_id FK
TEXT reason
TIMESTAMPTZ created_at
}
schedule_rules {
BIGSERIAL id PK
BIGINT subject_id FK
BIGINT semester_id FK
BIGINT schedule_version_id FK
BIGINT version_group_id
VARCHAR status
DATE valid_from
DATE valid_to
@@ -442,6 +469,12 @@ erDiagram
academic_years ||--o{ semesters : "academic_year_id"
academic_years ||--o{ academic_calendars : "academic_year_id"
academic_years ||--o{ student_group_calendar_assignments : "academic_year_id"
semesters ||--o{ schedule_versions : "semester_id"
schedule_versions ||--o{ schedule_rules : "schedule_version_id"
schedule_versions ||--o{ schedule_version_history : "version_id"
schedule_versions o|--o{ schedule_versions : "based_on/restored_from"
users ||--o{ schedule_versions : "created_by/published_by"
users ||--o{ schedule_version_history : "actor_id"
semesters ||--o{ schedule_rules : "semester_id"
specialties ||--o{ academic_calendars : "specialty_id"
specialty_profiles ||--o{ academic_calendars : "specialty_profile_id"
@@ -896,12 +929,50 @@ V1 создаёт GiST exclusion constraint `ex_academic_years_no_overlap` дл
триггеры защищают назначение при изменении группы, графика и границ учебного года; блокировки
ссылочных строк закрывают конкурентные записи между несколькими backend-pod.
#### `schedule_versions` — Версии расписания семестра
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID версии |
| `semester_id` | BIGINT FK → semesters (CASCADE) | Семестр |
| `version_number` | INT CHECK(> 0) | Последовательный номер внутри семестра |
| `name` | VARCHAR(160) | Пользовательское название |
| `status` | VARCHAR(20) | `DRAFT`, `PUBLISHED` или `ARCHIVED` |
| `based_on_version_id` | BIGINT FK → schedule_versions | Версия-основа черновика |
| `restored_from_version_id` | BIGINT FK → schedule_versions | Публикация, которую заменили при восстановлении |
| `change_reason` | TEXT | Причина последней публикации или восстановления |
| `created_by` | BIGINT FK → users | Автор черновика |
| `created_at` | TIMESTAMPTZ | Время создания |
| `published_by` | BIGINT FK → users | Автор публикации |
| `published_at` | TIMESTAMPTZ | Время последней публикации |
| `archived_at` | TIMESTAMPTZ | Время архивирования |
Пара `semester_id + version_number` уникальна. Частичный индекс
`uq_schedule_versions_published_semester` запрещает более одной строки `PUBLISHED` на
семестр. Начальная загрузка V1 создаёт опубликованную версию 1 для каждого семестра и
привязывает к ней существующие seed-правила.
#### `schedule_version_history` — Аудит версий расписания
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID события |
| `version_id` | BIGINT FK → schedule_versions (CASCADE) | Версия расписания |
| `action` | VARCHAR(30) | `CREATED`, `PUBLISHED`, `ARCHIVED` или `RESTORED` |
| `actor_id` | BIGINT FK → users | Автор действия; `NULL` для системной инициализации |
| `reason` | TEXT | Причина или описание события, до 2000 символов |
| `created_at` | TIMESTAMPTZ | Время события |
Журнал добавляется при каждом переходе версии и выводится в обратной хронологии. Он не
заменяет данные версии, а сохраняет отдельные факты аудита.
#### `schedule_rules` — Правила расписания
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `subject_id` | BIGINT FK → subjects | Дисциплина |
| `semester_id` | BIGINT FK → semesters | Семестр |
| `schedule_version_id` | BIGINT FK → schedule_versions (CASCADE) | Версия расписания |
| `lecture_academic_hours` | INT | Лимит академических часов лекций |
| `laboratory_academic_hours` | INT | Лимит академических часов лабораторных работ |
| `practice_academic_hours` | INT | Лимит академических часов практик |
@@ -914,7 +985,7 @@ V1 создаёт GiST exclusion constraint `ex_academic_years_no_overlap` дл
| `version_group_id` | BIGINT | Группа версий одного правила |
| `change_reason` | TEXT | Причина изменения |
`ScheduleRule` использует собственные поля жизненного цикла `status`, `valid_from` и `valid_to`: архивированное правило или правило вне периода действия не участвует в генерации расписания. В отличие от справочников на `LifecycleEntity`, таблица не содержит `active_from`/`active_to`, поэтому состояние правила проверяется по `valid_*`.
`ScheduleRule` использует собственные поля жизненного цикла `status`, `valid_from` и `valid_to`: архивированное правило или правило вне периода действия не участвует в генерации расписания. В отличие от справочников на `LifecycleEntity`, таблица не содержит `active_from`/`active_to`, поэтому состояние правила проверяется по `valid_*`. Публичная генерация дополнительно требует статус `PUBLISHED` у связанной версии. Уникальный индекс по `schedule_version_id + version_group_id` не позволяет дважды скопировать одну логическую линию правила в одну версию.
Базовая схема V1 требует, чтобы лимиты лекций, лабораторных и практик были кратны двум.
Неотрицательность каждого лимита и положительная сумма уже закреплены ограничениями V1;
@@ -1111,19 +1182,19 @@ CHECK фиксирует допустимую форму каждого типа
| Файл | Описание |
|------|----------|
| `V1__init.sql` | Полная baseline-схема: справочники, роли, refresh-сессии JWT, PostgreSQL rate limit и аудит входа, lifecycle-поля, история кафедр, календарные графики с интервальным хранением активностей и нумерацией недель `понедельник–воскресенье`, динамическое расписание, точечные изменения с переносом даты, seed, CHECK/UNIQUE/GiST-ограничения, конкурентно безопасные триггеры и комментарии |
| `V2__teacher_absences_and_replacement_wizard.sql` | Реестр отсутствий преподавателей, статусы согласования и журнал применённых/отклонённых решений со ссылками на обычные `schedule_overrides` |
| `V3__teacher_preferences_and_change_requests.sql` | Пожелания преподавателей на семестр, строгая и мягкая доступность, заявки на изменение занятия и неизменяемая история решений |
| `V1__init.sql` | Полная baseline-схема: справочники, роли, refresh-сессии JWT, PostgreSQL rate limit и аудит входа, lifecycle-поля, история кафедр, календарные графики, динамическое расписание, версии/черновики и аудит публикаций, точечные изменения, отсутствия и журнал замен, пожелания преподавателей, заявки на изменение занятий и их история, seed, CHECK/UNIQUE/GiST-ограничения, конкурентно безопасные триггеры и комментарии |
### Этап разработки
Исторические разработческие миграции V2–V7 по прямому решению владельца проекта были
объединены в baseline `V1`. После фиксации baseline нумерация начата заново: `V2`
добавляет отсутствия и мастер замены, а `V3` — пожелания преподавателей и заявки на
изменение занятий, не изменяя контрольную сумму `V1`.
Исторические разработческие миграции V2–V7, а затем повторно созданные V2 с отсутствиями
и мастером замены и V3 с пожеланиями и заявками преподавателей по прямому решению владельца
проекта объединены в baseline `V1`. В каталоге миграций остаётся один файл
`V1__init.sql`.
Интервальное хранение активностей и правильная нумерация недель календарного графика входят
непосредственно в V1.
Перед применением этой редакции требуется полностью пустая tenant-схема.
Перед применением этой редакции требуется полностью пустая tenant-схема: для базы, где
предыдущая V1 уже записана в `flyway_schema_history`, изменённая контрольная сумма вызовет
ошибку проверки.
### Полный сброс БД (локально)

View File

@@ -261,6 +261,9 @@ public class AbsenceController {
Текущее состояние проекта — единый baseline `V1__init.sql`: по прямому решению владельца
содержимое прежних V2–V7 объединено в V1, клиентских tenant-БД нет. До отдельного решения о
фиксации baseline новые DB-инварианты добавляются в V1 и проверяются на полностью чистой БД.
Таблицы отсутствий, решений по заменам, пожеланий и заявок преподавателей из последних
вариантов V2 и V3 также перенесены в этот единый baseline; отдельных файлов V2/V3 в
проекте нет.
### Применение

View File

@@ -112,15 +112,19 @@
## 2. Черновики, версии, публикация и аудит расписания
**Статус: реализовано в MVP.** Добавлены изолированные версии `DRAFT/PUBLISHED/ARCHIVED`,
полное копирование правил, валидация и diff правил/занятий, атомарная публикация,
восстановление предыдущей публикации и неизменяемый журнал действий. Экран качества умеет
проверять черновики до публикации; изменения в них выполняются через конструктор правил.
### Проблема и пользователи
Изменение активного правила сейчас сразу влияет на следующий запрос расписания. При большой
переработке семестра это создаёт риск показать студентам и преподавателям промежуточное
состояние. Учебному отделу нужен безопасный цикл «подготовить — проверить — опубликовать».
В таблице `schedule_rules` уже предусмотрены поля `version_group_id`, `change_reason`,
`created_by` и `created_at`, которые можно использовать как основу, но полноценный жизненный
цикл версий пока не реализован.
Поля `version_group_id`, `change_reason`, `created_by` и `created_at` в `schedule_rules`
используются вместе с отдельными таблицами версий и истории публикаций.
### Пользовательский сценарий
@@ -144,9 +148,10 @@
### Связь с текущей системой
Текущая динамическая генерация сохраняется, но получает явный контекст версии. Конструктор
правил работает как с черновиком, так и с опубликованной версией, а кабинеты просмотра — только с опубликованной версией. Точечные изменения можно вести отдельным аудитом либо включать в пакет публикации в зависимости от
будущих продуктовых требований.
Текущая динамическая генерация получила явный контекст версии. Конструктор правил меняет
только черновик, кабинеты просмотра используют только опубликованную версию, а анализатор
качества позволяет выбрать опубликованную, черновую или архивную версию. Точечные изменения
остаются привязаны к конкретной публикации и не переносятся в новый черновик автоматически.
### Дальнейшее развитие
@@ -396,7 +401,10 @@ feed должен отдавать только опубликованное р
**Ожидаемый результат:** заметное сокращение ручного заполнения и ошибок ввода.
**Сложность:** M. **Демонстрационный эффект:** высокий благодаря наглядному предпросмотру.
## 8. (отдельным запросом) Анализ качества и автоматическая оптимизация расписания
## 8. Анализ качества и автоматическая оптимизация расписания
**Статус:** реализован MVP анализатора и локальных рекомендаций. Полный solver, фоновые
черновики и сравнение нескольких глобальных вариантов остаются дальнейшим этапом.
### Проблема и пользователи
@@ -436,6 +444,12 @@ feed должен отдавать только опубликованное р
конфликтного или неудобного занятия. Такой MVP позволяет проверить метрики качества,
интерфейс объяснений и производительность на реальных данных.
Реализованный MVP анализирует фактическое расписание выбранного семестра с учётом ручных
правок, формирует оценку и список проблем, а для конкретного занятия предлагает проверенные
перестановки времени в пределах дня или аудитории. Кандидат применяется только после
подтверждения пользователя через существующий `schedule_override`; ручные правки считаются
закреплёнными.
Red Zone уже выявляет накладки и превышение вместимости, а вкладка загруженности показывает
занятые и свободные слоты. Новый анализатор не должен дублировать эти экраны: его ценность —
единая объяснимая оценка, мягкие критерии качества и рекомендации по улучшению.

View File

@@ -33,6 +33,8 @@ frontend/
│ ├── auth-session.test.mjs # Login/refresh/reload/logout и single-flight refresh
│ ├── dashboard-conflicts.test.mjs # Регрессии дат и состояний проверки конфликтов
│ ├── schedule-overrides.test.mjs # Действия, роли, недельный выбор и подбор времени разовой правки
│ ├── schedule-quality.test.mjs # Метрики качества, фильтры проблем и доступность вкладки
│ ├── schedule-versions.test.mjs # Статусы версий, журнал и навигация в черновик/качество
│ ├── schedule-view-semesters.test.mjs # Выбор семестра и расчёт двухнедельного диапазона просмотра
│ ├── teacher-absences.test.mjs # Payload мастера замены и доступность вкладки по ролям
│ ├── teacher-preferences.test.mjs # Календарь пожеланий, заявки и подсказки конструктора
@@ -52,7 +54,9 @@ frontend/
│ │ ├── modals.css # Модальные окна
│ │ ├── auditorium-workload.css # Таблицы расписаний и загруженности
│ │ ├── departments-data.css # Стили создания кафедры/специальности
│ │ └── teacher-absences.css # Отсутствия, пожелания и очередь заявок преподавателей
│ │ ├── teacher-absences.css # Отсутствия, пожелания и очередь заявок преподавателей
│ │ ├── schedule-quality.css # Диагностический экран качества расписания
│ │ └── schedule-versions.css # Контур публикации, diff и журнал версий
│ ├── js/
│ │ ├── main.js # Инициализация, маршрутизация, навигация
│ │ ├── role-capabilities.js # Единая матрица вкладок admin/settings по ролям
@@ -72,6 +76,8 @@ frontend/
│ │ ├── schedule-view.js # Просмотр расписаний и запуск разовой правки из карточки
│ │ ├── schedule-override-panel.js # Боковая панель и реестр разовых изменений
│ │ ├── teacher-absences.js # Отсутствия, согласование пожеланий и заявок на изменение
│ │ ├── schedule-quality.js # Оценка, фильтры и локальные рекомендации
│ │ ├── schedule-versions.js # Черновики, публикация, восстановление, diff и аудит
│ │ ├── schedule.js # Конструктор правил и подсказки пожеланий преподавателей
│ │ ├── academic-calendar-grid.js # Расчёт ISO-недели дневной сетки
│ │ ├── academic-calendar-title.js # Название из кода, профиля, формы и года
@@ -88,6 +94,8 @@ frontend/
│ │ ├── department-workspace.html
│ │ ├── schedule-view.html
│ │ ├── teacher-absences.html
│ │ ├── schedule-quality.html
│ │ ├── schedule-versions.html
│ │ ├── schedule.html
│ │ ├── academic-calendar.html
│ │ └── auditorium-workload.html
@@ -147,7 +155,7 @@ frontend/
| Роль | Доступные вкладки |
|------|-------------------|
| `ADMIN` | Все вкладки |
| `EDUCATION_OFFICE` | Просмотр и конструктор расписаний, отсутствия и замены, календарный график, загруженность и аудитории; в settings — временные слоты и формы обучения |
| `EDUCATION_OFFICE` | Просмотр, конструктор и анализ качества расписаний, отсутствия и замены, календарный график, загруженность и аудитории; в settings — временные слоты и формы обучения |
| `DEPARTMENT` | Кабинет кафедры, просмотр расписаний, отсутствия и подтверждение заявок своих преподавателей |
| `SCHEDULE_VIEWER` | Только просмотр расписаний |
@@ -173,6 +181,8 @@ frontend/
| `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/teacher-preferences`, `/api/teacher-change-requests`, `/api/users/teachers` |
| `schedule` | Конструктор правил динамического расписания с подсказками согласованных пожеланий и выезжающей визуальной матрицей групп | `/api/admin/schedule-rules`, `/api/teacher-preferences`, `/api/admin/time-slots`, `/api/admin/calendar/years`, `/api/lesson-types`, `/api/subgroups` |
| `schedule-versions` | Контур публикации: текущая версия, черновики, diff, архив, восстановление и журнал | `/api/edu-office/schedule/versions` |
| `schedule-quality` | Диагностика выбранной версии семестра: оценка, метрики, фильтруемые проблемы; для публикации — подтверждаемое локальное улучшение | `/api/edu-office/schedule/quality`, `/api/edu-office/schedule/quality/recommendations`, `/api/edu-office/schedule/overrides` |
| `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` |
@@ -197,6 +207,22 @@ frontend/
`ADMIN` и `EDUCATION_OFFICE`; кафедра видит записи своих преподавателей без управляющих
действий. Каждая карточка показывает исходное и запрошенное состояние, результат
предварительной проверки и хронологию решения.
- Вкладка `schedule-quality` доступна администратору и учебному отделу. После выбора
семестра и версии она выводит круговую оценку от 0 до 100, карточки метрик и реестр проблем с
фильтрами по серьёзности и типу. Для проблемы, связанной с конкретным незакреплённым
занятием опубликованной версии, кнопка `Подобрать улучшение` запрашивает проверенные варианты времени и
аудитории. Карточка кандидата показывает изменение оценки, улучшения и компромиссы;
override создаётся только после явного подтверждения, затем анализ запускается заново.
Для `DRAFT` экран рассчитывает те же метрики и проблемы напрямую по правилам черновика,
но направляет пользователя в конструктор и не создаёт override. `ARCHIVED` анализируется
только для чтения. Экран не предлагает автоматически менять занятия, уже отредактированные вручную.
- Вкладка `schedule-versions` доступна администратору и учебному отделу и оформлена как
отдельный контур публикации. Верхняя карточка показывает версию, которую видят конечные
пользователи; ниже расположены черновики, сравнение правил и занятий, архив и журнал.
Новый черновик копирует выбранную опубликованную версию. Перед публикацией интерфейс
одновременно запрашивает полную валидацию и diff, требует причину и блокирует действие
при ошибках. Кнопки черновика сохраняют его ID в `localStorage` и открывают конструктор
или анализ качества в нужном контексте; архивную публикацию можно восстановить с причиной.
- Компоновка `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`.