исправление хранения календаря

This commit is contained in:
Zuev
2026-07-31 21:24:11 +03:00
parent 87f3d98621
commit cc422255a2
31 changed files with 825 additions and 425 deletions

View File

@@ -2,7 +2,7 @@
## Назначение
Календарный учебный график хранит правила учебного года в той же логике, что и исходные Excel-таблицы: для каждого курса и каждой даты указывается код активности. Динамическое расписание использует этот график как фильтр: обычные пары выводятся только в дни теоретического обучения.
Календарный учебный график представляет правила учебного года в той же логике, что и исходные Excel-таблицы: для каждого курса и каждой даты указывается код активности. В базе последовательные даты с одинаковой активностью хранятся одним периодом. Динамическое расписание использует график как фильтр: обычные пары выводятся только в дни теоретического обучения.
График нужен, чтобы разные образовательные программы могли иметь разные периоды теории, экзаменов, практики, каникул и нерабочих дней. Несколько групп могут использовать один и тот же график, если они учатся по одной календарной модели.
@@ -18,7 +18,7 @@
- `specialty_profiles` хранит профили внутри специальности. У одной специальности может быть несколько профилей.
- `academic_calendars` хранит график профиля, формы обучения и учебного года.
- Форма обучения графика берётся из общего справочника `education_forms`; отдельной таблицы форм для календаря нет.
- `academic_calendar_days` хранит дневную сетку графика по курсу и дате.
- `academic_calendar_periods` хранит непересекающиеся периоды активности по курсу с включительными границами дат.
- `academic_calendar_subjects` хранит дисциплины графика по номерам учебных семестров; дисциплины выбираются из справочника `subjects`.
- `student_groups` связана со специальностью, профилем и той же таблицей форм обучения.
- `student_group_calendar_assignments` назначает группе конкретный график на учебный год.
@@ -52,7 +52,7 @@
1. Определяется учебный год и семестр даты.
2. Для группы находится назначение графика на этот учебный год.
3. По году начала обучения группы вычисляется текущий курс.
4. В `academic_calendar_days` ищется ячейка для курса и даты.
4. В `academic_calendar_periods` ищется период курса, содержащий дату.
5. Если код ячейки разрешает обычные пары, применяются правила `schedule_rules`.
6. Если код не разрешает пары, день пропускается и академические часы не списываются.
@@ -74,6 +74,8 @@
- Старый отдельный механизм праздников не используется: праздник хранится кодом `*` в дневной сетке.
- Дни вне учебного года или пустые периоды Excel хранятся кодом `=`.
- Дневной REST-контракт сохранён: при чтении backend разворачивает периоды в ячейки, а при полной замене объединяет соседние даты с одинаковой активностью в одну строку `academic_calendar_periods`.
- Отсутствие периода для даты означает отсутствие разрешённой активности; периоды одного курса и графика не могут пересекаться.
- График привязан к профилю обучения, а не только к коду специальности.
- Форма обучения графика должна совпадать с формой обучения группы при назначении графика.
- Название группы не является уникальным ключом; идентичные названия допустимы, если различаются другие параметры.

View File

@@ -551,8 +551,8 @@ GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
| `POST` | `/api/admin/academic-calendars` | Создать график |
| `PUT` | `/api/admin/academic-calendars/{id}` | Обновить график |
| `DELETE` | `/api/admin/academic-calendars/{id}` | Удалить график |
| `GET` | `/api/admin/academic-calendars/{id}/grid` | Дневная сетка графика |
| `PUT` | `/api/admin/academic-calendars/{id}/grid` | Полное сохранение дневной сетки |
| `GET` | `/api/admin/academic-calendars/{id}/grid` | Дневная сетка графика, развёрнутая из периодов |
| `PUT` | `/api/admin/academic-calendars/{id}/grid` | Полное сохранение дневной сетки с объединением периодов |
| `GET` | `/api/admin/academic-calendars/{id}/subjects` | Дисциплины графика по номерам учебных семестров |
| `PUT` | `/api/admin/academic-calendars/{id}/subjects` | Полная замена привязок дисциплин графика |
@@ -574,7 +574,7 @@ GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
Отдельного пользовательского названия у календарного графика нет.
При `PUT /api/admin/academic-calendars/{id}` backend блокирует график и повторно проверяет
все сохранённые назначения, дневную сетку и дисциплины. Изменение года, специальности,
все сохранённые назначения, периоды активности и дисциплины. Изменение года, специальности,
профиля, формы или количества курсов, которое сделает данные несовместимыми, возвращает
`409 Conflict` с русским сообщением и не изменяет график. В частности, нельзя уменьшить
`courseCount`, пока существуют строки старших курсов или дисциплины семестров выше
@@ -599,8 +599,12 @@ GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
`понедельник–воскресенье`. Неделя 1 содержит дату начала учебного года; дни этой недели
до начала года не входят в JSON и отображаются пустыми ячейками. Ключ
`(courseNumber, date)` не должен повторяться, каждый
`activityTypeId` или `activityCode` должен существовать. При любом `400` старая сетка
остаётся без изменений; `calendarId` из строки не переопределяет ID в URL.
`activityTypeId` или `activityCode` должен существовать. После проверки соседние даты
одного курса с одинаковой активностью объединяются в один период с включительными
границами. Разрыв хотя бы в один день или смена активности начинает новый период.
При `GET` сохранённые периоды разворачиваются обратно в тот же дневной JSON-контракт,
а `weekNumber` и `dayOfWeek` рассчитываются по дате. При любом `400` старая сетка остаётся
без изменений; `calendarId` из строки не переопределяет ID в URL.
**Привязка дисциплин к графику:**
```json

View File

@@ -317,10 +317,12 @@ pod не сохраняют два конфликтующих правила п
`AcademicCalendarController` делегирует полную замену дневной сетки
`AcademicCalendarGridService`. Публичный `replaceGrid()` проходит через транзакционный
Spring proxy: весь payload и все activity types проверяются до bulk delete, затем выполняются
`delete → flush → saveAll → flush`. Исключение не перехватывается внутри сервиса и вызывает
rollback. Инвалидация кэша зарегистрирована через transaction synchronization и выполняется
только после успешного commit.
Spring proxy: весь payload и все activity types проверяются до bulk delete, затем соседние
даты одного курса с одинаковой активностью сжимаются в периоды и выполняются
`delete → flush → saveAll → flush`. При чтении контроллер разворачивает интервалы обратно
в дневной REST-контракт и вычисляет номер недели и ISO-день. Исключение не перехватывается
внутри сервиса и вызывает rollback. Инвалидация кэша зарегистрирована через transaction
synchronization и выполняется только после успешного commit.
`ScheduleOverrideController` является HTTP-адаптером, а create/update/delete выполняет
`ScheduleOverrideService` через вызываемые Spring proxy-методы с `@Transactional`.
@@ -339,7 +341,8 @@ lost update или проверке устаревшего снимка.
`endDate - startDate + 1` одинаково в query- и generator-слоях.
`ScheduleQueryService` передаёт набор групп одним вызовом `buildScheduleForGroups()`.
`AcademicDateService` одним запросом загружает пересекающиеся семестры, затем batch-набор
назначений календарей и дневную сетку от начала затронутого семестра до конца диапазона.
назначений календарей и пересекающиеся периоды активности от начала затронутого семестра
до конца диапазона.
`ScheduleGeneratorService` отдельно batch-загружает правила и сетки звонков и строит lookup
по датам, группам, учебным годам, календарям и номерам пар. Снимки живут только во время
одного построения: изменения следующего запроса видны сразу, а singleton-кэш отсутствует.

View File

@@ -143,7 +143,7 @@ Bearer-токен проверяется на backend. Frontend-скрытие
| `specialty_profiles` | Профили обучения внутри специальности |
| `academic_calendars` | Календарный учебный график профиля, формы обучения и учебного года |
| `academic_calendar_activity_types` | Коды Excel-графика: `Т`, `Э`, `К`, `У`, `П`, `Пд`, `Н`, `Г`, `Д`, `ПА`, `С`, `*`, `=` |
| `academic_calendar_days` | Дневная сетка по курсу и дате |
| `academic_calendar_periods` | Непересекающиеся непрерывные периоды активности по курсу |
| `academic_calendar_subjects` | Дисциплины графика по номерам учебных семестров |
| `student_group_calendar_assignments` | Назначение конкретного графика группе на учебный год |
| `time_slots` | Настраиваемая сетка пар для тенанта |
@@ -165,12 +165,15 @@ Bearer-токен проверяется на backend. Frontend-скрытие
глобальные конкурентные гонки между разными backend-pod. Благодаря отсутствию пересечений
поиск семестра для даты возвращает не более одного результата и не зависит от порядка строк.
Полная замена `academic_calendar_days` выполняется через транзакционный
`AcademicCalendarGridService`. Сервис сначала проверяет и строит весь новый набор, включая
Полная замена дневной сетки выполняется через транзакционный
`AcademicCalendarGridService`. Сервис сначала проверяет весь дневной payload, включая
уникальность `(course, date)`, соответствие даты учебному году, номеру недели и ISO-дню,
и разрешает все коды активностей. Только после этого прежняя сетка удаляется и новый набор
записывается одной транзакцией. Ошибка любой строки сохраняет прежнюю сетку целиком, а кэш
расписания очищается только после commit. Недели дневной сетки выровнены по ISO-неделе
и разрешает все коды активностей. Затем соседние даты одного курса с одинаковой активностью
объединяются в один включительный период `academic_calendar_periods`. Только после этого
прежние периоды удаляются и новый набор записывается одной транзакцией. Ошибка любой строки
сохраняет прежнюю сетку целиком, а кэш расписания очищается только после commit. База
дополнительно запрещает пересекающиеся периоды одного курса и графика через exclusion
constraint. При чтении API разворачивает периоды обратно в дневные ячейки. Недели дневной сетки выровнены по ISO-неделе
`понедельник–воскресенье`: неделя 1 содержит первый день учебного года, а позиции до него
остаются пустыми. Поэтому при старте года во вторник следующий понедельник относится уже
к неделе 2.
@@ -189,10 +192,10 @@ Bearer-токен проверяется на backend. Frontend-скрытие
11. Применяет точечные изменения из `schedule_overrides` в расширенном поиске и отчётах.
Перед обходом дат генератор создаёт снимок на один запрос. Семестры диапазона, назначения
календарей всех выбранных групп, дневная сетка календарей, правила и эффективные сетки
звонков загружаются batch-запросами и индексируются по дате и идентификаторам. Поиск
семестра, проверка разрешённого дня и подстановка времени внутри циклов выполняются только
по lookup-картам. `ScheduleQueryService` передаёт все группы в один
календарей всех выбранных групп, пересекающиеся периоды активности, правила и эффективные
сетки звонков загружаются batch-запросами. Периоды группируются по календарю и курсу, а
поиск периода для даты выполняется бинарным поиском. Поиск семестра и подстановка времени
внутри циклов выполняются только по снимку запроса. `ScheduleQueryService` передаёт все группы в один
`buildScheduleForGroups()`, поэтому число запросов не растёт как
`дни × группы × правила`; singleton-кэш и общее между запросами состояние не используются.

View File

@@ -5,7 +5,7 @@
- **СУБД:** PostgreSQL (локально `postgres:16.3-alpine` с закреплённым digest, продакшн — managed PostgreSQL)
- **Управление схемой:** Flyway (программный запуск)
- **Hibernate DDL:** Отключён (`ddl-auto=none`)
- **Расширения:** `pgcrypto` (bcrypt-хеширование паролей), `btree_gist` (exclusion constraint временных слотов)
- **Расширения:** `pgcrypto` (bcrypt-хеширование паролей), `btree_gist` (exclusion constraints временных слотов и календарных периодов)
- **Мультитенантность:** Каждый тенант = отдельная БД
- **Абсолютное время:** `TIMESTAMPTZ`, Hibernate читает и записывает значения как UTC `Instant`
- **Календарные бизнес-даты:** `DATE`, интерпретируются в зоне `Europe/Moscow`
@@ -262,13 +262,12 @@ erDiagram
TIMESTAMPTZ updated_at
}
academic_calendar_days {
academic_calendar_periods {
BIGSERIAL id PK
BIGINT calendar_id FK
INT course_number
DATE date
INT week_number
INT day_of_week
DATE start_date
DATE end_date
BIGINT activity_type_id FK
}
@@ -375,10 +374,10 @@ erDiagram
specialties ||--o{ academic_calendars : "specialty_id"
specialty_profiles ||--o{ academic_calendars : "specialty_profile_id"
education_forms ||--o{ academic_calendars : "study_form_id"
academic_calendars ||--o{ academic_calendar_days : "calendar_id"
academic_calendars ||--o{ academic_calendar_periods : "calendar_id"
academic_calendars ||--o{ academic_calendar_subjects : "calendar_id"
academic_calendars ||--o{ student_group_calendar_assignments : "calendar_id"
academic_calendar_activity_types ||--o{ academic_calendar_days : "activity_type_id"
academic_calendar_activity_types ||--o{ academic_calendar_periods : "activity_type_id"
schedule_rules ||--o{ schedule_rule_groups : "schedule_rule_id"
schedule_rules ||--o{ schedule_rule_slots : "schedule_rule_id"
schedule_rule_slots ||--o{ schedule_rule_slot_subgroups : "schedule_rule_slot_id"
@@ -768,19 +767,22 @@ V1 создаёт GiST exclusion constraint `ex_academic_years_no_overlap` дл
группа стала несовместимой. Уменьшение `course_count` также запрещается, если в сетке
остаются строки старших курсов или дисциплины старших семестров.
#### `academic_calendar_days` — Дневная сетка календарного графика
#### `academic_calendar_periods` — Периоды активности календарного графика
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `calendar_id` | BIGINT FK → academic_calendars (CASCADE) | Календарный график |
| `course_number` | INT | Номер курса |
| `date` | DATE | Дата учебного года |
| `week_number` | INT | Номер периода `понедельник–воскресенье`; неделя 1 содержит начало учебного года |
| `day_of_week` | INT CHECK(1–7) | День недели ISO |
| `start_date` | DATE | Первая дата периода, включительно |
| `end_date` | DATE | Последняя дата периода, включительно |
| `activity_type_id` | BIGINT FK → academic_calendar_activity_types | Код активности |
Триггер `trg_calendar_days_dimensions` требует, чтобы `course_number` не превышал
`academic_calendars.course_count`, а дата находилась внутри учебного года графика.
Триггер `trg_calendar_periods_dimensions` требует, чтобы `course_number` не превышал
`academic_calendars.course_count`, обе границы находились внутри учебного года графика,
а `end_date` была не раньше `start_date`. Exclusion constraint
`ex_calendar_periods_no_overlap` запрещает пересечение периодов одного курса и графика.
Соседние даты с одинаковой активностью объединяет backend; номер недели и ISO-день
вычисляются при разворачивании периода в дневной REST-ответ и в таблице не дублируются.
#### `academic_calendar_subjects` — Дисциплины календарного графика
| Колонка | Тип | Описание |
@@ -921,12 +923,13 @@ lifecycle ресурсов, эффективная сетка целевого
| Файл | Описание |
|------|----------|
| `V1__init.sql` | Полная baseline-схема: справочники, роли, refresh-сессии JWT, PostgreSQL rate limit и аудит входа, lifecycle-поля, история кафедр, календарные графики с нумерацией недель `понедельник–воскресенье`, динамическое расписание, точечные изменения с переносом даты, seed, CHECK/UNIQUE/GiST-ограничения, конкурентно безопасные триггеры и комментарии |
| `V1__init.sql` | Полная baseline-схема: справочники, роли, refresh-сессии JWT, PostgreSQL rate limit и аудит входа, lifecycle-поля, история кафедр, календарные графики с интервальным хранением активностей и нумерацией недель `понедельник–воскресенье`, динамическое расписание, точечные изменения с переносом даты, seed, CHECK/UNIQUE/GiST-ограничения, конкурентно безопасные триггеры и комментарии |
### Этап разработки
По прямому решению владельца проекта разработческие миграции V2–V7 объединены в baseline
`V1`. Правильная нумерация недель календарного графика также входит непосредственно в V1.
`V1`. Интервальное хранение активностей и правильная нумерация недель календарного графика
также входят непосредственно в V1.
Перед применением этой редакции требуется полностью пустая tenant-схема.
### Полный сброс БД (локально)

View File

@@ -20,7 +20,7 @@
| `education_forms` | Общий справочник форм обучения для групп и календарных графиков |
| `academic_calendar_activity_types` | Коды активностей из Excel-графика |
| `academic_calendars` | Календарные графики профиля, формы обучения и учебного года |
| `academic_calendar_days` | Дневная сетка графика по курсу и дате |
| `academic_calendar_periods` | Непересекающиеся периоды активности графика по курсу |
| `student_group_calendar_assignments` | Назначения графиков группам по учебному году |
| `time_slots` | Настраиваемая сетка пар |
| `schedule_rules` | Правила проведения дисциплин с отдельными часами и стартовыми неделями лекций, лабораторных и практик |
@@ -28,11 +28,11 @@
| `schedule_rule_slots` | Шаблонные слоты правила |
| `schedule_rule_slot_subgroups` | Подгруппы лабораторного слота |
Праздничные и неучебные дни больше не хранятся отдельной сущностью: они задаются кодами `*` и `=` в дневной сетке.
Праздничные и неучебные дни больше не хранятся отдельной сущностью: они задаются кодами `*` и `=` в дневном редакторе, а в БД сохраняются непрерывными периодами.
## Backend
Добавлены сущности и репозитории для профилей, календарей, кодов активностей, дневной сетки и назначений группам.
Добавлены сущности и репозитории для профилей, календарей, кодов активностей, периодов активности и назначений группам. Дневной API сжимает соседние одинаковые ячейки при записи и разворачивает периоды при чтении.
Ключевые сервисы:

View File

@@ -188,7 +188,7 @@ frontend/
- Вкладка `auditorium-workload` стала общей вкладкой `Загруженность`: в поле «Что смотреть» выбираются аудитории, преподаватели или кафедры. Сводная матрица по выбранной дате использует одинаковую структуру: строки — выбранный тип сущности, столбцы — эффективные временные слоты дня из `/api/admin/time-slots/effective`, занятость собирается из динамического расписания `/api/schedule` по группам. Кафедральная матрица группирует занятия по кафедре преподавателя. Для аудиторий доступны фильтры корпуса, вместимости и оборудования. В поле «Отображение» можно выбрать конкретную аудиторию, преподавателя или кафедру; тогда сводная матрица заменяется одной таблицей по дням недели и времени для двухнедельного периода от выбранной даты. Таблица выбранной сущности растягивается до нижней части экрана. Ячейка делится вертикально только если верхняя и нижняя недели отличаются: нечётная неделя отображается сверху, чётная — снизу. Если состояние или занятие одинаковое, ячейка остаётся цельной. Чётность берётся из расписания, а для свободных дней рассчитывается по семестрам из `/api/admin/calendar/years`.
- Вкладка `university-structure` содержит внутренние разделы `Кафедры`, `Специальности` и `Профили`, оформленные тем же визуальным паттерном вкладок, что и `academic-calendar`: администратор создаёт профили как из общего списка, так и через кнопку `Профили` у конкретной специальности.
- Вкладка `schedule` имеет заголовок `Конструктор правил` и не обращается к старым `lessons` API. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы, отдельные часы и недели начала для лекций, лабораторных и практик, а также набор базовых слотов. Группы выбираются через выпадающий мультиселект. Поле подгруппы появляется только при выборе лабораторной работы; для лекций и практик оно не отображается. Если в правиле выбрана одна группа, селект подгруппы содержит пункт `Вся группа`; если выбрано несколько групп, лабораторный слот показывает мультиселект подгрупп, чтобы выбрать разные подгруппы разных групп. Типы занятий в слоте сортируются в порядке: лекция, лабораторная работа, практика. Единственный слот имеет действие `Очистить`, которое сбрасывает его поля; при наличии нескольких слотов у каждой строки показывается действие `Удалить`. Оба действия используют размер формы, минимальную ширину `125px` и высоту `44px`, поэтому совпадают по масштабу с соседними селектами. Список слотов отображается без внутреннего вертикального скролла: при добавлении строк форма расширяется вниз, а кнопка сохранения остаётся отдельным блоком под слотами. Из календарной системы здесь используется список семестров для выбора периода действия правила. Справа доступна сворачиваемая визуальная матрица: пользователь выбирает учебный год, семестр и группы, после чего матрица строится только по правилам выбранного семестра. Столбцы — выбранные в фильтре группы, строки — только день и время, где есть активные пары, ячейки показывают дисциплину, диапазон недель, тип, формат, преподавателя, аудиторию и подгруппы. Период недель не показывается для занятия на весь семестр; если занятие идёт до конца семестра не с первой недели, выводится только неделя начала в формате `(с 5 нед.)`, а ограниченный диапазон — как `(с 1 по 3 нед.)`. Если нечётная и чётная недели отличаются, ячейка делится на две половины; одинаковые занятия схлопываются в цельную ячейку. Кнопка с тремя точками в правой части карточки пары открывает контекстное меню: можно открыть полное правило в форме, изменить только день и базовую пару выбранного слота через компактную модалку или удалить правило целиком. В списке правил действия отображаются едиными кнопками одинакового размера с отступами между ними.
- Вкладка `academic-calendar` полностью отделяет календарную систему от расписания занятий и внутри себя разделена на три вкладки: `Графики` для учебных годов, семестров и карточек календарных графиков, `Сетки` для редактора дневной сетки, `Дисциплины` для ручной привязки дисциплин из `/api/subjects` к номерам учебных семестров графика. У календарного графика нет поля ввода и отдельного столбца названия; отображаемая подпись и сохраняемое backend название автоматически собираются как `код специальности — профиль обучения — форма обучения — учебный год`. Frontend не отправляет `title` в payload. Администратор выбирает форму обучения из общего справочника `/api/education-forms`, заполняет дневную сетку по курсам и кодам активностей, назначает ручную временную сетку на конкретную дату, а сохранение сетки идёт через `/api/admin/academic-calendars/{id}/grid`. Все шесть полей дат принимают до восьми цифр, автоматически показывают их в формате `ДД.ММ.ГГГГ`, ограничивают год четырьмя цифрами и перед отправкой преобразуют значение в ISO `ГГГГ-ММ-ДД`.
- Вкладка `academic-calendar` полностью отделяет календарную систему от расписания занятий и внутри себя разделена на три вкладки: `Графики` для учебных годов, семестров и карточек календарных графиков, `Сетки` для редактора дневной сетки, `Дисциплины` для ручной привязки дисциплин из `/api/subjects` к номерам учебных семестров графика. У календарного графика нет поля ввода и отдельного столбца названия; отображаемая подпись и сохраняемое backend название автоматически собираются как `код специальности — профиль обучения — форма обучения — учебный год`. Frontend не отправляет `title` в payload. Администратор выбирает форму обучения из общего справочника `/api/education-forms`, заполняет дневную сетку по курсам и кодам активностей, назначает ручную временную сетку на конкретную дату, а сохранение сетки идёт через `/api/admin/academic-calendars/{id}/grid`. REST-контракт остаётся дневным: backend объединяет соседние даты с одинаковой активностью в периоды при записи и разворачивает их при чтении. Все шесть полей дат принимают до восьми цифр, автоматически показывают их в формате `ДД.ММ.ГГГГ`, ограничивают год четырьмя цифрами и перед отправкой преобразуют значение в ISO `ГГГГ-ММ-ДД`.
- При сохранении правила во вкладке `schedule` frontend различает `409 Conflict` от остальных ошибок API. Если backend возвращает `conflictRule`, открывается широкое модальное окно разрешения конфликта: пользователь видит новое и ранее созданное правило, чипы причины конфликта из `conflictReasons` и подсветку соответствующих полей (`teacher`, `classroom`, `group`). Пользователь меняет у конфликтующего правила день, чётность, пару, преподавателя или аудиторию, после чего frontend сохраняет конфликтующее правило через `PUT /api/admin/schedule-rules/{id}` и автоматически повторяет исходный `POST` или `PUT`. Если после переноса появляется следующий конфликт, показывается следующее конфликтующее правило без потери исходного черновика.
- Редактор годового графика во вкладке `academic-calendar` показывает компактную табличную сетку: курсы раскрываются отдельными секциями со стрелкой, семестры внутри курса на широком экране идут рядом в две равные колонки одинаковой высоты, столбцы подписаны номерами недель учебного года, строки — днями недели, а ячейки содержат буквенный код активности. Недели выровнены по интервалам `понедельник–воскресенье`: если учебный год начинается не в понедельник, предшествующие позиции первой недели остаются пустыми, а следующий понедельник открывает неделю 2. В рамках одного курса семестровые таблицы получают одинаковое число недельных колонок: недостающие колонки заполняются пустыми ячейками, поэтому левая и правая части занимают всю ширину секции курса без внешней пустоты. Подробная расшифровка и изменение кода открываются в компактном модальном окне по клику на ячейку; при наведении возле курсора отображается кастомная подсказка с датой, курсом, кодом активности и временной сеткой. У границ viewport подсказка автоматически раскрывается в противоположную сторону и остаётся полностью видимой, а код активности в ней центрируется внутри квадратного индикатора. Ячейки можно выделять протяжкой мышью и менять код активности через ту же модалку, выбранные ячейки подсвечиваются мягкой заливкой, а кнопка «Применить» закрывает окно.
- Вкладка `classrooms` теперь показывает архивные аудитории через `includeArchived=true`. Кнопка удаления заменена на архивирование: аудитория выводится из эксплуатации, но остаётся в историческом расписании. Для архивных аудиторий доступно восстановление.

View File

@@ -21,7 +21,7 @@
- `specialties` и `specialty_profiles` определяют образовательную программу группы.
- `academic_calendars` хранит график профиля и формы обучения.
- `academic_calendar_activity_types` хранит коды из Excel-графика: `Т`, `Э`, `К`, `У`, `П`, `Пд`, `Н`, `Г`, `Д`, `ПА`, `С`, `*`, `=`.
- `academic_calendar_days` хранит дневную сетку по курсу и дате.
- `academic_calendar_periods` хранит непрерывные непересекающиеся периоды активности по курсу.
- `student_group_calendar_assignments` выбирает график для группы на учебный год.
### Слой правил расписания
@@ -47,4 +47,4 @@
## Административный сценарий
Администратор создаёт специальность и профиль, затем создаёт группу с этой связкой. В разделе расписания он создаёт учебный год, семестры и календарный график, заполняет дневную сетку кодами активностей и назначает график группе. После сохранения графика или назначения кэш генератора очищается, поэтому следующие запросы расписания используют новые правила сразу.
Администратор создаёт специальность и профиль, затем создаёт группу с этой связкой. В разделе расписания он создаёт учебный год, семестры и календарный график, заполняет дневную сетку кодами активностей и назначает график группе. Backend объединяет соседние одинаковые дни в периоды, не меняя дневной интерфейс. После сохранения графика или назначения кэш генератора очищается, поэтому следующие запросы расписания используют новые правила сразу.

View File

@@ -1,6 +1,6 @@
# Задачи: динамическое расписание и календарный учебный график
Актуальная декомпозиция относится к модели, где календарный учебный график хранится дневной сеткой и назначается группе на учебный год.
Актуальная декомпозиция относится к модели, где календарный учебный график редактируется дневной сеткой, хранится непрерывными периодами активности и назначается группе на учебный год.
## Выполнено
@@ -8,7 +8,7 @@
- [x] Добавлены специальности, профили обучения и связь группы с профилем.
- [x] Добавлены формы обучения календарного графика.
- [x] Добавлен справочник кодов активностей Excel-графика.
- [x] Добавлены календарные графики, дневная сетка и назначения графиков группам.
- [x] Добавлены календарные графики, интервальное хранение дневной сетки и назначения графиков группам.
- [x] Обновлён `AcademicDateService`: активность дня определяется по назначенному группе графику.
- [x] Обновлён `ScheduleGeneratorService`: обычные пары генерируются только на разрешённых днях.
- [x] В `V1__init.sql` добавлены отдельные часы и недели начала для лекций, лабораторных и практик.