исправил качество правил и изменение расписания (не черновика)

This commit is contained in:
Zuev
2026-08-12 01:07:35 +03:00
parent 18a97b293a
commit 491e373cc3
23 changed files with 826 additions and 316 deletions

View File

@@ -529,6 +529,8 @@ GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
Повторное название года, повторный тип семестра или пересечение возвращаются как
`409 Conflict`; неверный порядок дат и выход семестра за границы года — как `400 Bad Request`.
При сужении учебного года все существующие семестры должны оставаться внутри новых границ.
Создание семестра также создаёт для него пустую версию 1 со статусом `PUBLISHED` и названием
`Основное расписание`, поэтому конструктор сразу может работать в обычном режиме.
**Тело создания/обновления кода активности:**
```json
@@ -661,10 +663,13 @@ CRUD доступен по:
`timeSlotId` должен ссылаться на базовый слот (`scopeApplyMode = DEFAULT`). Субботняя и ручные сетки не выбираются в правиле напрямую.
Создание, изменение и архивирование правил разрешены только внутри версии со статусом
`DRAFT`; `scheduleVersionId` обязателен в payload. Версия должна относиться к указанному
семестру. Без `versionId` список возвращает только правила текущей опубликованной версии,
а конструктор всегда передаёт ID выбранного черновика.
Создание, изменение и архивирование правил разрешены внутри версии со статусом `PUBLISHED`
или `DRAFT`; `scheduleVersionId` обязателен в payload. Версия должна относиться к указанному
семестру. Изменения `PUBLISHED`-версии сразу влияют на публичное расписание, изменения
`DRAFT` остаются изолированными до публикации, а `ARCHIVED` доступна только для чтения.
Без `versionId` список возвращает только правила текущей опубликованной версии. При update
ID существующих слотов сохраняются; удаление слота, на который ссылаются точечные изменения,
заявки преподавателей или решения по отсутствиям, отклоняется явной ошибкой `400`.
`subgroupIds` можно передавать только для лабораторного слота. Каждая подгруппа должна относиться к одной из групп правила. Если лабораторная проводится у нескольких групп одновременно, в одном слоте можно передать разные подгруппы этих групп, например `[10, 22]`. Для совместимости одиночный `subgroupId` тоже принимается, но новый формат — `subgroupIds`. Для лекций и практик оба поля должны быть пустыми, иначе API вернёт ошибку валидации. В одном слоте нельзя выбрать больше одной подгруппы одной и той же группы.
@@ -862,7 +867,8 @@ API возвращает `409 Conflict`; соседние интервалы и
Доступ имеют только `ADMIN` и `EDUCATION_OFFICE`. Для семестра допускается ровно одна
версия со статусом `PUBLISHED`; остальные имеют статус `DRAFT` или `ARCHIVED`. Создание
копии переносит правила, группы, слоты и подгруппы, сохраняя `versionGroupId` для diff.
копии может использовать опубликованную версию или другой черновик и переносит правила,
группы, слоты и подгруппы, сохраняя `versionGroupId` для diff.
```json
{

View File

@@ -267,23 +267,27 @@ constraint. При чтении API разворачивает периоды о
- **Доступность аудитории:** `is_available=false` запрещает новые назначения, но не удаляет историю.
- **Конфликты слотов:** сначала попарно проверяются слоты самого нового payload, включая точные дубли, затем — активные правила той же версии. Конфликт возникает при пересечении дня, базового временного слота, чётности (`BOTH` пересекается с любой чётностью) и активных недель слота, если совпадает преподаватель, аудитория или учебная группа. `ODD` и `EVEN` между собой не конфликтуют. Активные недели считаются из лимита часов типа занятия, недели начала, чётности и порядка слотов внутри правила; например, занятие на 1-3 неделях не конфликтует с тем же ресурсом с 4 недели. Для лабораторных занятий разные подгруппы одной группы могут идти параллельно, но занятие для всей группы конфликтует с любой её подгруппой. Backend возвращает `409 Conflict`; `conflictRule` присутствует только для конфликта с сохранённым правилом, а внутренний конфликт описывается полями и русскими причинами без искусственной записи.
- **Конкурентная запись:** публичные методы `ScheduleRuleService` являются транзакционными. Создание блокирует строку семестра и выбранную версию, а update — правило, версию и старый/новый семестры в стабильном порядке. Публикация блокирует версию до завершения полной проверки, поэтому другой backend-pod не может дописать правило после валидации черновика.
- **Связанные слоты:** update сохраняет ID переданных существующих слотов, чтобы не разрывать точечные изменения и операционный аудит. Удаление слота отклоняется, если на него ссылаются override, заявка преподавателя или решение по отсутствию.
### Черновики, версии и публикация
Правила каждого семестра принадлежат явной версии расписания. Жизненный цикл версии:
1. `DRAFT` создаётся пустым или как полная копия выбранной версии.
2. Конструктор добавляет, изменяет и архивирует правила только в выбранном черновике.
3. Полная проверка выявляет внутренние конфликты правил; diff сопоставляет правила по
1. При создании семестра автоматически появляется пустая версия 1 `PUBLISHED` — основное расписание.
2. Конструктор по умолчанию открывает опубликованное расписание: изменения его правил сразу
видны конечным пользователям. Пользователь может переключиться на существующий `DRAFT`.
3. `DRAFT` создаётся пустым через API или прямо в конструкторе как полная копия выбранной
опубликованной либо черновой версии; после создания он сразу становится текущим.
4. Полная проверка выявляет внутренние конфликты правил; diff сопоставляет правила по
стабильному `version_group_id` и отдельно сравнивает сформированные занятия семестра.
4. Публикация требует причины и в одной транзакции архивирует прежнюю публикацию, затем
5. Публикация требует причины и в одной транзакции архивирует прежнюю публикацию, затем
переводит проверенный черновик в `PUBLISHED`.
5. Ранее опубликованная версия получает `ARCHIVED` и может быть восстановлена такой же
6. Ранее опубликованная версия получает `ARCHIVED` и может быть восстановлена такой же
атомарной операцией с обязательной причиной.
На уровне БД частичный уникальный индекс допускает только одну `PUBLISHED`-версию на
семестр. Блокировки версии и набора версий семестра не позволяют публикации пересечься с
редактированием черновика или конкурентной публикацией. Каждое создание, архивирование,
редактированием расписания или конкурентной публикацией. Каждое создание, архивирование,
публикация и восстановление записывается в неизменяемый журнал с автором, временем и
причиной.

View File

@@ -950,7 +950,9 @@ V1 создаёт GiST exclusion constraint `ex_academic_years_no_overlap` дл
Пара `semester_id + version_number` уникальна. Частичный индекс
`uq_schedule_versions_published_semester` запрещает более одной строки `PUBLISHED` на
семестр. Начальная загрузка V1 создаёт опубликованную версию 1 для каждого семестра и
привязывает к ней существующие seed-правила.
привязывает к ней существующие seed-правила. Для семестров, создаваемых через API после
запуска, `AcademicPeriodService` в той же транзакции создаёт пустую опубликованную версию 1
`Основное расписание` и две записи истории: создание и публикацию.
#### `schedule_version_history` — Аудит версий расписания

View File

@@ -115,7 +115,8 @@
**Статус: реализовано в MVP.** Добавлены изолированные версии `DRAFT/PUBLISHED/ARCHIVED`,
полное копирование правил, валидация и diff правил/занятий, атомарная публикация,
восстановление предыдущей публикации и неизменяемый журнал действий. Экран качества умеет
проверять черновики до публикации; изменения в них выполняются через конструктор правил.
проверять черновики до публикации. Конструктор по умолчанию редактирует опубликованное
расписание, позволяет переключиться на черновик и создать новую копию выбранной версии.
### Проблема и пользователи
@@ -128,8 +129,9 @@
### Пользовательский сценарий
1. Пользователь создаёт черновик расписания для выбранного семестра.
2. Все изменения правил применяются только к черновику и не видны обычным пользователям.
1. Пользователь редактирует основное опубликованное расписание напрямую либо создаёт
черновик для выбранного семестра на основе открытой версии.
2. Изменения опубликованной версии видны сразу, а изменения черновика изолированы до публикации.
3. Перед публикацией система проверяет конфликты и показывает сравнение с текущей версией:
добавленные, удалённые и изменённые занятия.
4. Ответственный сотрудник указывает причину и публикует версию одной операцией.
@@ -149,8 +151,9 @@
### Связь с текущей системой
Текущая динамическая генерация получила явный контекст версии. Конструктор правил меняет
только черновик, кабинеты просмотра используют только опубликованную версию, а анализатор
качества позволяет выбрать опубликованную, черновую или архивную версию. Точечные изменения
выбранную опубликованную или черновую версию, кабинеты просмотра используют только
опубликованную, а анализатор качества позволяет выбрать опубликованную, черновую или
архивную версию. Точечные изменения
остаются привязаны к конкретной публикации и не переносятся в новый черновик автоматически.
### Дальнейшее развитие

View File

@@ -180,7 +180,7 @@ frontend/
| `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/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` | Конструктор правил: по умолчанию редактирует опубликованное расписание, позволяет переключиться на черновик или создать его из выбранной версии; содержит подсказки пожеланий и визуальную матрицу групп | `/api/admin/schedule-rules`, `/api/edu-office/schedule/versions`, `/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` |
@@ -219,9 +219,12 @@ frontend/
- Вкладка `schedule-versions` доступна администратору и учебному отделу и оформлена как
отдельный контур публикации. Верхняя карточка показывает версию, которую видят конечные
пользователи; ниже расположены черновики, сравнение правил и занятий, архив и журнал.
Новый черновик копирует выбранную опубликованную версию. Перед публикацией интерфейс
В конструкторе опубликованная версия текущего семестра выбирается по умолчанию и выделяется
зелёным контуром; её изменения применяются сразу. Селект позволяет перейти в медный режим
черновика. Кнопка `Создать черновик` копирует выбранное опубликованное расписание или
черновик и сразу открывает новую рабочую копию. Перед публикацией интерфейс
одновременно запрашивает полную валидацию и diff, требует причину и блокирует действие
при ошибках. Кнопки черновика сохраняют его ID в `localStorage` и открывают конструктор
при ошибках. Кнопки версий передают их ID одноразово через `localStorage` и открывают конструктор
или анализ качества в нужном контексте; архивную публикацию можно восстановить с причиной.
- Компоновка `department-workspace` использует собственные CSS-сетки `department-workspace-filter-grid` и `department-workspace-actions-grid`: фильтры периода отделены от сеток расписания, загрузка дисциплин занимает широкую колонку, формы преподавателей выравниваются справа, а списки и таблицы идут полноширинными блоками ниже.
- Вкладка `schedule-view` показывает найденные занятия в режиме одной активной таблицы. Пользователь выбирает, что смотреть: группу, преподавателя, аудиторию или кафедру; основная кнопка `Показать` расположена в заголовке блока параметров, а пустое состояние таблицы с подсказкой об обновлении содержит дополнительную кнопку `Показать расписание`. В дополнительных фильтрах доступен семестр из справочника `/api/schedule/semesters`, предназначенного только для чтения. Для текущего семестра сохраняется текущая двухнедельная точка просмотра, а при выборе другого семестра диапазон начинается с понедельника его первой недели. Frontend запрашивает две недели и собирает найденные расписания в переключатель результатов. На странице не выводится стек таблиц: виден один выбранный результат, а остальные доступны через чипы и кнопки предыдущего/следующего расписания; чипы результатов переносятся и отделены от счётчика стабильным отступом. Для режима кафедры и роли `DEPARTMENT` расписание ограничивается кафедрой пользователя; преподавательские и студенческие отдельные страницы пока остаются самостоятельными. Таблица строится как строки пар и столбцы дней недели. Нечётная неделя отображается в верхней половине ячейки, чётная — в нижней, а одинаковые занятия в обе недели схлопываются в цельную ячейку. Бейдж диапазона недель скрывается для занятий на весь семестр, а для занятий до конца семестра показывает только неделю начала в формате `(с 5 нед.)`. На мобильной ширине вместо широкой недельной матрицы показывается один день активного расписания с переключателем дней.