Compare commits
1 Commits
main
...
course-sem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e82ed69639 |
@@ -1,85 +0,0 @@
|
|||||||
---
|
|
||||||
name: AutoUpdateDocs
|
|
||||||
description: Автоматическое обновление документации проекта после изменений в коде
|
|
||||||
---
|
|
||||||
|
|
||||||
# Скилл: Автоматическое обновление документации
|
|
||||||
|
|
||||||
## Когда активировать
|
|
||||||
|
|
||||||
Этот скилл **ДОЛЖЕН** выполняться автоматически после любых изменений, затрагивающих:
|
|
||||||
|
|
||||||
- **Контроллеры** (`backend/src/main/java/com/magistr/app/controller/`) → обновить `docs/API.md`
|
|
||||||
- **Модели или миграции** (`model/`, `db/migration/`) → обновить `docs/DATABASE.md`
|
|
||||||
- **Конфигурация тенантов** (`config/tenant/`) → обновить `docs/ARCHITECTURE.md`
|
|
||||||
- **Бизнес-правила или валидаторы** (`utils/`) → обновить `docs/BUSINESS_LOGIC.md`
|
|
||||||
- **Frontend** (`frontend/`) → обновить `docs/FRONTEND.md`
|
|
||||||
- **Docker/Kubernetes** (`compose.yaml`, `Dockerfile`, `../k8s/`) → обновить `docs/INFRASTRUCTURE.md`
|
|
||||||
- **Code style или структура пакетов** → обновить `docs/DEVELOPMENT.md`
|
|
||||||
- **Общая структура проекта** → обновить `docs/README.md`
|
|
||||||
|
|
||||||
## Карта соответствия «файл → документация»
|
|
||||||
|
|
||||||
| Изменённый файл/директория | Файл документации |
|
|
||||||
|----------------------------|-------------------|
|
|
||||||
| `controller/*Controller.java` | `docs/API.md` |
|
|
||||||
| `db/migration/V*__.sql` | `docs/DATABASE.md` |
|
|
||||||
| `model/*.java` | `docs/DATABASE.md` |
|
|
||||||
| `dto/*.java` | `docs/API.md` |
|
|
||||||
| `config/tenant/*.java` | `docs/ARCHITECTURE.md` |
|
|
||||||
| `utils/*.java` | `docs/BUSINESS_LOGIC.md` |
|
|
||||||
| `frontend/admin/js/views/*.js` | `docs/FRONTEND.md` |
|
|
||||||
| `frontend/admin/css/*.css` | `docs/FRONTEND.md` |
|
|
||||||
| `compose.yaml`, `Dockerfile` | `docs/INFRASTRUCTURE.md` |
|
|
||||||
| `application.properties` | `docs/ARCHITECTURE.md` |
|
|
||||||
|
|
||||||
## Пошаговая инструкция
|
|
||||||
|
|
||||||
### 1. Определить затронутые файлы документации
|
|
||||||
|
|
||||||
После выполнения задачи пользователя — проверить по таблице выше, какие файлы документации нужно обновить.
|
|
||||||
|
|
||||||
### 2. Прочитать текущую документацию
|
|
||||||
|
|
||||||
Открыть соответствующий файл из `docs/` и найти секцию, которую нужно обновить.
|
|
||||||
|
|
||||||
### 3. Внести точечные изменения
|
|
||||||
|
|
||||||
Обновить **только затронутые секции**, не переписывая весь файл. Примеры:
|
|
||||||
|
|
||||||
#### Новый контроллер → `docs/API.md`
|
|
||||||
Добавить новую секцию с описанием эндпоинтов:
|
|
||||||
- Метод + URL
|
|
||||||
- Тело запроса (JSON пример)
|
|
||||||
- Ответ (JSON пример)
|
|
||||||
- Валидация
|
|
||||||
|
|
||||||
#### Новая миграция → `docs/DATABASE.md`
|
|
||||||
- Добавить новую таблицу в ER-диаграмму (Mermaid)
|
|
||||||
- Добавить описание таблицы и колонок
|
|
||||||
- Добавить запись в таблицу «Текущие миграции»
|
|
||||||
|
|
||||||
#### Новый view → `docs/FRONTEND.md`
|
|
||||||
- Добавить в дерево файлов
|
|
||||||
- Добавить в таблицу «Разделы админ-панели»
|
|
||||||
|
|
||||||
### 4. Обновить AGENTS.md (при необходимости)
|
|
||||||
|
|
||||||
Если изменения затрагивают:
|
|
||||||
- Структуру директорий → обновить дерево в `AGENTS.md`
|
|
||||||
- Критические правила (Flyway, новые ограничения) → обновить секцию «Критические правила»
|
|
||||||
|
|
||||||
### 5. Сообщить пользователю
|
|
||||||
|
|
||||||
В конце ответа кратко упомянуть, какие файлы документации были обновлены:
|
|
||||||
|
|
||||||
> 📝 Обновлена документация: `docs/API.md` (добавлен эндпоинт `POST /api/absences`)
|
|
||||||
|
|
||||||
## Правила
|
|
||||||
|
|
||||||
1. **Язык:** Вся документация на русском языке
|
|
||||||
2. **Формат:** Сохранять существующий стиль оформления файла (заголовки, таблицы, примеры кода)
|
|
||||||
3. **Не удалять:** Не удалять существующие секции без явного запроса пользователя
|
|
||||||
4. **Mermaid:** При изменении схемы БД — обязательно обновлять ER-диаграмму в `docs/DATABASE.md`
|
|
||||||
5. **Минимальные правки:** Не переписывать весь файл ради добавления одной строки — использовать точечные изменения
|
|
||||||
6. **Консистентность:** Если одно и то же понятие упоминается в нескольких файлах `docs/`, обновить все вхождения
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
# Концепция динамической генерации расписания
|
|
||||||
|
|
||||||
Данный документ представляет собой подробное архитектурное описание новой системы управления расписанием. Система переходит от статического хранения каждой отдельной пары к параметрическому: мы сохраняем **правила проведения** дисциплины и **календарную сетку**, а фактическое расписание на любую дату вычисляется «на лету» (генерируется).
|
|
||||||
|
|
||||||
> **Контекст миграции:** Новая система полностью заменяет существующие таблицы `lessons` (статическое расписание) и `schedule_data` (плановая нагрузка). Обе таблицы будут мигрированы в единую модель `schedule_rules` + `schedule_rule_slots`, которая совмещает хранение нагрузки (часы) и расписания (слоты) в одной структуре.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Подробное описание компонентов системы
|
|
||||||
|
|
||||||
Новая архитектура строится на строгом разделении данных на три логических слоя: Календарь (основа отсчета времени), Правила (шаблоны занятий) и Генератор (движок рендеринга фактического расписания).
|
|
||||||
|
|
||||||
### 1.1 Справочная база времени (Календарный учебный график)
|
|
||||||
Чтобы система понимала, *когда* можно ставить пары, а когда нет, вводится понятие календарного графика. Он состоит из трёх взаимосвязанных сущностей:
|
|
||||||
|
|
||||||
* **Академические периоды (Учебные года и Семестры).** Иерархия из двух уровней:
|
|
||||||
* **Учебный год** — контейнер с названием и датами (напр. «2024/2025», `01.09.2024` — `30.06.2025`).
|
|
||||||
* **Семестр** — дочерняя сущность учебного года. Содержит дату начала, от которой отсчитывается «Неделя 1» данного семестра. Нумерация недель начинается заново для каждого семестра. Тип семестра (`autumn` / `spring`) определяет, какой набор правил активен.
|
|
||||||
|
|
||||||
Именно от даты начала семестра отсчитывается «Неделя 1». Конвенция чётности (верхняя = чётная или нечётная) **настраивается на уровне тенанта**, так как у разных университетов разные традиции. Это избавляет систему от уязвимостей, связанных с плавающими днями начала учёбы, високосными годами и смещениями дней недели.
|
|
||||||
|
|
||||||
* **Справочник исключений (Праздники и Выходные).** В этой таблице хранятся конкретные даты `YYYY-MM-DD`, когда университет юридически или физически закрыт (например, государственные праздники). Если по правилу пара должна быть в этот день, алгоритм будет знать, что его нужно пропустить без штрафов и ошибок.
|
|
||||||
|
|
||||||
* **Матрица учебного графика.** Это цифровая копия эксель-таблицы (`Курс + Специальность` → `Номер недели` → `Тип деятельности`). Привязка идёт к `course_number` + `specialty_id`, а **не** к конкретной группе, так как учебный график одинаков для всех групп одного курса одной специальности. Номер текущего курса группы вычисляется из поля `year_start_study` модели `StudentGroup` относительно текущей даты по формуле: `course = текущий_учебный_год - year_start_study + 1`. Типы деятельности включают `THEORY` (Теория, пары идут в штатном режиме), `EXAM` (Э — экзаменационная сессия), `VACATION` (К — каникулы), `PRACTICE` (У, П — практика). Если, например, у 3-го курса на 18-й неделе стоит статус `EXAM`, алгоритм даже не будет пытаться генерировать для них теоретические лекции, а отобразит блок «Экзаменационная сессия».
|
|
||||||
|
|
||||||
### 1.2 Справочник временных слотов (Time Slots)
|
|
||||||
Вместо хардкода фиксированных 7 пар, система хранит временные слоты в отдельной **настраиваемой таблице**. Каждый тенант (университет) может иметь собственное количество пар, их длительность и временные рамки.
|
|
||||||
|
|
||||||
Слот содержит:
|
|
||||||
* `order_number` — порядковый номер пары в дне (1, 2, 3...).
|
|
||||||
* `start_time` — время начала (напр. `08:00`).
|
|
||||||
* `end_time` — время окончания (напр. `09:30`).
|
|
||||||
* `duration_minutes` — длительность пары в минутах.
|
|
||||||
|
|
||||||
Это позволяет каждому университету настраивать количество и продолжительность пар без модификации кода.
|
|
||||||
|
|
||||||
### 1.3 Движок правил (Schedule Rules)
|
|
||||||
Старый подход подразумевал, что каждая пара в базе (каждая клеточка) — это изолированная запись `lessons` («понедельник, 1-я пара, математика»). Новая система вводит сущность сводного **Правила Дисциплины**. Одно правило описывает расписание целого курса по конкретному предмету для одной или нескольких студенческих групп (включая потоковые лекции).
|
|
||||||
|
|
||||||
**Базовые параметры (Лимиты Правила):**
|
|
||||||
* `subject_id` — ID преподаваемой дисциплины.
|
|
||||||
* `semester_id` — ID семестра, к которому привязано правило. Одна и та же дисциплина может читаться в разных семестрах с разными параметрами.
|
|
||||||
* `startDate` — Дата или номер недели семестра, с которой предмет начинает читаться (поскольку не все предметы идут строго с 1-й недели семестра).
|
|
||||||
* `totalHours` — Полный объём выделенных **академических часов** (1 ак. час = 45 минут; одна пара = 2 ак. часа). Это важнейший **лимитатор**, который обеспечивает автоматическую остановку генерации: как только заявленные часы будут вычитаны, предмет перестает отображаться в расписании студентов на последующих неделях.
|
|
||||||
|
|
||||||
**Связь с группами (Many-to-Many):**
|
|
||||||
Одно правило может быть связано с несколькими группами через промежуточную таблицу `schedule_rule_groups`. Это обеспечивает поддержку **потоковых лекций** — когда один преподаватель читает лекцию нескольким группам одновременно в одной аудитории. При этом правило создаётся один раз, а группы к нему привязываются списком.
|
|
||||||
|
|
||||||
**Массив паттернов (Слоты правила):**
|
|
||||||
Само «тело» правила разбивается на подчинённые слоты. Если предмет идёт в Пн и Ср, это будет 2 слота внутри одного Правила. Слот содержит:
|
|
||||||
* `dayOfWeek`: день недели (1–7, Пн–Вс).
|
|
||||||
* `parity`: тип четности — `ENUM('BOTH', 'EVEN', 'ODD')`. `BOTH` — каждую неделю, `EVEN` — по чётным (нижним) неделям, `ODD` — по нечётным (верхним). Конкретное соответствие «чётная = верхняя или нижняя» определяется настройкой тенанта.
|
|
||||||
* `time_slot_id`: FK на таблицу `time_slots` — порядковый номер и время пары.
|
|
||||||
* `subgroup_id`: FK на подгруппу (NULL = вся группа). *Это гарантирует, что мы сможем ставить разным подгруппам пересекающиеся занятия в разных аудиториях без алгоритмических конфликтов.*
|
|
||||||
* `teacher_id`: FK на преподавателя слота.
|
|
||||||
* `classroom_id`: FK на аудиторию слота.
|
|
||||||
* `lesson_type_id`: FK на тип занятия (`Лекция`, `Практическая работа`, `Лабораторная работа`).
|
|
||||||
* `lesson_format`: формат проведения (`Очно` / `Онлайн`).
|
|
||||||
|
|
||||||
> **Обоснование:** Хранение `teacher_id`, `classroom_id`, `lesson_type_id` и `lesson_format` в **слотах**, а не в главном правиле, позволяет гибко описывать ситуации вроде: лекции в понедельник читает лектор Иванов (Аудитория 100), а лабораторные в среду ведёт практик Петров (Аудитория 102В) — в рамках одного правила по предмету «Программирование», расходуя общий `totalHours`.
|
|
||||||
|
|
||||||
### 1.4 Генератор (Рендерер) расписания
|
|
||||||
Это слой бизнес-логики (служба `ScheduleGeneratorService` в Java), который работает исключительно в оперативной памяти бэкенда и производит расчёт расписания «on-demand» (по требованию) при запросе от клиента фронтенда.
|
|
||||||
|
|
||||||
**Пошаговый алгоритм работы генератора:**
|
|
||||||
1. Фронтенд (Интерфейс пользователя) запрашивает: *«Дай мне расписание группы ИТ-21 на конкретный период, например, с 14 октября по 20 октября»*.
|
|
||||||
2. Генератор определяет семестр по запрошенным датам и вычисляет, что 14 октября соответствует, к примеру, 7-й неделе семестра (вычисление от `startDate` семестра).
|
|
||||||
3. Он сверяется с *Матрицей учебного графика*. Для этого генератор определяет текущий курс группы по формуле `текущий_учебный_год - year_start_study + 1` и находит `specialty_id` группы. Если у данного курса/специальности сейчас стоит `VACATION` (Каникулы) или `PRACTICE` (Практика), генератор сразу возвращает пустой ответ или ответ со статусом периода.
|
|
||||||
4. Если статус недели позволяет проводить занятия (`THEORY`), генератор поднимает из Базы Данных все активные **Правила** для запрошенной группы (через таблицу `schedule_rule_groups`), привязанные к текущему семестру.
|
|
||||||
5. **Механика Лимитатора часов:** Для каждого правила алгоритм «симулирует» прогон времени с даты старта правила до текущей запрошенной недели. Он подсчитывает количество успешно проведённых ак. часов (по 2 ак. часа за каждый отработанный слот), пропуская даты, попавшие в справочник праздников, и недели с типом деятельности отличным от `THEORY`.
|
|
||||||
6. Если у правила лимит `totalHours` достиг значения `0`, программа понимает, что курс вычитан, и предмет не отображается. Если часы ещё остались, алгоритм проецирует шаблоны (слоты правила) на запрошенную текущую неделю с учётом чётности, аудиторий и подгрупп, отдавая готовый JSON-массив в браузер пользователя.
|
|
||||||
|
|
||||||
**Генерация расписания для преподавателя:**
|
|
||||||
Аналогричный алгоритм, но поиск правил идёт не по привязке к группе, а по `teacher_id` в слотах. Генератор собирает все `schedule_rule_slots`, где `teacher_id` = ID текущего преподавателя, получает родительские правила и рендерит расписание, обогащая каждую запись списком групп из `schedule_rule_groups`.
|
|
||||||
|
|
||||||
**Кеширование:**
|
|
||||||
Для оптимизации производительности (т.к. симуляция прогона за весь семестр для каждого запроса ресурсоёмка) предусмотрен кеш:
|
|
||||||
* Список праздников текущего учебного года кешируется при первом обращении и инвалидируется при изменении таблицы `holidays`.
|
|
||||||
* Матрица учебного графика кешируется по ключу `(course, specialty_id, semester_id)`.
|
|
||||||
* Результаты подсчёта `consumed_hours` для каждого правила могут кешироваться с инвалидацией при изменении праздников или правил.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Архитектурные Решения
|
|
||||||
|
|
||||||
На основе обсуждений были задокументированы следующие концептуальные решения по архитектуре:
|
|
||||||
|
|
||||||
1. **Реакция на праздники (Продление курса):**
|
|
||||||
Алгоритм воспринимает праздник как «пропуск хода», не отнимая проведённые часы от `totalHours`. Это означает, что пара **не переносится** на другой день или время — она просто пропускается без вычета часов. Фактически предмет будет отображаться в расписании дольше (больше недель), пока `totalHours` не будет полностью исчерпан. Преподаватель честно выработает положенный объём часов за счёт увеличения количества недель преподавания.
|
|
||||||
|
|
||||||
2. **Нормализация через связанные таблицы:**
|
|
||||||
Мы не используем сырые массивы (`INTEGER[]`) или JSONB-колонки. Реализована структура со строгой нормализацией:
|
|
||||||
* Главная таблица: `schedule_rules` (хранит лимиты и дату старта).
|
|
||||||
* Подчинённая таблица: `schedule_rule_slots` (хранит конкретный день, чётность, номер пары, преподавателя, аудиторию, тип и формат — прикреплённые к ID главного правила через Foreign Key).
|
|
||||||
* Связующая таблица: `schedule_rule_groups` (Many-to-Many между правилом и группами).
|
|
||||||
Это позволяет базе данных строить сложные выборки в стиле «Покажи загруженность кабинета №21 во вторник на второй паре по чётным неделям», исключая тяжёлый парсинг JSON.
|
|
||||||
|
|
||||||
3. **Поддержка подгрупп внутри слотов:**
|
|
||||||
В таблицу `schedule_rule_slots` введено поле `subgroup_id` (Id подгруппы, nullable). Алгоритм генератора сможет рендерить два предмета для одной группы одновременно и без конфликтов, если они ассоциированы с разными подгруппами одной материнской группы.
|
|
||||||
|
|
||||||
4. **Обогащённые слоты (Вариант Б):**
|
|
||||||
`teacher_id`, `classroom_id`, `lesson_type_id` и `lesson_format` хранятся в каждой строке `schedule_rule_slots`, а не в главном правиле. Это позволяет описывать лекции и практики одного предмета в рамках одного правила, расходуя общий `totalHours`.
|
|
||||||
|
|
||||||
5. **Потоковые лекции через Many-to-Many:**
|
|
||||||
Одно правило связывается с несколькими группами через `schedule_rule_groups`. Для потоковой лекции создаётся одно правило, к которому привязываются все участвующие группы.
|
|
||||||
|
|
||||||
6. **Настраиваемость по тенантам:**
|
|
||||||
Архитектурно все тенанты одинаковы — каждый университет получает идентичную пустую базу данных. Временные слоты (количество, длительность, время начала/окончания пар), конвенция чётности и прочие параметры не требуют специального механизма: каждый университет просто заполняет свою БД самостоятельно через панель администратора.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Подробный План Действий по Реализации
|
|
||||||
|
|
||||||
Интеграция новой архитектуры затронет весь стек приложения (DB → Backend → API → Frontend). Работу предлагается вести строго поэтапно:
|
|
||||||
|
|
||||||
### Этап 1. База Данных (Flyway Миграции)
|
|
||||||
|
|
||||||
**Схема Временных слотов:**
|
|
||||||
* `time_slots` (id, order_number, start_time TIME, end_time TIME, duration_minutes INT).
|
|
||||||
* Заполняется администратором. Нет фиксированных значений — каждый тенант настраивает свою сетку пар.
|
|
||||||
|
|
||||||
**Схема Календарного графика:**
|
|
||||||
* `academic_years` (id, title VARCHAR, start_date DATE, end_date DATE).
|
|
||||||
* `semesters` (id, academic_year_id FK, semester_type ENUM('autumn','spring'), start_date DATE, end_date DATE).
|
|
||||||
* Именно от `semesters.start_date` отсчитывается «Неделя 1».
|
|
||||||
* `holidays` (id, date DATE, academic_year_id FK, description VARCHAR).
|
|
||||||
* `academic_calendar_matrix` (id, semester_id FK, course_number INT, specialty_id FK, week_number INT, activity_type ENUM('THEORY','EXAM','VACATION','PRACTICE')).
|
|
||||||
* Привязка к `course_number` + `specialty_id`, а НЕ к конкретной группе.
|
|
||||||
|
|
||||||
**Схема Движка Правил:**
|
|
||||||
* `schedule_rules` (id, subject_id FK, semester_id FK, active_from_date DATE, total_academic_hours INT).
|
|
||||||
* `total_academic_hours` — в академических часах (1 ак. час = 45 мин, одна пара = 2 ак. часа).
|
|
||||||
* `schedule_rule_groups` (schedule_rule_id FK, group_id FK) — PK составной.
|
|
||||||
* Связующая таблица для потоковых лекций.
|
|
||||||
* `schedule_rule_slots` (id, schedule_rule_id FK, day_of_week INT CHECK(1–7), parity ENUM('BOTH','EVEN','ODD'), time_slot_id FK, subgroup_id FK NULL, teacher_id FK, classroom_id FK, lesson_type_id FK, lesson_format VARCHAR).
|
|
||||||
|
|
||||||
**Скрипт Миграции (Data ETL):** Написание SQL/Java скрипта для миграции данных из двух источников:
|
|
||||||
1. **Из `schedule_data`** → `schedule_rules` + `schedule_rule_groups`: перенос плановой нагрузки (`number_of_hours` → `total_academic_hours`, `group_id`, `subjects_id`, `teacher_id`, `lesson_type_id`, `is_division`, `semester_type`, `period`).
|
|
||||||
2. **Из `lessons`** → `schedule_rule_slots`: перенос расписания с трансформацией данных:
|
|
||||||
* `day` (строка «Понедельник»...«Суббота») → `day_of_week` (INT 1–6).
|
|
||||||
* `time` (строка «8:00 - 9:30») → `time_slot_id` (FK на `time_slots`).
|
|
||||||
* `week` (строка «Верхняя»/«Нижняя»/«Обе») → `parity` (ENUM `ODD`/`EVEN`/`BOTH`).
|
|
||||||
* Группировка записей с одинаковым `(subject_id, group_id)` в одно правило.
|
|
||||||
|
|
||||||
После успешной миграции и верификации данных — удаление таблиц `lessons` и `schedule_data`.
|
|
||||||
|
|
||||||
### Этап 2. Бэкенд и Вычислительное Ядро (Java + Spring Boot)
|
|
||||||
* `AcademicDateService.java` — сервис утилит для календарной математики:
|
|
||||||
* Перевод дат в номер недели семестра.
|
|
||||||
* Определение чётности недели с учётом настройки тенанта.
|
|
||||||
* Проверка попадания дня в справочник `holidays`.
|
|
||||||
* Вычисление текущего курса группы: `текущий_учебный_год - year_start_study + 1`.
|
|
||||||
* `ScheduleRuleRepository.java` — JPA репозитории для извлечения графа правил из базы данных, с оптимизацией N+1 проблемы через `JOIN FETCH` со слотами и группами.
|
|
||||||
* `ScheduleGeneratorService.java` — Сердце системы. Основные методы:
|
|
||||||
* `List<RenderedLesson> buildScheduleForGroup(Long groupId, LocalDate startDate, LocalDate endDate)` — расписание группы.
|
|
||||||
* `List<RenderedLesson> buildScheduleForTeacher(Long teacherId, LocalDate startDate, LocalDate endDate)` — расписание преподавателя (поиск по `teacher_id` в слотах, обогащение информацией о группах).
|
|
||||||
* Реализует всю бизнес-логику из пункта 1.4 (подсчёт вычитанных часов, пропуск праздников, кеширование).
|
|
||||||
* Адаптация валидаторов пересечения аудиторий: теперь валидатор должен работать не на уровне «каждой пары», а симулировать весь семестр на этапе сохранения нового Правила в панели администратора.
|
|
||||||
|
|
||||||
### Этап 3. Обновление REST API (Контроллеры)
|
|
||||||
* **Новый эндпоинт расписания:** `GET /api/schedule` переходит на диапазонную модель. Параметры: `?groupId=123&startDate=2024-10-14&endDate=2024-10-20` или `?teacherId=456&startDate=...&endDate=...`. Ответ — массив объектов с полными датами `YYYY-MM-DD`.
|
|
||||||
* **Обратная совместимость:** Старый эндпоинт `GET /api/users/lessons` будет помечен как `@Deprecated` и продолжит работать до полной миграции фронтенда. После завершения миграции фронтенда — удаление.
|
|
||||||
* **CRUD-контроллеры для админки:**
|
|
||||||
* `/api/admin/time-slots` (настройка сетки временных слотов).
|
|
||||||
* `/api/admin/calendar/years` (учебные годы и семестры).
|
|
||||||
* `/api/admin/calendar/matrix` (настройка каникул и сессий по курсам/специальностям/неделям).
|
|
||||||
* `/api/admin/calendar/holidays` (добавление исключений).
|
|
||||||
* `/api/admin/schedule-rules` (управление жизненным циклом Правил, их слотами и привязкой к группам).
|
|
||||||
|
|
||||||
### Этап 4. Интерфейсы Frontend (Vanilla JS + HTML)
|
|
||||||
* **Страницы просмотра (Студенты и Преподаватели):**
|
|
||||||
* Реализация переключателя календарных дат (Date Picker или кнопки-перелистывания недель).
|
|
||||||
* Логика, которая при свайпе или клике запрашивает у API конкретный диапазон дат и перерисовывает DOM-дерево.
|
|
||||||
* Для преподавателей — отображение всех групп, привязанных к каждому занятию.
|
|
||||||
* **Панель Администратора (SPA-интерфейсы):**
|
|
||||||
* **Вкладка «Временные слоты»:** Настройка сетки пар — количество, время начала/окончания, длительность.
|
|
||||||
* **Вкладка «Учебный график»:** Визуальная сетка-матрица (недели по горизонтали, Курсы/Специальности по вертикали), где админ может закрашивать пересечения разными цветами, назначая статусы (Практика, Каникулы, Теория, Экзамены).
|
|
||||||
* **Вкладка «Конструктор Правил»:** Глобально новый визуальный инструмент расписания. Админ выбирает Группы (одну или несколько для потока) и Дисциплину, задаёт `totalHours` в академических часах, а затем динамически добавляет строчки массива слотов через кнопку «Добавить занятие» со списками (Selects) для Дня Недели, Временного слота, Подгруппы, Чётности, Преподавателя, Аудитории и Типа занятия.
|
|
||||||
@@ -1,218 +0,0 @@
|
|||||||
# 📋 Задачи: Динамическая генерация расписания
|
|
||||||
|
|
||||||
> Декомпозиция [`SCHEDULE_PROPOSAL.md`](SCHEDULE_PROPOSAL.md) на подзадачи для доски планирования.
|
|
||||||
> Категории: **Backend**, **Frontend**, **DevOps/DB**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## DevOps / Database
|
|
||||||
|
|
||||||
### Flyway-миграция: Временные слоты
|
|
||||||
- [ ] Создать миграцию: таблица `time_slots` (id, order_number, start_time, end_time, duration_minutes)
|
|
||||||
- [ ] Добавить CHECK-ограничения (start_time < end_time, duration_minutes > 0, order_number > 0)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Flyway-миграция: Учебные годы и семестры
|
|
||||||
- [ ] Создать миграцию: таблица `academic_years` (id, title, start_date, end_date)
|
|
||||||
- [ ] Создать миграцию: таблица `semesters` (id, academic_year_id FK, semester_type ENUM, start_date, end_date)
|
|
||||||
- [ ] Добавить CHECK-ограничения и индексы
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Flyway-миграция: Праздники
|
|
||||||
- [ ] Создать миграцию: таблица `holidays` (id, date, academic_year_id FK, description)
|
|
||||||
- [ ] Добавить уникальный индекс на (date, academic_year_id)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Flyway-миграция: Матрица учебного графика
|
|
||||||
- [ ] Создать миграцию: таблица `academic_calendar_matrix` (id, semester_id FK, course_number, specialty_id FK, week_number, activity_type ENUM)
|
|
||||||
- [ ] Добавить ENUM: `THEORY`, `EXAM`, `VACATION`, `PRACTICE`
|
|
||||||
- [ ] Добавить уникальный индекс на (semester_id, course_number, specialty_id, week_number)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Flyway-миграция: Правила расписания
|
|
||||||
- [ ] Создать миграцию: таблица `schedule_rules` (id, subject_id FK, semester_id FK, active_from_date, total_academic_hours)
|
|
||||||
- [ ] Создать миграцию: связующая таблица `schedule_rule_groups` (schedule_rule_id FK, group_id FK, PK составной)
|
|
||||||
- [ ] Создать миграцию: таблица `schedule_rule_slots` (id, schedule_rule_id FK, day_of_week, parity ENUM, time_slot_id FK, subgroup_id FK NULL, teacher_id FK, classroom_id FK, lesson_type_id FK, lesson_format)
|
|
||||||
- [ ] Добавить CHECK на day_of_week (1–7)
|
|
||||||
- [ ] Добавить ENUM: `BOTH`, `EVEN`, `ODD`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ETL-миграция данных
|
|
||||||
- [ ] Написать SQL/Java скрипт миграции `schedule_data` → `schedule_rules` + `schedule_rule_groups`
|
|
||||||
- [ ] Маппинг `number_of_hours` → `total_academic_hours`
|
|
||||||
- [ ] Маппинг привязок групп
|
|
||||||
- [ ] Написать SQL/Java скрипт миграции `lessons` → `schedule_rule_slots`
|
|
||||||
- [ ] Трансформация `day` (строка) → `day_of_week` (INT 1–6)
|
|
||||||
- [ ] Трансформация `time` (строка) → `time_slot_id` (FK)
|
|
||||||
- [ ] Трансформация `week` (строка) → `parity` (ENUM)
|
|
||||||
- [ ] Группировка записей с одинаковым (subject_id, group_id) в одно правило
|
|
||||||
- [ ] Верификация мигрированных данных (количество записей, целостность FK)
|
|
||||||
- [ ] Создать миграцию на удаление устаревших таблиц `lessons` и `schedule_data` (после верификации)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Backend (Java + Spring Boot)
|
|
||||||
|
|
||||||
### JPA-сущности (Model)
|
|
||||||
- [ ] Создать Entity: `TimeSlot`
|
|
||||||
- [ ] Создать Entity: `AcademicYear`
|
|
||||||
- [ ] Создать Entity: `Semester` (связь ManyToOne → AcademicYear)
|
|
||||||
- [ ] Создать Entity: `Holiday` (связь ManyToOne → AcademicYear)
|
|
||||||
- [ ] Создать Entity: `AcademicCalendarMatrix` (связи на Semester, Specialty)
|
|
||||||
- [ ] Создать Entity: `ScheduleRule` (связи на Subject, Semester)
|
|
||||||
- [ ] Создать Entity: `ScheduleRuleSlot` (связи на ScheduleRule, TimeSlot, Teacher, Classroom, LessonType)
|
|
||||||
- [ ] Настроить ManyToMany-связь ScheduleRule ↔ StudentGroup через `schedule_rule_groups`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### DTO
|
|
||||||
- [ ] Создать DTO: `TimeSlotDto`
|
|
||||||
- [ ] Создать DTO: `AcademicYearDto`, `SemesterDto`
|
|
||||||
- [ ] Создать DTO: `HolidayDto`
|
|
||||||
- [ ] Создать DTO: `AcademicCalendarMatrixDto`
|
|
||||||
- [ ] Создать DTO: `ScheduleRuleDto`, `ScheduleRuleSlotDto`
|
|
||||||
- [ ] Создать DTO: `RenderedLessonDto` (ответ генератора расписания)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Repository
|
|
||||||
- [ ] Создать `TimeSlotRepository`
|
|
||||||
- [ ] Создать `AcademicYearRepository`
|
|
||||||
- [ ] Создать `SemesterRepository` (метод findByDateRange)
|
|
||||||
- [ ] Создать `HolidayRepository` (метод findByAcademicYearId)
|
|
||||||
- [ ] Создать `AcademicCalendarMatrixRepository` (метод findBySemesterAndCourseAndSpecialty)
|
|
||||||
- [ ] Создать `ScheduleRuleRepository` с JOIN FETCH (решение N+1 проблемы)
|
|
||||||
- [ ] Метод: findByGroupIdAndSemesterId (через schedule_rule_groups)
|
|
||||||
- [ ] Метод: findByTeacherIdAndSemesterId (через schedule_rule_slots.teacher_id)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Сервис: AcademicDateService
|
|
||||||
- [ ] Метод: перевод произвольной даты → номер недели семестра
|
|
||||||
- [ ] Метод: определение чётности недели с учётом настройки тенанта
|
|
||||||
- [ ] Метод: проверка попадания даты в справочник `holidays`
|
|
||||||
- [ ] Метод: вычисление текущего курса группы (`текущий_учебный_год - year_start_study + 1`)
|
|
||||||
- [ ] Метод: определение семестра по дате
|
|
||||||
- [ ] Написать юнит-тесты для AcademicDateService
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Сервис: ScheduleGeneratorService
|
|
||||||
- [ ] Метод: `buildScheduleForGroup(groupId, startDate, endDate)` — расписание группы
|
|
||||||
- [ ] Определение семестра по диапазону дат
|
|
||||||
- [ ] Вычисление номера недели и курса группы
|
|
||||||
- [ ] Проверка типа деятельности через матрицу графика
|
|
||||||
- [ ] Загрузка активных правил для группы
|
|
||||||
- [ ] Симуляция прогона часов (подсчёт consumed_hours)
|
|
||||||
- [ ] Пропуск праздников при подсчёте часов
|
|
||||||
- [ ] Проекция слотов на запрошенную неделю с учётом чётности и подгрупп
|
|
||||||
- [ ] Метод: `buildScheduleForTeacher(teacherId, startDate, endDate)` — расписание преподавателя
|
|
||||||
- [ ] Поиск правил по teacher_id в слотах
|
|
||||||
- [ ] Обогащение ответа списком групп из schedule_rule_groups
|
|
||||||
- [ ] Написать юнит-тесты для ScheduleGeneratorService
|
|
||||||
- [ ] Написать интеграционные тесты (полный цикл с тестовой БД)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Кеширование
|
|
||||||
- [ ] Реализовать кеш списка праздников по учебному году
|
|
||||||
- [ ] Реализовать кеш матрицы учебного графика по ключу (course, specialty_id, semester_id)
|
|
||||||
- [ ] Реализовать кеш consumed_hours для каждого правила
|
|
||||||
- [ ] Реализовать инвалидацию кеша праздников при CRUD-операциях с holidays
|
|
||||||
- [ ] Реализовать инвалидацию кеша consumed_hours при изменении правил или праздников
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Валидация
|
|
||||||
- [ ] Адаптировать валидатор пересечения аудиторий (симуляция всего семестра при сохранении правила)
|
|
||||||
- [ ] Валидация пересечения преподавателей (один преподаватель не может вести две пары одновременно)
|
|
||||||
- [ ] Валидация пересечения групп (одна группа не может быть на двух занятиях одновременно, кроме подгрупп)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### REST API: Контроллеры
|
|
||||||
- [ ] `GET /api/schedule` — Новый эндпоинт расписания (параметры: groupId/teacherId + startDate + endDate)
|
|
||||||
- [ ] Пометить `GET /api/users/lessons` как `@Deprecated` (обратная совместимость)
|
|
||||||
- [ ] CRUD: `POST/GET/PUT/DELETE /api/admin/time-slots`
|
|
||||||
- [ ] CRUD: `POST/GET/PUT/DELETE /api/admin/calendar/years`
|
|
||||||
- [ ] CRUD: `GET/PUT /api/admin/calendar/semesters` (вложены в years)
|
|
||||||
- [ ] CRUD: `POST/GET/PUT/DELETE /api/admin/calendar/holidays`
|
|
||||||
- [ ] CRUD: `GET/PUT /api/admin/calendar/matrix` (массовое сохранение матрицы)
|
|
||||||
- [ ] CRUD: `POST/GET/PUT/DELETE /api/admin/schedule-rules`
|
|
||||||
- [ ] Включая вложенные слоты и привязку групп
|
|
||||||
- [ ] Написать интеграционные тесты для API
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Удаление устаревшего кода
|
|
||||||
- [ ] Удалить/рефакторить старый `LessonsController` (после миграции фронтенда)
|
|
||||||
- [ ] Удалить/рефакторить старый `ScheduleDataController`
|
|
||||||
- [ ] Удалить старые Entity: `Lesson`, `ScheduleData`
|
|
||||||
- [ ] Удалить старые Repository и Service для lessons/schedule_data
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Frontend (Vanilla JS + HTML/CSS)
|
|
||||||
|
|
||||||
### Просмотр расписания: Студенты
|
|
||||||
- [ ] Реализовать переключатель дат (Date Picker / кнопки-стрелки по неделям)
|
|
||||||
- [ ] Переключить API-запросы на новый `GET /api/schedule?groupId=...&startDate=...&endDate=...`
|
|
||||||
- [ ] Рендеринг расписания по дням и временным слотам
|
|
||||||
- [ ] Отображение статуса периода (Каникулы / Практика / Экзамены), если неделя не учебная
|
|
||||||
- [ ] Отображение информации о подгруппах (два занятия рядом для разных подгрупп)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Просмотр расписания: Преподаватели
|
|
||||||
- [ ] Реализовать переключатель дат (Date Picker / кнопки-стрелки по неделям)
|
|
||||||
- [ ] Переключить API-запросы на новый `GET /api/schedule?teacherId=...&startDate=...&endDate=...`
|
|
||||||
- [ ] Отображение всех групп, привязанных к каждому занятию
|
|
||||||
- [ ] Отображение подгрупп, если преподаватель ведёт у подгруппы
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Панель администратора: Вкладка «Временные слоты»
|
|
||||||
- [ ] Создать UI-страницу настройки временных слотов
|
|
||||||
- [ ] CRUD-интерфейс: добавление/редактирование/удаление пар
|
|
||||||
- [ ] Отображение таблицы: номер пары → время начала → время окончания → длительность
|
|
||||||
- [ ] Валидация на фронтенде (пересечение времён, корректность данных)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Панель администратора: Вкладка «Учебный график»
|
|
||||||
- [ ] Создать UI: выбор учебного года и семестра
|
|
||||||
- [ ] Создать UI: CRUD учебных годов и семестров
|
|
||||||
- [ ] Создать UI: CRUD праздников (список дат с описанием)
|
|
||||||
- [ ] Создать визуальную сетку-матрицу:
|
|
||||||
- [ ] Горизонтальная ось — номера недель
|
|
||||||
- [ ] Вертикальная ось — Курс + Специальность
|
|
||||||
- [ ] Цветовая кодировка ячеек: Теория/Экзамены/Каникулы/Практика
|
|
||||||
- [ ] Клик/драг для массового назначения статуса
|
|
||||||
- [ ] Сохранение матрицы через API `PUT /api/admin/calendar/matrix`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Панель администратора: Вкладка «Конструктор Правил»
|
|
||||||
- [ ] Создать UI: список существующих правил с фильтрацией (по группе, предмету, семестру)
|
|
||||||
- [ ] Форма создания/редактирования правила:
|
|
||||||
- [ ] Мультиселект групп (для потоковых лекций)
|
|
||||||
- [ ] Выбор дисциплины (subject)
|
|
||||||
- [ ] Выбор семестра
|
|
||||||
- [ ] Ввод totalHours (академические часы)
|
|
||||||
- [ ] Ввод даты начала (active_from_date)
|
|
||||||
- [ ] Динамический массив слотов (кнопка «Добавить занятие»):
|
|
||||||
- [ ] Select: День недели
|
|
||||||
- [ ] Select: Временной слот (из таблицы time_slots)
|
|
||||||
- [ ] Select: Чётность (Обе/Чётная/Нечётная)
|
|
||||||
- [ ] Select: Подгруппа (опционально)
|
|
||||||
- [ ] Select: Преподаватель
|
|
||||||
- [ ] Select: Аудитория
|
|
||||||
- [ ] Select: Тип занятия (Лекция/Практика/Лаба)
|
|
||||||
- [ ] Select: Формат (Очно/Онлайн)
|
|
||||||
- [ ] Визуальное предупреждение при конфликтах (аудитория/преподаватель уже заняты)
|
|
||||||
- [ ] Удаление правила с подтверждением
|
|
||||||
@@ -6,14 +6,12 @@ import com.magistr.app.model.EducationForm;
|
|||||||
import com.magistr.app.model.StudentGroup;
|
import com.magistr.app.model.StudentGroup;
|
||||||
import com.magistr.app.repository.EducationFormRepository;
|
import com.magistr.app.repository.EducationFormRepository;
|
||||||
import com.magistr.app.repository.GroupRepository;
|
import com.magistr.app.repository.GroupRepository;
|
||||||
import com.magistr.app.utils.CourseAndSemesterCalculator;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
import java.time.Year;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
@@ -41,21 +39,18 @@ public class GroupController {
|
|||||||
List<StudentGroup> groups = groupRepository.findAll();
|
List<StudentGroup> groups = groupRepository.findAll();
|
||||||
|
|
||||||
List<GroupResponse> response = groups.stream()
|
List<GroupResponse> response = groups.stream()
|
||||||
.map(g -> {
|
.map(g -> new GroupResponse(
|
||||||
int course = CourseAndSemesterCalculator.getActualCourse(g.getYearStartStudy());
|
|
||||||
int semester = CourseAndSemesterCalculator.getActualSemester(g.getYearStartStudy());
|
|
||||||
return new GroupResponse(
|
|
||||||
g.getId(),
|
g.getId(),
|
||||||
g.getName(),
|
g.getName(),
|
||||||
g.getGroupSize(),
|
g.getGroupSize(),
|
||||||
g.getEducationForm().getId(),
|
g.getEducationForm().getId(),
|
||||||
g.getEducationForm().getName(),
|
g.getEducationForm().getName(),
|
||||||
g.getDepartmentId(),
|
g.getDepartmentId(),
|
||||||
course,
|
g.getEnrollmentYear(),
|
||||||
semester,
|
g.getCourse(),
|
||||||
|
g.getSemester(),
|
||||||
g.getSpecialityCode()
|
g.getSpecialityCode()
|
||||||
);
|
))
|
||||||
})
|
|
||||||
.toList();
|
.toList();
|
||||||
logger.info("Получено {} групп", response.size());
|
logger.info("Получено {} групп", response.size());
|
||||||
return response;
|
return response;
|
||||||
@@ -77,27 +72,9 @@ public class GroupController {
|
|||||||
.body("Группы для указанной кафедры не найдены");
|
.body("Группы для указанной кафедры не найдены");
|
||||||
}
|
}
|
||||||
|
|
||||||
List<GroupResponse> response = groups.stream()
|
logger.info("Найдено {} групп для кафедры с ID - {}", groups.size(), departmentId);
|
||||||
.map(g -> {
|
|
||||||
int course = CourseAndSemesterCalculator.getActualCourse(g.getYearStartStudy());
|
|
||||||
int semester = CourseAndSemesterCalculator.getActualSemester(g.getYearStartStudy());
|
|
||||||
return new GroupResponse(
|
|
||||||
g.getId(),
|
|
||||||
g.getName(),
|
|
||||||
g.getGroupSize(),
|
|
||||||
g.getEducationForm().getId(),
|
|
||||||
g.getEducationForm().getName(),
|
|
||||||
g.getDepartmentId(),
|
|
||||||
course,
|
|
||||||
semester,
|
|
||||||
g.getSpecialityCode()
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
logger.info("Найдено {} групп для кафедры с ID - {}", response.size(), departmentId);
|
return ResponseEntity.ok(groups);
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Получена ошибка при получении списка групп для кафедры с ID - {}: {}", departmentId, e.getMessage());
|
logger.error("Получена ошибка при получении списка групп для кафедры с ID - {}: {}", departmentId, e.getMessage());
|
||||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
@@ -107,8 +84,8 @@ public class GroupController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<?> createGroup(@RequestBody CreateGroupRequest request) {
|
public ResponseEntity<?> createGroup(@RequestBody CreateGroupRequest request) {
|
||||||
logger.info("Получен запрос на создание новой группы: name = {}, groupSize = {}, educationFormId = {}, departmentId = {}, yearStartStudy = {}",
|
logger.info("Получен запрос на создание новой группы: name = {}, groupSize = {}, educationFormId = {}, departmentId = {}, enrollmentYear = {}",
|
||||||
request.getName(), request.getGroupSize(), request.getEducationFormId(), request.getDepartmentId(), request.getYearStartStudy());
|
request.getName(), request.getGroupSize(), request.getEducationFormId(), request.getDepartmentId(), request.getEnrollmentYear());
|
||||||
try {
|
try {
|
||||||
if (request.getName() == null || request.getName().isBlank()) {
|
if (request.getName() == null || request.getName().isBlank()) {
|
||||||
String errorMessage = "Название группы обязательно";
|
String errorMessage = "Название группы обязательно";
|
||||||
@@ -135,12 +112,7 @@ public class GroupController {
|
|||||||
logger.error("Ошибка валидации: {}", errorMessage);
|
logger.error("Ошибка валидации: {}", errorMessage);
|
||||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
}
|
}
|
||||||
// if (request.getCourse() == null || request.getCourse() == 0) {
|
if (request.getEnrollmentYear() == null || request.getEnrollmentYear() == 0) {
|
||||||
// String errorMessage = "Курс обязателен";
|
|
||||||
// logger.error("Ошибка валидации: {}", errorMessage);
|
|
||||||
// return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
|
||||||
// }
|
|
||||||
if (request.getYearStartStudy() == null || request.getYearStartStudy() == 0) {
|
|
||||||
String errorMessage = "Год начала обучения обязателен";
|
String errorMessage = "Год начала обучения обязателен";
|
||||||
logger.error("Ошибка валидации: {}", errorMessage);
|
logger.error("Ошибка валидации: {}", errorMessage);
|
||||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
@@ -161,7 +133,7 @@ public class GroupController {
|
|||||||
group.setGroupSize(request.getGroupSize());
|
group.setGroupSize(request.getGroupSize());
|
||||||
group.setEducationForm(efOpt.get());
|
group.setEducationForm(efOpt.get());
|
||||||
group.setDepartmentId(request.getDepartmentId());
|
group.setDepartmentId(request.getDepartmentId());
|
||||||
group.setYearStartStudy(request.getYearStartStudy());
|
group.setEnrollmentYear(request.getEnrollmentYear());
|
||||||
group.setSpecialityCode(request.getSpecialityCode());
|
group.setSpecialityCode(request.getSpecialityCode());
|
||||||
groupRepository.save(group);
|
groupRepository.save(group);
|
||||||
|
|
||||||
@@ -174,7 +146,9 @@ public class GroupController {
|
|||||||
group.getEducationForm().getId(),
|
group.getEducationForm().getId(),
|
||||||
group.getEducationForm().getName(),
|
group.getEducationForm().getName(),
|
||||||
group.getDepartmentId(),
|
group.getDepartmentId(),
|
||||||
group.getYearStartStudy(),
|
group.getEnrollmentYear(),
|
||||||
|
group.getCourse(),
|
||||||
|
group.getSemester(),
|
||||||
group.getSpecialityCode()));
|
group.getSpecialityCode()));
|
||||||
} catch (Exception e ) {
|
} catch (Exception e ) {
|
||||||
logger.error("Ошибка при создании группы: {}", e.getMessage(), e);
|
logger.error("Ошибка при создании группы: {}", e.getMessage(), e);
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import com.magistr.app.dto.CreateScheduleDataRequest;
|
|||||||
import com.magistr.app.dto.ScheduleResponse;
|
import com.magistr.app.dto.ScheduleResponse;
|
||||||
import com.magistr.app.model.*;
|
import com.magistr.app.model.*;
|
||||||
import com.magistr.app.repository.*;
|
import com.magistr.app.repository.*;
|
||||||
import com.magistr.app.utils.CourseAndSemesterCalculator;
|
|
||||||
import com.magistr.app.utils.SemesterTypeValidator;
|
import com.magistr.app.utils.SemesterTypeValidator;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
@@ -48,6 +47,7 @@ public class ScheduleDataController {
|
|||||||
.map(s -> new ScheduleData(
|
.map(s -> new ScheduleData(
|
||||||
s.getId(),
|
s.getId(),
|
||||||
s.getDepartmentId(),
|
s.getDepartmentId(),
|
||||||
|
s.getSemester(),
|
||||||
s.getGroupId(),
|
s.getGroupId(),
|
||||||
s.getSubjectsId(),
|
s.getSubjectsId(),
|
||||||
s.getLessonTypeId(),
|
s.getLessonTypeId(),
|
||||||
@@ -90,20 +90,12 @@ public class ScheduleDataController {
|
|||||||
.map(StudentGroup::getName)
|
.map(StudentGroup::getName)
|
||||||
.orElse("Неизвестно");
|
.orElse("Неизвестно");
|
||||||
|
|
||||||
int groupSemester = 0;
|
Integer groupCourse = groupRepository.findById(s.getGroupId())
|
||||||
int groupCourse = 0;
|
.map(StudentGroup::getCourse)
|
||||||
|
.orElse(null);
|
||||||
|
|
||||||
String specialityCode = "Неизвестно";
|
String specialityCode = "Неизвестно";
|
||||||
|
|
||||||
StudentGroup group = groupRepository.findById(s.getGroupId()).orElse(null);
|
StudentGroup group = groupRepository.findById(s.getGroupId()).orElse(null);
|
||||||
|
|
||||||
if (group != null) {
|
|
||||||
groupCourse = CourseAndSemesterCalculator.getFutureCourse(group.getYearStartStudy(), period);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group != null) {
|
|
||||||
groupSemester = CourseAndSemesterCalculator.getFutureSemester(group.getYearStartStudy(), period, semesterType);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
Long specialityId = group.getSpecialityCode();
|
Long specialityId = group.getSpecialityCode();
|
||||||
specialityCode = specialtiesRepository.findById(specialityId).
|
specialityCode = specialtiesRepository.findById(specialityId).
|
||||||
@@ -131,9 +123,9 @@ public class ScheduleDataController {
|
|||||||
s.getId(),
|
s.getId(),
|
||||||
s.getDepartmentId(),
|
s.getDepartmentId(),
|
||||||
specialityCode,
|
specialityCode,
|
||||||
|
s.getSemester(),
|
||||||
groupName,
|
groupName,
|
||||||
groupCourse,
|
groupCourse,
|
||||||
groupSemester,
|
|
||||||
subjectName,
|
subjectName,
|
||||||
lessonType,
|
lessonType,
|
||||||
s.getNumberOfHours(),
|
s.getNumberOfHours(),
|
||||||
@@ -156,8 +148,8 @@ public class ScheduleDataController {
|
|||||||
// Доделать проверки получаемых полей!!!
|
// Доделать проверки получаемых полей!!!
|
||||||
@PostMapping("/create")
|
@PostMapping("/create")
|
||||||
public ResponseEntity<?> createScheduleData(@RequestBody CreateScheduleDataRequest request) {
|
public ResponseEntity<?> createScheduleData(@RequestBody CreateScheduleDataRequest request) {
|
||||||
logger.info("Получен запрос на создание записи данных для расписаний: departmentId={}, groupId={}, subjectsId={}, lessonTypeId={}, numberOfHours={}, division={}, teacherId={}, semesterType={}, period={}",
|
logger.info("Получен запрос на создание записи данных для расписаний: departmentId={}, semester={}, groupId={}, subjectsId={}, lessonTypeId={}, numberOfHours={}, division={}, teacherId={}, semesterType={}, period={}",
|
||||||
request.getDepartmentId(), request.getGroupId(), request.getSubjectsId(), request.getLessonTypeId(), request.getNumberOfHours(), request.getDivision(), request.getTeacherId(), request.getSemesterType(), request.getPeriod());
|
request.getDepartmentId(), request.getSemester(), request.getGroupId(), request.getSubjectsId(), request.getLessonTypeId(), request.getNumberOfHours(), request.getDivision(), request.getTeacherId(), request.getSemesterType(), request.getPeriod());
|
||||||
try {
|
try {
|
||||||
if (request.getDepartmentId() == null || request.getDepartmentId() == 0) {
|
if (request.getDepartmentId() == null || request.getDepartmentId() == 0) {
|
||||||
String errorMessage = "ID кафедры обязателен";
|
String errorMessage = "ID кафедры обязателен";
|
||||||
@@ -169,6 +161,16 @@ public class ScheduleDataController {
|
|||||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (request.getSemester() == null || request.getSemester() == 0) {
|
||||||
|
String errorMessage = "Семестр обязателен";
|
||||||
|
logger.info("Ошибка валидации: {}", errorMessage);
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
} else if(request.getSemester() > 12) {
|
||||||
|
String errorMessage = "Семестр должен быть меньше или равен 12";
|
||||||
|
logger.info("Семестр должен быть меньше или равен 12");
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
if (request.getGroupId() == null || request.getGroupId() == 0) {
|
if (request.getGroupId() == null || request.getGroupId() == 0) {
|
||||||
String errorMessage = "ID группы обязателен";
|
String errorMessage = "ID группы обязателен";
|
||||||
logger.info("Ошибка валидации: {}", errorMessage);
|
logger.info("Ошибка валидации: {}", errorMessage);
|
||||||
@@ -213,8 +215,9 @@ public class ScheduleDataController {
|
|||||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean existsRecord = scheduleDataRepository.existsByDepartmentIdAndGroupIdAndSubjectsIdAndLessonTypeIdAndNumberOfHoursAndDivisionAndTeacherIdAndSemesterTypeAndPeriod(
|
boolean existsRecord = scheduleDataRepository.existsByDepartmentIdAndSemesterAndGroupIdAndSubjectsIdAndLessonTypeIdAndNumberOfHoursAndDivisionAndTeacherIdAndSemesterTypeAndPeriod(
|
||||||
request.getDepartmentId(),
|
request.getDepartmentId(),
|
||||||
|
request.getSemester(),
|
||||||
request.getGroupId(),
|
request.getGroupId(),
|
||||||
request.getSubjectsId(),
|
request.getSubjectsId(),
|
||||||
request.getLessonTypeId(),
|
request.getLessonTypeId(),
|
||||||
@@ -232,6 +235,7 @@ public class ScheduleDataController {
|
|||||||
|
|
||||||
ScheduleData scheduleData = new ScheduleData();
|
ScheduleData scheduleData = new ScheduleData();
|
||||||
scheduleData.setDepartmentId(request.getDepartmentId());
|
scheduleData.setDepartmentId(request.getDepartmentId());
|
||||||
|
scheduleData.setSemester(request.getSemester());
|
||||||
scheduleData.setGroupId(request.getGroupId());
|
scheduleData.setGroupId(request.getGroupId());
|
||||||
scheduleData.setSubjectsId(request.getSubjectsId());
|
scheduleData.setSubjectsId(request.getSubjectsId());
|
||||||
scheduleData.setLessonTypeId(request.getLessonTypeId());
|
scheduleData.setLessonTypeId(request.getLessonTypeId());
|
||||||
@@ -246,6 +250,7 @@ public class ScheduleDataController {
|
|||||||
Map<String, Object> response = new LinkedHashMap<>();
|
Map<String, Object> response = new LinkedHashMap<>();
|
||||||
response.put("id", savedSchedule.getId());
|
response.put("id", savedSchedule.getId());
|
||||||
response.put("departmentId", savedSchedule.getDepartmentId());
|
response.put("departmentId", savedSchedule.getDepartmentId());
|
||||||
|
response.put("semester", savedSchedule.getSemester());
|
||||||
response.put("groupId", savedSchedule.getGroupId());
|
response.put("groupId", savedSchedule.getGroupId());
|
||||||
response.put("subjectId", savedSchedule.getSubjectsId());
|
response.put("subjectId", savedSchedule.getSubjectsId());
|
||||||
response.put("lessonTypeId", savedSchedule.getLessonTypeId());
|
response.put("lessonTypeId", savedSchedule.getLessonTypeId());
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ public class CreateGroupRequest {
|
|||||||
private Long groupSize;
|
private Long groupSize;
|
||||||
private Long educationFormId;
|
private Long educationFormId;
|
||||||
private Long departmentId;
|
private Long departmentId;
|
||||||
private Integer yearStartStudy;
|
private Integer enrollmentYear;
|
||||||
private Long specialityCode;
|
private Long specialityCode;
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@@ -41,12 +41,12 @@ public class CreateGroupRequest {
|
|||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getYearStartStudy() {
|
public Integer getEnrollmentYear() {
|
||||||
return yearStartStudy;
|
return enrollmentYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setYearStartStudy(Integer yearStartStudy) {
|
public void setEnrollmentYear(Integer enrollmentYear) {
|
||||||
this.yearStartStudy = yearStartStudy;
|
this.enrollmentYear = enrollmentYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getSpecialityCode() {
|
public Long getSpecialityCode() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.magistr.app.model.SemesterType;
|
|||||||
public class CreateScheduleDataRequest {
|
public class CreateScheduleDataRequest {
|
||||||
private Long id;
|
private Long id;
|
||||||
private Long departmentId;
|
private Long departmentId;
|
||||||
|
private Long semester;
|
||||||
private Long groupId;
|
private Long groupId;
|
||||||
private Long subjectsId;
|
private Long subjectsId;
|
||||||
private Long lessonTypeId;
|
private Long lessonTypeId;
|
||||||
@@ -30,6 +31,14 @@ public class CreateScheduleDataRequest {
|
|||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getSemester() {
|
||||||
|
return semester;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSemester(Long semester) {
|
||||||
|
this.semester = semester;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getGroupId() {
|
public Long getGroupId() {
|
||||||
return groupId;
|
return groupId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package com.magistr.app.dto;
|
package com.magistr.app.dto;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
|
||||||
|
|
||||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
|
||||||
public class GroupResponse {
|
public class GroupResponse {
|
||||||
|
|
||||||
private Long id;
|
private Long id;
|
||||||
@@ -11,34 +8,27 @@ public class GroupResponse {
|
|||||||
private Long educationFormId;
|
private Long educationFormId;
|
||||||
private String educationFormName;
|
private String educationFormName;
|
||||||
private Long departmentId;
|
private Long departmentId;
|
||||||
private Integer yearStartStudy;
|
private Integer enrollmentYear;
|
||||||
private Integer course;
|
private Integer course;
|
||||||
private Integer semester;
|
private Integer semester;
|
||||||
private Long specialityCode;
|
private Long specialityCode;
|
||||||
|
|
||||||
public GroupResponse(Long id, String name, Long groupSize, Long educationFormId, String educationFormName, Long departmentId, Integer course, Integer semester, Long specialityCode) {
|
public GroupResponse(Long id, String name, Long groupSize, Long educationFormId,
|
||||||
|
String educationFormName, Long departmentId,
|
||||||
|
Integer enrollmentYear, Integer course, Integer semester,
|
||||||
|
Long specialityCode) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.groupSize = groupSize;
|
this.groupSize = groupSize;
|
||||||
this.educationFormId = educationFormId;
|
this.educationFormId = educationFormId;
|
||||||
this.educationFormName = educationFormName;
|
this.educationFormName = educationFormName;
|
||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
|
this.enrollmentYear = enrollmentYear;
|
||||||
this.course = course;
|
this.course = course;
|
||||||
this.semester = semester;
|
this.semester = semester;
|
||||||
this.specialityCode = specialityCode;
|
this.specialityCode = specialityCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public GroupResponse(Long id, String name, Long groupSize, Long educationFormId, String educationFormName, Long departmentId, Integer yearStartStudy, Long specialityCode) {
|
|
||||||
this.id = id;
|
|
||||||
this.name = name;
|
|
||||||
this.groupSize = groupSize;
|
|
||||||
this.educationFormId = educationFormId;
|
|
||||||
this.educationFormName = educationFormName;
|
|
||||||
this.departmentId = departmentId;
|
|
||||||
this.yearStartStudy = yearStartStudy;
|
|
||||||
this.specialityCode = specialityCode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
@@ -63,6 +53,10 @@ public class GroupResponse {
|
|||||||
return departmentId;
|
return departmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getEnrollmentYear() {
|
||||||
|
return enrollmentYear;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getCourse() {
|
public Integer getCourse() {
|
||||||
return course;
|
return course;
|
||||||
}
|
}
|
||||||
@@ -71,10 +65,6 @@ public class GroupResponse {
|
|||||||
return semester;
|
return semester;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getYearStartStudy() {
|
|
||||||
return yearStartStudy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getSpecialityCode() {
|
public Long getSpecialityCode() {
|
||||||
return specialityCode;
|
return specialityCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ public class ScheduleResponse {
|
|||||||
private Long id;
|
private Long id;
|
||||||
private String specialityCode;
|
private String specialityCode;
|
||||||
private Long departmentId;
|
private Long departmentId;
|
||||||
|
private Long semester;
|
||||||
private Long groupId;
|
private Long groupId;
|
||||||
private String groupName;
|
private String groupName;
|
||||||
private Integer groupCourse;
|
private Integer groupCourse;
|
||||||
private Integer groupSemester;
|
|
||||||
private Long subjectsId;
|
private Long subjectsId;
|
||||||
private String subjectName;
|
private String subjectName;
|
||||||
private Long lessonTypeId;
|
private Long lessonTypeId;
|
||||||
@@ -24,9 +24,10 @@ public class ScheduleResponse {
|
|||||||
private SemesterType semesterType;
|
private SemesterType semesterType;
|
||||||
private String period;
|
private String period;
|
||||||
|
|
||||||
public ScheduleResponse(Long id, Long departmentId, Long groupId, Long subjectsId, Long lessonTypeId, String lessonType, Long numberOfHours, Boolean division, Long teacherId, SemesterType semesterType, String period) {
|
public ScheduleResponse(Long id, Long departmentId, Long semester, Long groupId, Long subjectsId, Long lessonTypeId, String lessonType, Long numberOfHours, Boolean division, Long teacherId, SemesterType semesterType, String period) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
|
this.semester = semester;
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
this.subjectsId = subjectsId;
|
this.subjectsId = subjectsId;
|
||||||
this.lessonTypeId = lessonTypeId;
|
this.lessonTypeId = lessonTypeId;
|
||||||
@@ -37,13 +38,13 @@ public class ScheduleResponse {
|
|||||||
this.period = period;
|
this.period = period;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScheduleResponse(Long id, Long departmentId, String specialityCode, String groupName, Integer groupCourse, Integer groupSemester, String subjectName, String lessonType, Long numberOfHours, Boolean division, String teacherName, String teacherJobTitle, SemesterType semesterType, String period) {
|
public ScheduleResponse(Long id, Long departmentId, String specialityCode, Long semester, String groupName, Integer groupCourse, String subjectName, String lessonType, Long numberOfHours, Boolean division, String teacherName, String teacherJobTitle, SemesterType semesterType, String period) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
this.specialityCode = specialityCode;
|
this.specialityCode = specialityCode;
|
||||||
|
this.semester = semester;
|
||||||
this.groupName = groupName;
|
this.groupName = groupName;
|
||||||
this.groupCourse = groupCourse;
|
this.groupCourse = groupCourse;
|
||||||
this.groupSemester = groupSemester;
|
|
||||||
this.subjectName = subjectName;
|
this.subjectName = subjectName;
|
||||||
this.lessonType = lessonType;
|
this.lessonType = lessonType;
|
||||||
this.numberOfHours = numberOfHours;
|
this.numberOfHours = numberOfHours;
|
||||||
@@ -66,6 +67,10 @@ public class ScheduleResponse {
|
|||||||
return departmentId;
|
return departmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getSemester() {
|
||||||
|
return semester;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getGroupId() {
|
public Long getGroupId() {
|
||||||
return groupId;
|
return groupId;
|
||||||
}
|
}
|
||||||
@@ -78,10 +83,6 @@ public class ScheduleResponse {
|
|||||||
return groupCourse;
|
return groupCourse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getGroupSemester() {
|
|
||||||
return groupSemester;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Long getSubjectsId() {
|
public Long getSubjectsId() {
|
||||||
return subjectsId;
|
return subjectsId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ public class ScheduleData {
|
|||||||
@Column(name="department_id", nullable = false)
|
@Column(name="department_id", nullable = false)
|
||||||
private Long departmentId;
|
private Long departmentId;
|
||||||
|
|
||||||
|
@Column(name="semester", nullable = false)
|
||||||
|
private Long semester;
|
||||||
|
|
||||||
@Column(name="group_id", nullable = false)
|
@Column(name="group_id", nullable = false)
|
||||||
private Long groupId;
|
private Long groupId;
|
||||||
|
|
||||||
@@ -40,9 +43,10 @@ public class ScheduleData {
|
|||||||
|
|
||||||
public ScheduleData() {}
|
public ScheduleData() {}
|
||||||
|
|
||||||
public ScheduleData(Long id, Long departmentId, Long groupId, Long subjectsId, Long lessonTypeId, Long numberOfHours, Boolean division, Long teacherId, SemesterType semesterType, String period) {
|
public ScheduleData(Long id, Long departmentId, Long semester, Long groupId, Long subjectsId, Long lessonTypeId, Long numberOfHours, Boolean division, Long teacherId, SemesterType semesterType, String period) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
|
this.semester = semester;
|
||||||
this.groupId = groupId;
|
this.groupId = groupId;
|
||||||
this.subjectsId = subjectsId;
|
this.subjectsId = subjectsId;
|
||||||
this.lessonTypeId = lessonTypeId;
|
this.lessonTypeId = lessonTypeId;
|
||||||
@@ -69,6 +73,14 @@ public class ScheduleData {
|
|||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getSemester() {
|
||||||
|
return semester;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSemester(Long semester) {
|
||||||
|
this.semester = semester;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getGroupId() {
|
public Long getGroupId() {
|
||||||
return groupId;
|
return groupId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.magistr.app.model;
|
package com.magistr.app.model;
|
||||||
|
|
||||||
|
import com.magistr.app.utils.CourseCalculator;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -23,12 +24,12 @@ public class StudentGroup {
|
|||||||
@Column(name = "department_id", nullable = false)
|
@Column(name = "department_id", nullable = false)
|
||||||
private Long departmentId;
|
private Long departmentId;
|
||||||
|
|
||||||
|
@Column(name = "enrollment_year", nullable = false)
|
||||||
|
private Integer enrollmentYear;
|
||||||
|
|
||||||
@Column(name="specialty_code", nullable = false)
|
@Column(name="specialty_code", nullable = false)
|
||||||
private Long specialityCode;
|
private Long specialityCode;
|
||||||
|
|
||||||
@Column(name="year_start_study", nullable = false)
|
|
||||||
private Integer yearStartStudy;
|
|
||||||
|
|
||||||
public StudentGroup() {
|
public StudentGroup() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +73,32 @@ public class StudentGroup {
|
|||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getEnrollmentYear() {
|
||||||
|
return enrollmentYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnrollmentYear(Integer enrollmentYear) {
|
||||||
|
this.enrollmentYear = enrollmentYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вычисляемый курс на основе года начала обучения.
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
public Integer getCourse() {
|
||||||
|
if (enrollmentYear == null) return null;
|
||||||
|
return CourseCalculator.calculateCourse(enrollmentYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вычисляемый семестр на основе года начала обучения.
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
public Integer getSemester() {
|
||||||
|
if (enrollmentYear == null) return null;
|
||||||
|
return CourseCalculator.calculateSemester(enrollmentYear);
|
||||||
|
}
|
||||||
|
|
||||||
public Long getSpecialityCode() {
|
public Long getSpecialityCode() {
|
||||||
return specialityCode;
|
return specialityCode;
|
||||||
}
|
}
|
||||||
@@ -79,12 +106,4 @@ public class StudentGroup {
|
|||||||
public void setSpecialityCode(Long specialityCode) {
|
public void setSpecialityCode(Long specialityCode) {
|
||||||
this.specialityCode = specialityCode;
|
this.specialityCode = specialityCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getYearStartStudy() {
|
|
||||||
return yearStartStudy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setYearStartStudy(Integer yearStartStudy) {
|
|
||||||
this.yearStartStudy = yearStartStudy;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ public interface ScheduleDataRepository extends JpaRepository<ScheduleData, Long
|
|||||||
|
|
||||||
List<ScheduleData> findByDepartmentIdAndSemesterTypeAndPeriod(Long departmentId, SemesterType semesterType, String period);
|
List<ScheduleData> findByDepartmentIdAndSemesterTypeAndPeriod(Long departmentId, SemesterType semesterType, String period);
|
||||||
|
|
||||||
boolean existsByDepartmentIdAndGroupIdAndSubjectsIdAndLessonTypeIdAndNumberOfHoursAndDivisionAndTeacherIdAndSemesterTypeAndPeriod(
|
boolean existsByDepartmentIdAndSemesterAndGroupIdAndSubjectsIdAndLessonTypeIdAndNumberOfHoursAndDivisionAndTeacherIdAndSemesterTypeAndPeriod(
|
||||||
Long departmentId,
|
Long departmentId,
|
||||||
|
Long semester,
|
||||||
Long groupId,
|
Long groupId,
|
||||||
Long subjectsId,
|
Long subjectsId,
|
||||||
Long lessonTypeId,
|
Long lessonTypeId,
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.magistr.app.utils;
|
|
||||||
|
|
||||||
import com.magistr.app.model.SemesterType;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
public class CourseAndSemesterCalculator {
|
|
||||||
|
|
||||||
public static int getActualCourse(Integer yearStartStudy) {
|
|
||||||
LocalDate now = LocalDate.now();
|
|
||||||
int currentYear = now.getYear();
|
|
||||||
int currentMonth = now.getMonthValue();
|
|
||||||
|
|
||||||
if (currentMonth >= 9) {
|
|
||||||
return currentYear - yearStartStudy + 1;
|
|
||||||
} else {
|
|
||||||
return currentYear - yearStartStudy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getActualSemester(Integer yearStartStudy) {
|
|
||||||
int course = getActualCourse(yearStartStudy);
|
|
||||||
int currentMonth = LocalDate.now().getMonthValue();
|
|
||||||
|
|
||||||
if ( currentMonth <= 1 || currentMonth >= 9) {
|
|
||||||
return course * 2 - 1;
|
|
||||||
} else {
|
|
||||||
return course * 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getFutureCourse(Integer yearStartStudy, String periodYears) {
|
|
||||||
int recordYear = Integer.parseInt(periodYears.substring(0, 4));
|
|
||||||
return recordYear - yearStartStudy + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static int getFutureSemester(Integer yearStartStudy, String periodYears, SemesterType semesterType) {
|
|
||||||
int course = getFutureCourse(yearStartStudy, periodYears);
|
|
||||||
|
|
||||||
if (semesterType == SemesterType.autumn) {
|
|
||||||
return course * 2 - 1;
|
|
||||||
} else if (semesterType == SemesterType.spring) {
|
|
||||||
return course * 2;
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("Неизвестный semesterType: " + semesterType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.magistr.app.utils;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Утилитный класс для вычисления курса и семестра группы
|
||||||
|
* на основе года начала обучения.
|
||||||
|
*/
|
||||||
|
public final class CourseCalculator {
|
||||||
|
|
||||||
|
private CourseCalculator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вычисляет текущий курс группы.
|
||||||
|
* До сентября студент ещё на старом курсе, с сентября — на следующем.
|
||||||
|
*
|
||||||
|
* @param enrollmentYear год начала обучения (напр. 2023)
|
||||||
|
* @return номер курса (1, 2, 3, ...)
|
||||||
|
*/
|
||||||
|
public static int calculateCourse(int enrollmentYear) {
|
||||||
|
LocalDate now = LocalDate.now();
|
||||||
|
int currentYear = now.getYear();
|
||||||
|
int currentMonth = now.getMonthValue();
|
||||||
|
|
||||||
|
// С сентября начинается новый учебный год
|
||||||
|
if (currentMonth >= 9) {
|
||||||
|
return currentYear - enrollmentYear + 1;
|
||||||
|
} else {
|
||||||
|
return currentYear - enrollmentYear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вычисляет текущий семестр группы.
|
||||||
|
* Сентябрь–январь → нечётный (осенний) семестр, февраль–август → чётный (весенний).
|
||||||
|
*
|
||||||
|
* @param enrollmentYear год начала обучения (напр. 2023)
|
||||||
|
* @return номер семестра (1, 2, 3, ...)
|
||||||
|
*/
|
||||||
|
public static int calculateSemester(int enrollmentYear) {
|
||||||
|
int course = calculateCourse(enrollmentYear);
|
||||||
|
int currentMonth = LocalDate.now().getMonthValue();
|
||||||
|
|
||||||
|
// Сентябрь–январь: осенний (нечётный) семестр
|
||||||
|
// Февраль–август: весенний (чётный) семестр
|
||||||
|
if (currentMonth >= 9 || currentMonth <= 1) {
|
||||||
|
return (course - 1) * 2 + 1;
|
||||||
|
} else {
|
||||||
|
return (course - 1) * 2 + 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -74,15 +74,14 @@ CREATE TABLE IF NOT EXISTS student_groups (
|
|||||||
group_size BIGINT NOT NULL,
|
group_size BIGINT NOT NULL,
|
||||||
education_form_id BIGINT NOT NULL REFERENCES education_forms(id),
|
education_form_id BIGINT NOT NULL REFERENCES education_forms(id),
|
||||||
department_id BIGINT NOT NULL REFERENCES departments(id),
|
department_id BIGINT NOT NULL REFERENCES departments(id),
|
||||||
specialty_code INT NOT NULL REFERENCES specialties(id),
|
course INT CHECK (course BETWEEN 1 AND 6),
|
||||||
year_start_study BIGINT NOT NULL,
|
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Тестовая базовая группа для работы
|
-- Тестовая базовая группа для работы
|
||||||
INSERT INTO student_groups (name, group_size, education_form_id, department_id, specialty_code, year_start_study)
|
INSERT INTO student_groups (name, group_size, education_form_id, department_id, course)
|
||||||
VALUES ('ИВТ-21-1', 25, 1, 1, 2, 2025),
|
VALUES ('ИВТ-21-1', 25, 1, 1, 3),
|
||||||
('ИБ-41м', 15, 2, 1, 1, 2024)
|
('ИБ-41м', 15, 2, 1, 2)
|
||||||
ON CONFLICT (name) DO NOTHING;
|
ON CONFLICT (name) DO NOTHING;
|
||||||
|
|
||||||
-- ==========================================
|
-- ==========================================
|
||||||
@@ -239,6 +238,7 @@ INSERT INTO lessons (teacher_id, group_id, subject_id, lesson_format, type_lesso
|
|||||||
CREATE TABLE IF NOT EXISTS schedule_data (
|
CREATE TABLE IF NOT EXISTS schedule_data (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
department_id BIGINT NOT NULL REFERENCES departments(id),
|
department_id BIGINT NOT NULL REFERENCES departments(id),
|
||||||
|
semester INT NOT NULL,
|
||||||
group_id BIGINT NOT NULL REFERENCES student_groups(id),
|
group_id BIGINT NOT NULL REFERENCES student_groups(id),
|
||||||
subjects_id BIGINT NOT NULL REFERENCES subjects(id),
|
subjects_id BIGINT NOT NULL REFERENCES subjects(id),
|
||||||
lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id),
|
lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id),
|
||||||
@@ -249,17 +249,10 @@ CREATE TABLE IF NOT EXISTS schedule_data (
|
|||||||
period VARCHAR(255) NOT NULL
|
period VARCHAR(255) NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
INSERT INTO schedule_data (department_id, group_id, subjects_id, lesson_type_id, number_of_hours, is_division, teacher_id, semester_type, period)
|
INSERT INTO schedule_data (department_id, semester, group_id, subjects_id, lesson_type_id, number_of_hours, is_division, teacher_id, semester_type, period)
|
||||||
VALUES (1, 1, 1, 3, 2, true, 1, 'autumn', '2024-2025'),
|
VALUES (1, 1, 1, 1, 3, 2, true, 1, 'Весенний', '2024/2025'),
|
||||||
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
(2, 4, 2, 3, 2, 1, false, 2, 'Осенний', '2025/2026'),
|
||||||
(3, 1, 2, 1, 3, true, 1, 'autumn', '2023-2024'),
|
(3, 5, 1, 2, 1, 3, true, 1, 'Весенний', '2023/2024');
|
||||||
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
|
||||||
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
|
||||||
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
|
||||||
(1, 1, 1, 1, 2, true, 2, 'autumn', '2024-2025'),
|
|
||||||
(1, 2, 2, 3, 4, false, 2, 'autumn', '2024-2025'),
|
|
||||||
(1, 1, 4, 2, 1, false, 1, 'autumn', '2024-2025'),
|
|
||||||
(1, 2, 5, 1, 7, true, 1, 'autumn', '2024-2025');
|
|
||||||
|
|
||||||
-- ==========================================
|
-- ==========================================
|
||||||
-- Функция обновления timestamp
|
-- Функция обновления timestamp
|
||||||
@@ -286,6 +279,7 @@ COMMENT ON TABLE departments IS 'Кафедры';
|
|||||||
COMMENT ON TABLE specialties IS 'Специальности';
|
COMMENT ON TABLE specialties IS 'Специальности';
|
||||||
COMMENT ON TABLE schedule_data IS 'Данные к составлению расписания';
|
COMMENT ON TABLE schedule_data IS 'Данные к составлению расписания';
|
||||||
COMMENT ON COLUMN schedule_data.department_id IS 'Идентификатор кафедры';
|
COMMENT ON COLUMN schedule_data.department_id IS 'Идентификатор кафедры';
|
||||||
|
COMMENT ON COLUMN schedule_data.semester IS 'Номер семестра';
|
||||||
COMMENT ON COLUMN schedule_data.group_id IS 'Идентификатор группы';
|
COMMENT ON COLUMN schedule_data.group_id IS 'Идентификатор группы';
|
||||||
COMMENT ON COLUMN schedule_data.subjects_id IS 'Идентификатор предмета';
|
COMMENT ON COLUMN schedule_data.subjects_id IS 'Идентификатор предмета';
|
||||||
COMMENT ON COLUMN schedule_data.lesson_type_id IS 'Идентификатор типа занятия';
|
COMMENT ON COLUMN schedule_data.lesson_type_id IS 'Идентификатор типа занятия';
|
||||||
@@ -324,6 +318,7 @@ COMMENT ON COLUMN student_groups.name IS 'Название группы';
|
|||||||
COMMENT ON COLUMN student_groups.group_size IS 'Количество студентов';
|
COMMENT ON COLUMN student_groups.group_size IS 'Количество студентов';
|
||||||
COMMENT ON COLUMN student_groups.education_form_id IS 'ID формы обучения, к которой относится группа';
|
COMMENT ON COLUMN student_groups.education_form_id IS 'ID формы обучения, к которой относится группа';
|
||||||
COMMENT ON COLUMN student_groups.department_id IS 'ID кафедры';
|
COMMENT ON COLUMN student_groups.department_id IS 'ID кафедры';
|
||||||
|
COMMENT ON COLUMN student_groups.course IS 'Курс';
|
||||||
COMMENT ON COLUMN student_groups.created_at IS 'Дата и время создания';
|
COMMENT ON COLUMN student_groups.created_at IS 'Дата и время создания';
|
||||||
|
|
||||||
COMMENT ON COLUMN subgroups.id IS 'ID подгруппы';
|
COMMENT ON COLUMN subgroups.id IS 'ID подгруппы';
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
-- ==========================================
|
||||||
|
-- Редактирование учебных групп
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
ALTER TABLE student_groups
|
||||||
|
ADD COLUMN IF NOT EXISTS specialty_code INT REFERENCES specialties(id);
|
||||||
|
|
||||||
|
UPDATE student_groups
|
||||||
|
SET specialty_code = 1
|
||||||
|
WHERE specialty_code IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE student_groups
|
||||||
|
ALTER COLUMN specialty_code SET NOT NULL;
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- Редактирование данных для расписания
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
INSERT INTO schedule_data (department_id, semester, group_id, subjects_id, lesson_type_id, number_of_hours, is_division, teacher_id, semester_type, period)
|
||||||
|
VALUES (1, 1, 1, 1, 3, 2, true, 1, 'autumn', '2024-2025'),
|
||||||
|
(2, 4, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
||||||
|
(3, 5, 1, 2, 1, 3, true, 1, 'autumn', '2023-2024'),
|
||||||
|
(2, 4, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
||||||
|
(2, 4, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
||||||
|
(2, 4, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
||||||
|
(1, 1, 1, 1, 1, 2, true, 2, 'autumn', '2024-2025'),
|
||||||
|
(1, 2, 2, 2, 3, 4, false, 2, 'autumn', '2024-2025'),
|
||||||
|
(1, 3, 1, 4, 2, 1, false, 1, 'autumn', '2024-2025'),
|
||||||
|
(1, 4, 2, 5, 1, 7, true, 1, 'autumn', '2024-2025');
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- Год начала обучения вместо статического курса
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
ALTER TABLE student_groups
|
||||||
|
ADD COLUMN IF NOT EXISTS enrollment_year INT;
|
||||||
|
|
||||||
|
-- Обратный расчёт: enrollment_year = текущий_год - course + 1
|
||||||
|
-- (для месяцев до сентября курс ещё не увеличился)
|
||||||
|
UPDATE student_groups
|
||||||
|
SET enrollment_year = EXTRACT(YEAR FROM NOW())::INT - course
|
||||||
|
+ CASE WHEN EXTRACT(MONTH FROM NOW()) >= 9 THEN 1 ELSE 0 END;
|
||||||
|
|
||||||
|
ALTER TABLE student_groups
|
||||||
|
ALTER COLUMN enrollment_year SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE student_groups
|
||||||
|
DROP COLUMN IF EXISTS course;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN student_groups.enrollment_year IS 'Год начала обучения группы';
|
||||||
@@ -51,7 +51,8 @@ erDiagram
|
|||||||
BIGINT group_size
|
BIGINT group_size
|
||||||
BIGINT education_form_id FK
|
BIGINT education_form_id FK
|
||||||
BIGINT department_id FK
|
BIGINT department_id FK
|
||||||
INT course
|
INT enrollment_year
|
||||||
|
INT specialty_code FK
|
||||||
TIMESTAMP created_at
|
TIMESTAMP created_at
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +221,10 @@ erDiagram
|
|||||||
| `group_size` | BIGINT | Количество студентов |
|
| `group_size` | BIGINT | Количество студентов |
|
||||||
| `education_form_id` | BIGINT FK → education_forms | Форма обучения |
|
| `education_form_id` | BIGINT FK → education_forms | Форма обучения |
|
||||||
| `department_id` | BIGINT FK → departments | Кафедра |
|
| `department_id` | BIGINT FK → departments | Кафедра |
|
||||||
| `course` | INT CHECK(1–6) | Курс |
|
| `enrollment_year` | INT NOT NULL | Год начала обучения (напр. 2023) |
|
||||||
|
| `specialty_code` | INT FK → specialties | Код специальности |
|
||||||
|
|
||||||
|
> **Примечание:** Курс и семестр **вычисляются динамически** на основе `enrollment_year` и текущей даты (утилита `CourseCalculator.java`). В БД не хранятся.
|
||||||
|
|
||||||
#### `subgroups` — Подгруппы
|
#### `subgroups` — Подгруппы
|
||||||
| Колонка | Тип | Описание |
|
| Колонка | Тип | Описание |
|
||||||
@@ -341,6 +345,7 @@ erDiagram
|
|||||||
| Файл | Описание |
|
| Файл | Описание |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `V1__init.sql` | Инициализация: все таблицы, тестовые данные, триггеры, комментарии |
|
| `V1__init.sql` | Инициализация: все таблицы, тестовые данные, триггеры, комментарии |
|
||||||
|
| `V2__editScheduleData.sql` | Добавление `specialty_code`, тестовые данные расписания, замена `course` → `enrollment_year` |
|
||||||
|
|
||||||
### Накатывание на существующих тенантов
|
### Накатывание на существующих тенантов
|
||||||
|
|
||||||
|
|||||||
@@ -1,154 +0,0 @@
|
|||||||
/* ===== Auditorium Workload Specific Styles ===== */
|
|
||||||
|
|
||||||
.workload-grid-container {
|
|
||||||
width: 100%;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow: auto;
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
border: 1px solid var(--bg-card-border);
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workload-table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
min-width: 800px;
|
|
||||||
table-layout: fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workload-table th, .workload-table td {
|
|
||||||
border: 1px solid var(--bg-card-border);
|
|
||||||
padding: 0.5rem;
|
|
||||||
vertical-align: top;
|
|
||||||
position: relative;
|
|
||||||
min-width: 150px;
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workload-table th {
|
|
||||||
background: var(--bg-input);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
|
||||||
padding: 1rem 0.5rem;
|
|
||||||
box-shadow: 0 1px 0 var(--bg-card-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workload-table .time-cell {
|
|
||||||
background: var(--bg-input);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
|
||||||
vertical-align: middle;
|
|
||||||
width: 120px;
|
|
||||||
min-width: 120px;
|
|
||||||
position: sticky;
|
|
||||||
left: 0;
|
|
||||||
z-index: 5;
|
|
||||||
box-shadow: 1px 0 0 var(--bg-card-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workload-table .top-left-cell {
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 20;
|
|
||||||
background: var(--bg-input);
|
|
||||||
min-width: 120px;
|
|
||||||
width: 120px;
|
|
||||||
box-shadow: 1px 1px 0 var(--bg-card-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Diagonal line using SVG or linear-gradient */
|
|
||||||
.workload-table .top-left-cell::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background-image: linear-gradient(
|
|
||||||
to bottom right,
|
|
||||||
transparent calc(50% - 1px),
|
|
||||||
var(--bg-card-border) 50%,
|
|
||||||
transparent calc(50% + 1px)
|
|
||||||
);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-left-cell span.top-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.top-left-cell span.bottom-label {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0.5rem;
|
|
||||||
left: 0.5rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Lesson Cards inside grid cells */
|
|
||||||
.lesson-card {
|
|
||||||
background: var(--bg-card);
|
|
||||||
border: 1px solid var(--bg-card-border);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
padding: 0.5rem;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
transition: all 0.2s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="light"] .lesson-card {
|
|
||||||
background: rgba(255, 255, 255, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lesson-card:hover {
|
|
||||||
background: var(--bg-hover);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
|
|
||||||
border-color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.lesson-subject {
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 0.2rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lesson-group {
|
|
||||||
font-weight: 700; /* Bolder specific for groups request mockup */
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: 0.1rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lesson-teacher {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom scrollbar adjustments for grid container */
|
|
||||||
.workload-grid-container::-webkit-scrollbar {
|
|
||||||
width: 8px;
|
|
||||||
height: 8px;
|
|
||||||
}
|
|
||||||
.workload-grid-container::-webkit-scrollbar-track {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.workload-grid-container::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(255, 255, 255, 0.15);
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
[data-theme="light"] .workload-grid-container::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(0, 0, 0, 0.15);
|
|
||||||
}
|
|
||||||
@@ -313,32 +313,3 @@ details.table-item .content{
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== Контейнер занятий преподавателя в модалках ===== */
|
|
||||||
.cs-modal-table .lessons-container {
|
|
||||||
max-height: 50vh;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 0.5rem;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgba(99, 102, 241, 0.55) rgba(255, 255, 255, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.cs-modal-table .lessons-container::-webkit-scrollbar {
|
|
||||||
width: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cs-modal-table .lessons-container::-webkit-scrollbar-track {
|
|
||||||
background: rgba(255, 255, 255, 0.06);
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cs-modal-table .lessons-container::-webkit-scrollbar-thumb {
|
|
||||||
background: rgba(99, 102, 241, 0.55);
|
|
||||||
border-radius: 10px;
|
|
||||||
border: 2px solid rgba(0, 0, 0, 0);
|
|
||||||
background-clip: padding-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cs-modal-table .lessons-container::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: rgba(99, 102, 241, 0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -16,7 +16,6 @@
|
|||||||
<link rel="stylesheet" href="css/modals.css">
|
<link rel="stylesheet" href="css/modals.css">
|
||||||
<link rel="stylesheet" href="css/department.css">
|
<link rel="stylesheet" href="css/department.css">
|
||||||
<link rel="stylesheet" href="css/departments-data.css">
|
<link rel="stylesheet" href="css/departments-data.css">
|
||||||
<link rel="stylesheet" href="css/auditorium-workload.css">
|
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -122,14 +121,6 @@
|
|||||||
</svg>
|
</svg>
|
||||||
<span>Расписание занятий</span>
|
<span>Расписание занятий</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="nav-item" data-tab="auditorium-workload">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
|
|
||||||
<line x1="3" y1="9" x2="21" y2="9"></line>
|
|
||||||
<line x1="9" y1="21" x2="9" y2="9"></line>
|
|
||||||
</svg>
|
|
||||||
<span>Загруженность аудиторий</span>
|
|
||||||
</a>
|
|
||||||
<a href="#" class="nav-item" data-tab="database">
|
<a href="#" class="nav-item" data-tab="database">
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
|
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {initSchedule} from "./views/schedule.js";
|
|||||||
import {initDatabase} from "./views/database.js";
|
import {initDatabase} from "./views/database.js";
|
||||||
import {initDepartment} from "./views/department.js";
|
import {initDepartment} from "./views/department.js";
|
||||||
import {initDepartmentsData} from "./views/departments-data.js";
|
import {initDepartmentsData} from "./views/departments-data.js";
|
||||||
import {initAuditoriumWorkload} from "./views/auditorium-workload.js";
|
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
const ROUTES = {
|
const ROUTES = {
|
||||||
@@ -39,7 +38,6 @@ const ROUTES = {
|
|||||||
classrooms: { title: 'Аудитории', file: 'views/classrooms.html', init: initClassrooms },
|
classrooms: { title: 'Аудитории', file: 'views/classrooms.html', init: initClassrooms },
|
||||||
subjects: { title: 'Дисциплины и преподаватели', file: 'views/subjects.html', init: initSubjects },
|
subjects: { title: 'Дисциплины и преподаватели', file: 'views/subjects.html', init: initSubjects },
|
||||||
schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule },
|
schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule },
|
||||||
'auditorium-workload': { title: 'Загруженность аудиторий', file: 'views/auditorium-workload.html', init: initAuditoriumWorkload },
|
|
||||||
database: { title: 'База данных', file: 'views/database.html', init: initDatabase },
|
database: { title: 'База данных', file: 'views/database.html', init: initDatabase },
|
||||||
department: { title: 'Кафедры', file: 'views/department.html', init: initDepartment },
|
department: { title: 'Кафедры', file: 'views/department.html', init: initDepartment },
|
||||||
'departments-data': { title: 'Создание кафедры/специальности', file: 'views/departments-data.html', init: initDepartmentsData },
|
'departments-data': { title: 'Создание кафедры/специальности', file: 'views/departments-data.html', init: initDepartmentsData },
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
import { initMultiSelect } from '../utils.js';
|
|
||||||
|
|
||||||
export function initAuditoriumWorkload() {
|
|
||||||
// Initialize date input with current date
|
|
||||||
const dateInput = document.getElementById('workload-date');
|
|
||||||
if (dateInput) {
|
|
||||||
const today = new Date();
|
|
||||||
const yyyy = today.getFullYear();
|
|
||||||
const mm = String(today.getMonth() + 1).padStart(2, '0');
|
|
||||||
const dd = String(today.getDate()).padStart(2, '0');
|
|
||||||
dateInput.value = `${yyyy}-${mm}-${dd}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize Multi-Selects
|
|
||||||
initMultiSelect('building-box', 'building-menu', 'building-text', 'building-checkboxes');
|
|
||||||
initMultiSelect('capacity-box', 'capacity-menu', 'capacity-text', 'capacity-checkboxes');
|
|
||||||
initMultiSelect('equipment-box', 'equipment-menu', 'equipment-text', 'equipment-checkboxes');
|
|
||||||
|
|
||||||
// Populate Filters with Mock/Initial Data
|
|
||||||
populateFilters();
|
|
||||||
|
|
||||||
// Render Mock Data for the Grid based on the UI requested layout
|
|
||||||
renderMockGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
function populateFilters() {
|
|
||||||
// Buildings
|
|
||||||
const buildingsContainer = document.getElementById('building-checkboxes');
|
|
||||||
const buildings = [
|
|
||||||
{ id: 1, name: "Корпус 1 (Главный)" },
|
|
||||||
{ id: 2, name: "Корпус 2 (Физ-мат)" },
|
|
||||||
{ id: 3, name: "Корпус 3 (Гуманитарный)" }
|
|
||||||
];
|
|
||||||
buildingsContainer.innerHTML = buildings.map(item => `
|
|
||||||
<label class="checkbox-item">
|
|
||||||
<input type="checkbox" value="${item.id}">
|
|
||||||
<span class="checkmark"></span>
|
|
||||||
<span class="checkbox-label">${item.name}</span>
|
|
||||||
</label>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// Capacities
|
|
||||||
const capacityContainer = document.getElementById('capacity-checkboxes');
|
|
||||||
const capacities = [
|
|
||||||
{ id: 'small', name: "До 30 мест" },
|
|
||||||
{ id: 'medium', name: "30 - 60 мест" },
|
|
||||||
{ id: 'large', name: "60 - 100 мест" },
|
|
||||||
{ id: 'xlarge', name: "Более 100 мест" }
|
|
||||||
];
|
|
||||||
capacityContainer.innerHTML = capacities.map(item => `
|
|
||||||
<label class="checkbox-item">
|
|
||||||
<input type="checkbox" value="${item.id}">
|
|
||||||
<span class="checkmark"></span>
|
|
||||||
<span class="checkbox-label">${item.name}</span>
|
|
||||||
</label>
|
|
||||||
`).join('');
|
|
||||||
|
|
||||||
// Equipment
|
|
||||||
const equipmentContainer = document.getElementById('equipment-checkboxes');
|
|
||||||
const equipmentList = [
|
|
||||||
{ id: 1, name: "Проектор" },
|
|
||||||
{ id: 2, name: "Компьютерные места" },
|
|
||||||
{ id: 3, name: "Интерактивная доска" },
|
|
||||||
{ id: 4, name: "Микрофон" }
|
|
||||||
];
|
|
||||||
equipmentContainer.innerHTML = equipmentList.map(item => `
|
|
||||||
<label class="checkbox-item">
|
|
||||||
<input type="checkbox" value="${item.id}">
|
|
||||||
<span class="checkmark"></span>
|
|
||||||
<span class="checkbox-label">${item.name}</span>
|
|
||||||
</label>
|
|
||||||
`).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderMockGrid() {
|
|
||||||
// In future this will be loaded from API
|
|
||||||
const timeslots = [
|
|
||||||
"8:00-9:30",
|
|
||||||
"9:40-11:10",
|
|
||||||
"11:40-13:10",
|
|
||||||
"13:20-14:50",
|
|
||||||
"15:00-16:30",
|
|
||||||
"16:50-18:20",
|
|
||||||
"18:30-19:50",
|
|
||||||
"20:00-21:20"
|
|
||||||
];
|
|
||||||
|
|
||||||
const auditoriums = [
|
|
||||||
"201", "202", "204", "205", "206", "207", "208"
|
|
||||||
];
|
|
||||||
|
|
||||||
// Mock schedule data mapped by room and time
|
|
||||||
// Key: "roomId_timeSlotId", Value: Lesson object
|
|
||||||
const mockSchedule = {
|
|
||||||
"201_8:00-9:30": { subject: "Физика", group: "ИБ-41м", teacher: "Атлетов А.Р." },
|
|
||||||
"201_9:40-11:10": { subject: "Физика", group: "ИВТ-21-1", teacher: "Атлетов А.Р." },
|
|
||||||
"201_11:40-13:10": { subject: "Физика", group: "ИБ-41м", teacher: "Физик В.Г." },
|
|
||||||
"201_13:20-14:50": { subject: "Физика", group: "ИБ-41м", teacher: "Физик В.Г." },
|
|
||||||
|
|
||||||
"202_9:40-11:10": { subject: "Химия", group: "ИВТ-21-1", teacher: "Химоза Я.В." },
|
|
||||||
"202_13:20-14:50": { subject: "Математика", group: "ИВТ-21-1", teacher: "Рутина Л.П." },
|
|
||||||
"202_15:00-16:30": { subject: "Химия", group: "ИВТ-21-1", teacher: "Химоза Я.В." },
|
|
||||||
"202_16:50-18:20": { subject: "Физика", group: "ИВТ-21-1", teacher: "Атлетов А.Р." },
|
|
||||||
|
|
||||||
"205_9:40-11:10": { subject: "Организация аудита ИБ", group: "ИБ-41м", teacher: "Таныгин М.О." },
|
|
||||||
};
|
|
||||||
|
|
||||||
// Render Headers
|
|
||||||
const headerRow = document.getElementById('workload-header-row');
|
|
||||||
// Start after the first fixed cell (which is already in HTML)
|
|
||||||
|
|
||||||
auditoriums.forEach(room => {
|
|
||||||
const th = document.createElement('th');
|
|
||||||
th.textContent = room;
|
|
||||||
headerRow.appendChild(th);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Render Body Rows
|
|
||||||
const tbody = document.getElementById('workload-tbody');
|
|
||||||
|
|
||||||
timeslots.forEach((time) => {
|
|
||||||
const tr = document.createElement('tr');
|
|
||||||
|
|
||||||
// Add Time Cell
|
|
||||||
const tdTime = document.createElement('td');
|
|
||||||
tdTime.className = 'time-cell';
|
|
||||||
tdTime.textContent = time;
|
|
||||||
tr.appendChild(tdTime);
|
|
||||||
|
|
||||||
// Add Room Cells for this Time
|
|
||||||
auditoriums.forEach(room => {
|
|
||||||
const td = document.createElement('td');
|
|
||||||
|
|
||||||
const scheduleKey = `${room}_${time}`;
|
|
||||||
const lesson = mockSchedule[scheduleKey];
|
|
||||||
|
|
||||||
if (lesson) {
|
|
||||||
// Render lesson card
|
|
||||||
td.innerHTML = `
|
|
||||||
<div class="lesson-card">
|
|
||||||
<div class="lesson-subject">${lesson.subject}</div>
|
|
||||||
<div class="lesson-group">${lesson.group}</div>
|
|
||||||
<div class="lesson-teacher">${lesson.teacher}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
tr.appendChild(td);
|
|
||||||
});
|
|
||||||
|
|
||||||
tbody.appendChild(tr);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||||
|
|
||||||
// Ключ для хранения данных в sessionStorage
|
|
||||||
const STORAGE_KEY = 'department_schedule_blocks';
|
|
||||||
|
|
||||||
export async function initDepartment() {
|
export async function initDepartment() {
|
||||||
const form = document.getElementById('department-schedule-form');
|
const form = document.getElementById('department-schedule-form');
|
||||||
const departmentSelect = document.getElementById('filter-department');
|
const departmentSelect = document.getElementById('filter-department');
|
||||||
@@ -20,9 +17,6 @@ export async function initDepartment() {
|
|||||||
departmentSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
|
departmentSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Восстанавливаем ранее загруженные таблицы из sessionStorage =====
|
|
||||||
restoreScheduleBlocks();
|
|
||||||
|
|
||||||
form.addEventListener('submit', async (e) => {
|
form.addEventListener('submit', async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
hideAlert('schedule-form-alert');
|
hideAlert('schedule-form-alert');
|
||||||
@@ -45,32 +39,17 @@ export async function initDepartment() {
|
|||||||
const semesterName = semesterType === 'spring' ? 'весенний' : (semesterType === 'autumn' ? 'осенний' : semesterType);
|
const semesterName = semesterType === 'spring' ? 'весенний' : (semesterType === 'autumn' ? 'осенний' : semesterType);
|
||||||
const periodName = period.replace('-', '/');
|
const periodName = period.replace('-', '/');
|
||||||
|
|
||||||
renderScheduleBlock(deptName, semesterName, periodName, data, departmentId, semesterType, period);
|
renderScheduleBlock(deptName, semesterName, periodName, data);
|
||||||
|
form.reset();
|
||||||
// НЕ сбрасываем форму — фильтры остаются заполненными (fix #3)
|
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlert('schedule-form-alert', err.message || 'Ошибка загрузки данных', 'error');
|
showAlert('schedule-form-alert', err.message || 'Ошибка загрузки данных', 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== Уникальный ключ для таблицы по параметрам =====
|
function renderScheduleBlock(deptName, semester, period, schedule) {
|
||||||
function blockKey(departmentId, semesterType, period) {
|
|
||||||
return `${departmentId}_${semesterType}_${period}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Рендер блока таблицы (с дедупликацией — fix #6) =====
|
|
||||||
function renderScheduleBlock(deptName, semester, period, schedule, departmentId, semesterType, rawPeriod) {
|
|
||||||
const key = blockKey(departmentId, semesterType, rawPeriod);
|
|
||||||
|
|
||||||
// Удаляем ранее загруженный блок с тем же ключом
|
|
||||||
const existing = container.querySelector(`[data-block-key="${key}"]`);
|
|
||||||
if (existing) existing.remove();
|
|
||||||
|
|
||||||
const details = document.createElement('details');
|
const details = document.createElement('details');
|
||||||
details.className = 'table-item';
|
details.className = 'table-item';
|
||||||
details.open = true;
|
details.open = true;
|
||||||
details.setAttribute('data-block-key', key);
|
|
||||||
details.innerHTML = `
|
details.innerHTML = `
|
||||||
<summary>
|
<summary>
|
||||||
<div class="chev" aria-hidden="true">
|
<div class="chev" aria-hidden="true">
|
||||||
@@ -85,7 +64,7 @@ export async function initDepartment() {
|
|||||||
<span class="title-sub">Семестр: <b>${escapeHtml(semester)}</b></span>
|
<span class="title-sub">Семестр: <b>${escapeHtml(semester)}</b></span>
|
||||||
<span class="title-sub">Уч. год: <b>${escapeHtml(period)}</b></span>
|
<span class="title-sub">Уч. год: <b>${escapeHtml(period)}</b></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">${Array.isArray(schedule) ? schedule.length : 0} записей</div>
|
<div class="meta">${schedule ? schedule.length : 0} записей</div>
|
||||||
</summary>
|
</summary>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<table>
|
<table>
|
||||||
@@ -107,15 +86,11 @@ export async function initDepartment() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
container.prepend(details);
|
container.prepend(details);
|
||||||
|
|
||||||
// Сохраняем в sessionStorage
|
|
||||||
saveScheduleBlock(key, { deptName, semester, period, schedule, departmentId, semesterType, rawPeriod });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderRows(schedule) {
|
function renderRows(schedule) {
|
||||||
if (!Array.isArray(schedule) || schedule.length === 0) {
|
if (!schedule || schedule.length === 0) {
|
||||||
return '<tr><td colspan="8" class="loading-row">Нет данных</td></tr>';
|
return '<tr><td colspan="8" class="loading-row">Нет данных</td></tr>';
|
||||||
}
|
}
|
||||||
return schedule.map(r => `
|
return schedule.map(r => `
|
||||||
@@ -142,32 +117,6 @@ export async function initDepartment() {
|
|||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Persistence: sessionStorage (fix #4) =====
|
|
||||||
function saveScheduleBlock(key, blockData) {
|
|
||||||
try {
|
|
||||||
const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
|
|
||||||
stored[key] = blockData;
|
|
||||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(stored));
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Ошибка сохранения в sessionStorage:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function restoreScheduleBlocks() {
|
|
||||||
try {
|
|
||||||
const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}');
|
|
||||||
const keys = Object.keys(stored);
|
|
||||||
if (keys.length === 0) return;
|
|
||||||
|
|
||||||
keys.forEach(key => {
|
|
||||||
const b = stored[key];
|
|
||||||
renderScheduleBlock(b.deptName, b.semester, b.period, b.schedule, b.departmentId, b.semesterType, b.rawPeriod);
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Ошибка восстановления из sessionStorage:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// ЛОГИКА ДЛЯ ФУНКЦИОНАЛА "СОЗДАТЬ ЗАПИСЬ (К/Ф)"
|
// ЛОГИКА ДЛЯ ФУНКЦИОНАЛА "СОЗДАТЬ ЗАПИСЬ (К/Ф)"
|
||||||
// Два модальных окна поверх всего контента в одном оверлее
|
// Два модальных окна поверх всего контента в одном оверлее
|
||||||
@@ -209,28 +158,12 @@ export async function initDepartment() {
|
|||||||
csSubjectSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
|
csSubjectSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
|
||||||
csSubjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
|
csSubjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
|
||||||
|
|
||||||
// Загрузка преподавателей: сначала по кафедре, при ошибке — все преподаватели
|
|
||||||
csTeachers = [];
|
|
||||||
if (localDepartmentId) {
|
if (localDepartmentId) {
|
||||||
try {
|
|
||||||
csTeachers = await api.get(`/api/users/teachers/${localDepartmentId}`);
|
csTeachers = await api.get(`/api/users/teachers/${localDepartmentId}`);
|
||||||
} catch (e) {
|
|
||||||
console.warn('Не удалось загрузить преподавателей для кафедры, загружаем всех:', e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Фолбэк: загружаем всех преподавателей
|
|
||||||
if (!Array.isArray(csTeachers) || csTeachers.length === 0) {
|
|
||||||
try {
|
|
||||||
csTeachers = await api.get('/api/users/teachers');
|
|
||||||
} catch (e2) {
|
|
||||||
console.error('Ошибка загрузки всех преподавателей:', e2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (Array.isArray(csTeachers) && csTeachers.length > 0) {
|
|
||||||
csTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' +
|
csTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' +
|
||||||
csTeachers.map(t => `<option value="${t.id}">${escapeHtml(t.fullName || t.username)}</option>`).join('');
|
csTeachers.map(t => `<option value="${t.id}">${escapeHtml(t.fullName || t.username)}</option>`).join('');
|
||||||
} else {
|
} else {
|
||||||
csTeacherSelect.innerHTML = '<option value="">Нет преподавателей</option>';
|
csTeacherSelect.innerHTML = '<option value="">Ошибка: Не найден ID кафедры</option>';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Ошибка загрузки справочников:', e);
|
console.error('Ошибка загрузки справочников:', e);
|
||||||
@@ -242,7 +175,7 @@ export async function initDepartment() {
|
|||||||
// ===== Открытие / Закрытие оверлея =====
|
// ===== Открытие / Закрытие оверлея =====
|
||||||
function openOverlay() {
|
function openOverlay() {
|
||||||
csOverlay.classList.add('open');
|
csOverlay.classList.add('open');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden'; // Предотвращаем скролл страницы
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeOverlay() {
|
function closeOverlay() {
|
||||||
@@ -271,6 +204,7 @@ export async function initDepartment() {
|
|||||||
modalCreateScheduleClose.addEventListener('click', closeOverlay);
|
modalCreateScheduleClose.addEventListener('click', closeOverlay);
|
||||||
|
|
||||||
csOverlay.addEventListener('click', (e) => {
|
csOverlay.addEventListener('click', (e) => {
|
||||||
|
// Закрыть по клику на затемнённый фон (но не по клику на содержимое модалок)
|
||||||
if (e.target === csOverlay || e.target.classList.contains('cs-overlay-scroll')) {
|
if (e.target === csOverlay || e.target.classList.contains('cs-overlay-scroll')) {
|
||||||
closeOverlay();
|
closeOverlay();
|
||||||
}
|
}
|
||||||
@@ -282,10 +216,10 @@ export async function initDepartment() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===== Рендер таблицы подготовленных записей =====
|
// ===== Рендер таблицы =====
|
||||||
function renderPreparedSchedules() {
|
function renderPreparedSchedules() {
|
||||||
if (preparedSchedules.length === 0) {
|
if (preparedSchedules.length === 0) {
|
||||||
preparedSchedulesTbody.innerHTML = '<tr><td colspan="9" class="loading-row">Нет записей</td></tr>';
|
preparedSchedulesTbody.innerHTML = '<tr><td colspan="10" class="loading-row">Нет записей</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
preparedSchedulesTbody.innerHTML = preparedSchedules.map((s, index) => {
|
preparedSchedulesTbody.innerHTML = preparedSchedules.map((s, index) => {
|
||||||
@@ -296,13 +230,14 @@ export async function initDepartment() {
|
|||||||
const lessonTypeName = LESSON_TYPE_LABELS[s.lessonTypeId] || 'Неизвестно';
|
const lessonTypeName = LESSON_TYPE_LABELS[s.lessonTypeId] || 'Неизвестно';
|
||||||
const semLabel = SEMESTER_LABELS[s.semesterType] || s.semesterType;
|
const semLabel = SEMESTER_LABELS[s.semesterType] || s.semesterType;
|
||||||
const periodDisplay = s.period.replace('-', '/');
|
const periodDisplay = s.period.replace('-', '/');
|
||||||
const divText = s.isDivision ? '✓' : '';
|
const divText = s.division ? '✓' : '';
|
||||||
const hasError = !!s._errorMsg;
|
const hasError = !!s._errorMsg;
|
||||||
const rowStyle = hasError ? ' style="background: rgba(239, 68, 68, 0.08);"' : '';
|
const rowStyle = hasError ? ' style="background: rgba(239, 68, 68, 0.08);"' : '';
|
||||||
let row = `
|
let row = `
|
||||||
<tr${rowStyle}>
|
<tr${rowStyle}>
|
||||||
<td>${escapeHtml(periodDisplay)}</td>
|
<td>${escapeHtml(periodDisplay)}</td>
|
||||||
<td>${escapeHtml(semLabel)}</td>
|
<td>${escapeHtml(semLabel)}</td>
|
||||||
|
<td>${s.semester}</td>
|
||||||
<td>${escapeHtml(String(groupName))}</td>
|
<td>${escapeHtml(String(groupName))}</td>
|
||||||
<td>${escapeHtml(String(subjectName))}</td>
|
<td>${escapeHtml(String(subjectName))}</td>
|
||||||
<td>${escapeHtml(lessonTypeName)}</td>
|
<td>${escapeHtml(lessonTypeName)}</td>
|
||||||
@@ -313,7 +248,7 @@ export async function initDepartment() {
|
|||||||
</tr>`;
|
</tr>`;
|
||||||
if (hasError) {
|
if (hasError) {
|
||||||
row += `<tr style="background: rgba(239, 68, 68, 0.05);">
|
row += `<tr style="background: rgba(239, 68, 68, 0.05);">
|
||||||
<td colspan="9" style="color: var(--error); font-size: 0.85rem; padding: 0.4rem 0.85rem;">
|
<td colspan="10" style="color: var(--error); font-size: 0.85rem; padding: 0.4rem 0.85rem;">
|
||||||
⚠ ${escapeHtml(s._errorMsg)}
|
⚠ ${escapeHtml(s._errorMsg)}
|
||||||
</td>
|
</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
@@ -333,6 +268,8 @@ export async function initDepartment() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ===== Очистка полей формы (частичная) =====
|
// ===== Очистка полей формы (частичная) =====
|
||||||
|
// НЕ очищаем select'ы — они остаются заполненными для удобства.
|
||||||
|
// Пользователь сам изменит нужные поля для следующей записи.
|
||||||
function clearFormFields() {
|
function clearFormFields() {
|
||||||
document.getElementById('cs-hours').value = '';
|
document.getElementById('cs-hours').value = '';
|
||||||
document.getElementById('cs-division').checked = false;
|
document.getElementById('cs-division').checked = false;
|
||||||
@@ -346,39 +283,42 @@ export async function initDepartment() {
|
|||||||
const depId = csDepartmentIdInput.value;
|
const depId = csDepartmentIdInput.value;
|
||||||
const period = document.getElementById('cs-period').value;
|
const period = document.getElementById('cs-period').value;
|
||||||
const semesterType = document.querySelector('input[name="csSemesterType"]:checked')?.value;
|
const semesterType = document.querySelector('input[name="csSemesterType"]:checked')?.value;
|
||||||
|
const semester = document.getElementById('cs-semester').value;
|
||||||
const groupId = csGroupSelect.value;
|
const groupId = csGroupSelect.value;
|
||||||
const subjectId = csSubjectSelect.value;
|
const subjectId = csSubjectSelect.value;
|
||||||
const lessonTypeId = document.getElementById('cs-lesson-type').value;
|
const lessonTypeId = document.getElementById('cs-lesson-type').value;
|
||||||
const hours = document.getElementById('cs-hours').value;
|
const hours = document.getElementById('cs-hours').value;
|
||||||
const isDivision = document.getElementById('cs-division').checked;
|
const division = document.getElementById('cs-division').checked;
|
||||||
const teacherId = csTeacherSelect.value;
|
const teacherId = csTeacherSelect.value;
|
||||||
|
|
||||||
if (!period || !semesterType || !groupId || !subjectId || !lessonTypeId || !hours || !teacherId) {
|
if (!period || !semesterType || !semester || !groupId || !subjectId || !lessonTypeId || !hours || !teacherId) {
|
||||||
showAlert('create-schedule-alert', 'Заполните все обязательные поля', 'error');
|
showAlert('create-schedule-alert', 'Заполните все обязательные поля', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newRecord = {
|
const newRecord = {
|
||||||
departmentId: Number(depId),
|
departmentId: Number(depId),
|
||||||
|
semester: Number(semester),
|
||||||
groupId: Number(groupId),
|
groupId: Number(groupId),
|
||||||
subjectsId: Number(subjectId),
|
subjectsId: Number(subjectId),
|
||||||
lessonTypeId: Number(lessonTypeId),
|
lessonTypeId: Number(lessonTypeId),
|
||||||
numberOfHours: Number(hours),
|
numberOfHours: Number(hours),
|
||||||
isDivision: isDivision,
|
division: division,
|
||||||
teacherId: Number(teacherId),
|
teacherId: Number(teacherId),
|
||||||
semesterType: semesterType,
|
semesterType: semesterType,
|
||||||
period: period
|
period: period
|
||||||
};
|
};
|
||||||
|
|
||||||
// Проверка на дубликат
|
// Проверка на дубликат в уже добавленных записях
|
||||||
const isDuplicate = preparedSchedules.some(s =>
|
const isDuplicate = preparedSchedules.some(s =>
|
||||||
s.period === newRecord.period &&
|
s.period === newRecord.period &&
|
||||||
s.semesterType === newRecord.semesterType &&
|
s.semesterType === newRecord.semesterType &&
|
||||||
|
s.semester === newRecord.semester &&
|
||||||
s.groupId === newRecord.groupId &&
|
s.groupId === newRecord.groupId &&
|
||||||
s.subjectsId === newRecord.subjectsId &&
|
s.subjectsId === newRecord.subjectsId &&
|
||||||
s.lessonTypeId === newRecord.lessonTypeId &&
|
s.lessonTypeId === newRecord.lessonTypeId &&
|
||||||
s.numberOfHours === newRecord.numberOfHours &&
|
s.numberOfHours === newRecord.numberOfHours &&
|
||||||
s.isDivision === newRecord.isDivision &&
|
s.division === newRecord.division &&
|
||||||
s.teacherId === newRecord.teacherId
|
s.teacherId === newRecord.teacherId
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -392,7 +332,7 @@ export async function initDepartment() {
|
|||||||
clearFormFields();
|
clearFormFields();
|
||||||
|
|
||||||
showAlert('create-schedule-alert', 'Запись добавлена ✓', 'success');
|
showAlert('create-schedule-alert', 'Запись добавлена ✓', 'success');
|
||||||
setTimeout(() => hideAlert('create-schedule-alert'), 4000); // fix #1: 4 секунды
|
setTimeout(() => hideAlert('create-schedule-alert'), 2000);
|
||||||
|
|
||||||
renderPreparedSchedules();
|
renderPreparedSchedules();
|
||||||
updateTableVisibility();
|
updateTableVisibility();
|
||||||
@@ -420,6 +360,7 @@ export async function initDepartment() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Ошибка сохранения записи:', err);
|
console.error('Ошибка сохранения записи:', err);
|
||||||
errors++;
|
errors++;
|
||||||
|
// Помечаем запись как дубликат, если бэк вернул соответствующую ошибку
|
||||||
const isDuplicate = err.status === 409 ||
|
const isDuplicate = err.status === 409 ||
|
||||||
(err.message && err.message.toLowerCase().includes('уже существует'));
|
(err.message && err.message.toLowerCase().includes('уже существует'));
|
||||||
failedRecords.push({
|
failedRecords.push({
|
||||||
@@ -441,6 +382,7 @@ export async function initDepartment() {
|
|||||||
updateTableVisibility();
|
updateTableVisibility();
|
||||||
setTimeout(closeOverlay, 2000);
|
setTimeout(closeOverlay, 2000);
|
||||||
} else {
|
} else {
|
||||||
|
// Оставляем неудачные записи для повторной попытки / удаления
|
||||||
preparedSchedules = failedRecords;
|
preparedSchedules = failedRecords;
|
||||||
renderPreparedSchedules();
|
renderPreparedSchedules();
|
||||||
if (saved > 0) {
|
if (saved > 0) {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export async function initGroups() {
|
|||||||
populateEfSelects(educationForms);
|
populateEfSelects(educationForms);
|
||||||
await loadGroups();
|
await loadGroups();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Ошибка загрузки данных</td></tr>';
|
groupsTbody.innerHTML = '<tr><td colspan="9" class="loading-row">Ошибка загрузки данных</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export async function initGroups() {
|
|||||||
allGroups = await api.get('/api/groups');
|
allGroups = await api.get('/api/groups');
|
||||||
applyGroupFilter();
|
applyGroupFilter();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Ошибка загрузки</td></tr>';
|
groupsTbody.innerHTML = '<tr><td colspan="9" class="loading-row">Ошибка загрузки</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export async function initGroups() {
|
|||||||
|
|
||||||
function renderGroups(groups) {
|
function renderGroups(groups) {
|
||||||
if (!groups || !groups.length) {
|
if (!groups || !groups.length) {
|
||||||
groupsTbody.innerHTML = '<tr><td colspan="8" class="loading-row">Нет групп</td></tr>';
|
groupsTbody.innerHTML = '<tr><td colspan="9" class="loading-row">Нет групп</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
groupsTbody.innerHTML = groups.map(g => `
|
groupsTbody.innerHTML = groups.map(g => `
|
||||||
@@ -71,8 +71,9 @@ export async function initGroups() {
|
|||||||
<td>${escapeHtml(g.groupSize)}</td>
|
<td>${escapeHtml(g.groupSize)}</td>
|
||||||
<td><span class="badge badge-ef">${escapeHtml(g.educationFormName)}</span></td>
|
<td><span class="badge badge-ef">${escapeHtml(g.educationFormName)}</span></td>
|
||||||
<td>${g.departmentId || '-'}</td>
|
<td>${g.departmentId || '-'}</td>
|
||||||
|
<td>${g.enrollmentYear || '-'}</td>
|
||||||
<td>${g.course || '-'}</td>
|
<td>${g.course || '-'}</td>
|
||||||
<td>${escapeHtml(g.specialityCode || '-')}</td>
|
<td>${g.semester || '-'}</td>
|
||||||
<td><button class="btn-delete" data-id="${g.id}">Удалить</button></td>
|
<td><button class="btn-delete" data-id="${g.id}">Удалить</button></td>
|
||||||
</tr>`).join('');
|
</tr>`).join('');
|
||||||
}
|
}
|
||||||
@@ -84,15 +85,13 @@ export async function initGroups() {
|
|||||||
const groupSize = document.getElementById('new-group-size').value;
|
const groupSize = document.getElementById('new-group-size').value;
|
||||||
const educationFormId = newGroupEfSelect.value;
|
const educationFormId = newGroupEfSelect.value;
|
||||||
const departmentId = document.getElementById('new-group-department').value;
|
const departmentId = document.getElementById('new-group-department').value;
|
||||||
const course = document.getElementById('new-group-course').value;
|
const enrollmentYear = document.getElementById('new-group-enrollment-year').value;
|
||||||
const specialityCode = document.getElementById('new-group-speciality-code').value.trim();
|
|
||||||
|
|
||||||
if (!name) { showAlert('create-group-alert', 'Введите название группы', 'error'); return; }
|
if (!name) { showAlert('create-group-alert', 'Введите название группы', 'error'); return; }
|
||||||
if (!groupSize) { showAlert('create-group-alert', 'Введите размер группы', 'error'); return; }
|
if (!groupSize) { showAlert('create-group-alert', 'Введите размер группы', 'error'); return; }
|
||||||
if (!educationFormId) { showAlert('create-group-alert', 'Выберите форму обучения', 'error'); return; }
|
if (!educationFormId) { showAlert('create-group-alert', 'Выберите форму обучения', 'error'); return; }
|
||||||
if (!departmentId) { showAlert('create-group-alert', 'Введите ID кафедры', 'error'); return; }
|
if (!departmentId) { showAlert('create-group-alert', 'Введите ID кафедры', 'error'); return; }
|
||||||
if (!course) { showAlert('create-group-alert', 'Введите курс', 'error'); return; }
|
if (!enrollmentYear) { showAlert('create-group-alert', 'Введите год начала обучения', 'error'); return; }
|
||||||
if (!specialityCode) { showAlert('create-group-alert', 'Введите код специальности', 'error'); return; }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api.post('/api/groups', {
|
const data = await api.post('/api/groups', {
|
||||||
@@ -100,8 +99,7 @@ export async function initGroups() {
|
|||||||
groupSize: Number(groupSize),
|
groupSize: Number(groupSize),
|
||||||
educationFormId: Number(educationFormId),
|
educationFormId: Number(educationFormId),
|
||||||
departmentId: Number(departmentId),
|
departmentId: Number(departmentId),
|
||||||
course: Number(course),
|
enrollmentYear: Number(enrollmentYear)
|
||||||
specialityCode: specialityCode
|
|
||||||
});
|
});
|
||||||
showAlert('create-group-alert', `Группа "${escapeHtml(data.name || name)}" создана`, 'success');
|
showAlert('create-group-alert', `Группа "${escapeHtml(data.name || name)}" создана`, 'success');
|
||||||
createGroupForm.reset();
|
createGroupForm.reset();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { api } from '../api.js';
|
import { api } from '../api.js';
|
||||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
import { escapeHtml } from '../utils.js';
|
||||||
|
|
||||||
export async function initSchedule() {
|
export async function initSchedule() {
|
||||||
const tbody = document.getElementById('schedule-tbody');
|
const tbody = document.getElementById('schedule-tbody');
|
||||||
@@ -20,6 +20,7 @@ export async function initSchedule() {
|
|||||||
|
|
||||||
// ===================== Фильтрация =====================
|
// ===================== Фильтрация =====================
|
||||||
|
|
||||||
|
// Извлечение отображаемого значения поля для фильтрации
|
||||||
function getDisplayValue(lesson, key) {
|
function getDisplayValue(lesson, key) {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'teacher':
|
case 'teacher':
|
||||||
@@ -37,17 +38,20 @@ export async function initSchedule() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Собрать уникальные значения из данных
|
||||||
function getUniqueValues(key) {
|
function getUniqueValues(key) {
|
||||||
const vals = new Set();
|
const vals = new Set();
|
||||||
lessonsData.forEach(lesson => {
|
lessonsData.forEach(lesson => {
|
||||||
vals.add(getDisplayValue(lesson, key));
|
vals.add(getDisplayValue(lesson, key));
|
||||||
});
|
});
|
||||||
|
// Для дней — сортируем по порядку
|
||||||
if (key === 'day') {
|
if (key === 'day') {
|
||||||
return [...vals].sort((a, b) => (dayOrder[a.toLowerCase()] ?? 99) - (dayOrder[b.toLowerCase()] ?? 99));
|
return [...vals].sort((a, b) => (dayOrder[a.toLowerCase()] ?? 99) - (dayOrder[b.toLowerCase()] ?? 99));
|
||||||
}
|
}
|
||||||
return [...vals].sort((a, b) => a.localeCompare(b, 'ru'));
|
return [...vals].sort((a, b) => a.localeCompare(b, 'ru'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Применить все фильтры
|
||||||
function applyFilters(lessons) {
|
function applyFilters(lessons) {
|
||||||
return lessons.filter(lesson => {
|
return lessons.filter(lesson => {
|
||||||
for (const key of Object.keys(activeFilters)) {
|
for (const key of Object.keys(activeFilters)) {
|
||||||
@@ -75,6 +79,7 @@ export async function initSchedule() {
|
|||||||
|
|
||||||
function onDocumentClick(e) {
|
function onDocumentClick(e) {
|
||||||
if (currentPopup && !currentPopup.contains(e.target)) {
|
if (currentPopup && !currentPopup.contains(e.target)) {
|
||||||
|
// Проверяем, не кликнули ли по иконке фильтра
|
||||||
if (!e.target.closest('.filter-icon')) {
|
if (!e.target.closest('.filter-icon')) {
|
||||||
closePopup();
|
closePopup();
|
||||||
}
|
}
|
||||||
@@ -82,6 +87,7 @@ export async function initSchedule() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openFilterPopup(th, filterKey) {
|
function openFilterPopup(th, filterKey) {
|
||||||
|
// Если уже открыт этот же — закрыть
|
||||||
if (currentPopup && currentPopup.dataset.filterKey === filterKey) {
|
if (currentPopup && currentPopup.dataset.filterKey === filterKey) {
|
||||||
closePopup();
|
closePopup();
|
||||||
return;
|
return;
|
||||||
@@ -91,16 +97,19 @@ export async function initSchedule() {
|
|||||||
const uniqueValues = getUniqueValues(filterKey);
|
const uniqueValues = getUniqueValues(filterKey);
|
||||||
const currentFilter = activeFilters[filterKey];
|
const currentFilter = activeFilters[filterKey];
|
||||||
|
|
||||||
|
// Создаём попап
|
||||||
const popup = document.createElement('div');
|
const popup = document.createElement('div');
|
||||||
popup.className = 'filter-popup';
|
popup.className = 'filter-popup';
|
||||||
popup.dataset.filterKey = filterKey;
|
popup.dataset.filterKey = filterKey;
|
||||||
|
|
||||||
|
// Поисковое поле
|
||||||
const searchInput = document.createElement('input');
|
const searchInput = document.createElement('input');
|
||||||
searchInput.type = 'text';
|
searchInput.type = 'text';
|
||||||
searchInput.className = 'filter-search';
|
searchInput.className = 'filter-search';
|
||||||
searchInput.placeholder = 'Поиск...';
|
searchInput.placeholder = 'Поиск...';
|
||||||
popup.appendChild(searchInput);
|
popup.appendChild(searchInput);
|
||||||
|
|
||||||
|
// Кнопки «Выбрать все» / «Сбросить»
|
||||||
const btnRow = document.createElement('div');
|
const btnRow = document.createElement('div');
|
||||||
btnRow.className = 'filter-btn-row';
|
btnRow.className = 'filter-btn-row';
|
||||||
|
|
||||||
@@ -124,6 +133,7 @@ export async function initSchedule() {
|
|||||||
btnRow.appendChild(btnNone);
|
btnRow.appendChild(btnNone);
|
||||||
popup.appendChild(btnRow);
|
popup.appendChild(btnRow);
|
||||||
|
|
||||||
|
// Список чекбоксов
|
||||||
const listWrap = document.createElement('div');
|
const listWrap = document.createElement('div');
|
||||||
listWrap.className = 'filter-list';
|
listWrap.className = 'filter-list';
|
||||||
|
|
||||||
@@ -136,6 +146,7 @@ export async function initSchedule() {
|
|||||||
const cb = document.createElement('input');
|
const cb = document.createElement('input');
|
||||||
cb.type = 'checkbox';
|
cb.type = 'checkbox';
|
||||||
cb.value = val;
|
cb.value = val;
|
||||||
|
// Если фильтр активен — отмечаем только выбранные; если нет — все отмечены
|
||||||
cb.checked = currentFilter ? currentFilter.has(val) : true;
|
cb.checked = currentFilter ? currentFilter.has(val) : true;
|
||||||
|
|
||||||
const span = document.createElement('span');
|
const span = document.createElement('span');
|
||||||
@@ -149,6 +160,7 @@ export async function initSchedule() {
|
|||||||
|
|
||||||
popup.appendChild(listWrap);
|
popup.appendChild(listWrap);
|
||||||
|
|
||||||
|
// Кнопка «Применить»
|
||||||
const btnApply = document.createElement('button');
|
const btnApply = document.createElement('button');
|
||||||
btnApply.className = 'filter-btn-apply';
|
btnApply.className = 'filter-btn-apply';
|
||||||
btnApply.textContent = 'Применить';
|
btnApply.textContent = 'Применить';
|
||||||
@@ -159,6 +171,7 @@ export async function initSchedule() {
|
|||||||
if (cb.checked) selected.add(cb.value);
|
if (cb.checked) selected.add(cb.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Если все выбраны — снимаем фильтр
|
||||||
if (selected.size === uniqueValues.length) {
|
if (selected.size === uniqueValues.length) {
|
||||||
delete activeFilters[filterKey];
|
delete activeFilters[filterKey];
|
||||||
th.classList.remove('filter-active');
|
th.classList.remove('filter-active');
|
||||||
@@ -172,6 +185,7 @@ export async function initSchedule() {
|
|||||||
});
|
});
|
||||||
popup.appendChild(btnApply);
|
popup.appendChild(btnApply);
|
||||||
|
|
||||||
|
// Поиск по чекбоксам
|
||||||
searchInput.addEventListener('input', () => {
|
searchInput.addEventListener('input', () => {
|
||||||
const query = searchInput.value.toLowerCase();
|
const query = searchInput.value.toLowerCase();
|
||||||
listWrap.querySelectorAll('.filter-item').forEach(item => {
|
listWrap.querySelectorAll('.filter-item').forEach(item => {
|
||||||
@@ -180,22 +194,28 @@ export async function initSchedule() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Предотвращаем всплытие кликов внутри попапа (чтобы не срабатывала сортировка th)
|
||||||
popup.addEventListener('click', (e) => e.stopPropagation());
|
popup.addEventListener('click', (e) => e.stopPropagation());
|
||||||
searchInput.addEventListener('click', (e) => e.stopPropagation());
|
searchInput.addEventListener('click', (e) => e.stopPropagation());
|
||||||
|
|
||||||
|
// Позиционируем попап под th
|
||||||
th.style.position = 'relative';
|
th.style.position = 'relative';
|
||||||
th.appendChild(popup);
|
th.appendChild(popup);
|
||||||
currentPopup = popup;
|
currentPopup = popup;
|
||||||
|
|
||||||
|
// Фокус на поиск
|
||||||
setTimeout(() => searchInput.focus(), 50);
|
setTimeout(() => searchInput.focus(), 50);
|
||||||
|
|
||||||
|
// Закрытие по клику вне
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.addEventListener('click', onDocumentClick, true);
|
document.addEventListener('click', onDocumentClick, true);
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Обработчики кликов по заголовкам с фильтрами (клик по всей ячейке)
|
||||||
table.querySelectorAll('thead th.filterable').forEach(th => {
|
table.querySelectorAll('thead th.filterable').forEach(th => {
|
||||||
th.addEventListener('click', (e) => {
|
th.addEventListener('click', (e) => {
|
||||||
|
// Не открываем попап при клике внутри самого попапа
|
||||||
if (e.target.closest('.filter-popup')) return;
|
if (e.target.closest('.filter-popup')) return;
|
||||||
const filterKey = th.dataset.filterKey;
|
const filterKey = th.dataset.filterKey;
|
||||||
openFilterPopup(th, filterKey);
|
openFilterPopup(th, filterKey);
|
||||||
@@ -229,6 +249,7 @@ export async function initSchedule() {
|
|||||||
case 'week':
|
case 'week':
|
||||||
return (lesson.week || '').toLowerCase();
|
return (lesson.week || '').toLowerCase();
|
||||||
case 'time': {
|
case 'time': {
|
||||||
|
// Составной ключ: день + время для правильной сортировки
|
||||||
const d = (lesson.day || '').toLowerCase();
|
const d = (lesson.day || '').toLowerCase();
|
||||||
const dayNum = dayOrder[d] ?? 99;
|
const dayNum = dayOrder[d] ?? 99;
|
||||||
const t = lesson.time || '99:99';
|
const t = lesson.time || '99:99';
|
||||||
@@ -266,8 +287,10 @@ export async function initSchedule() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Навешиваем обработчики клика на заголовки (сортировка)
|
||||||
table.querySelectorAll('thead th.sortable').forEach(th => {
|
table.querySelectorAll('thead th.sortable').forEach(th => {
|
||||||
th.addEventListener('click', (e) => {
|
th.addEventListener('click', (e) => {
|
||||||
|
// Не сортируем, если кликнули по иконке фильтра или внутри попапа
|
||||||
if (e.target.closest('.filter-icon') || e.target.closest('.filter-popup')) return;
|
if (e.target.closest('.filter-icon') || e.target.closest('.filter-popup')) return;
|
||||||
|
|
||||||
const key = th.dataset.sortKey;
|
const key = th.dataset.sortKey;
|
||||||
@@ -287,7 +310,7 @@ export async function initSchedule() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ===================== Загрузка и рендер таблицы =====================
|
// ===================== Загрузка и рендер =====================
|
||||||
|
|
||||||
async function loadSchedule() {
|
async function loadSchedule() {
|
||||||
try {
|
try {
|
||||||
@@ -295,20 +318,21 @@ export async function initSchedule() {
|
|||||||
lessonsData = lessons;
|
lessonsData = lessons;
|
||||||
renderSchedule(lessons);
|
renderSchedule(lessons);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
tbody.innerHTML = `<tr><td colspan="11" class="loading-row">Ошибка загрузки: ${escapeHtml(e.message)}</td></tr>`;
|
tbody.innerHTML = `<tr><td colspan="8" class="loading-row">Ошибка загрузки: ${escapeHtml(e.message)}</td></tr>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSchedule(lessons) {
|
function renderSchedule(lessons) {
|
||||||
if (!lessons || !lessons.length) {
|
if (!lessons || !lessons.length) {
|
||||||
tbody.innerHTML = '<tr><td colspan="11" class="loading-row">Нет занятий</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="8" class="loading-row">Нет занятий</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сначала фильтруем, потом сортируем
|
||||||
const filtered = applyFilters(lessons);
|
const filtered = applyFilters(lessons);
|
||||||
|
|
||||||
if (!filtered.length) {
|
if (!filtered.length) {
|
||||||
tbody.innerHTML = '<tr><td colspan="11" class="loading-row">Нет занятий по выбранным фильтрам</td></tr>';
|
tbody.innerHTML = '<tr><td colspan="8" class="loading-row">Нет занятий по выбранным фильтрам</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,343 +366,5 @@ export async function initSchedule() {
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== Модалки добавления занятия =====================
|
|
||||||
|
|
||||||
const overlay = document.getElementById('sch-overlay');
|
|
||||||
const modalForm = document.getElementById('sch-modal-form');
|
|
||||||
const modalLessons = document.getElementById('sch-modal-lessons');
|
|
||||||
const btnAddLesson = document.getElementById('sch-btn-add-lesson');
|
|
||||||
const btnClose = document.getElementById('sch-modal-close');
|
|
||||||
const addForm = document.getElementById('sch-add-lesson-form');
|
|
||||||
|
|
||||||
const schTeacherSelect = document.getElementById('sch-teacher');
|
|
||||||
const schGroupSelect = document.getElementById('sch-group');
|
|
||||||
const schDisciplineSelect = document.getElementById('sch-discipline');
|
|
||||||
const schClassroomSelect = document.getElementById('sch-classroom');
|
|
||||||
const schDaySelect = document.getElementById('sch-day');
|
|
||||||
const schTimeSelect = document.getElementById('sch-time');
|
|
||||||
const schTypeSelect = document.getElementById('sch-type');
|
|
||||||
const schWeekUpper = document.getElementById('sch-week-upper');
|
|
||||||
const schWeekLower = document.getElementById('sch-week-lower');
|
|
||||||
const schFormatOffline = document.getElementById('sch-format-offline');
|
|
||||||
|
|
||||||
const schTeacherName = document.getElementById('sch-teacher-name');
|
|
||||||
const schLessonsContainer = document.getElementById('sch-lessons-container');
|
|
||||||
|
|
||||||
let groups = [];
|
|
||||||
let subjects = [];
|
|
||||||
let classrooms = [];
|
|
||||||
let teachers = [];
|
|
||||||
|
|
||||||
const weekdaysTimes = [
|
|
||||||
"8:00-9:30", "9:40-11:10", "11:40-13:10",
|
|
||||||
"13:20-14:50", "15:00-16:30", "16:50-18:20", "18:30-19:00"
|
|
||||||
];
|
|
||||||
const saturdayTimes = [
|
|
||||||
"8:20-9:50", "10:00-11:30", "11:40-13:10", "13:20-14:50"
|
|
||||||
];
|
|
||||||
|
|
||||||
// ===== Загрузка справочников =====
|
|
||||||
async function loadGroups() {
|
|
||||||
try {
|
|
||||||
groups = await api.get('/api/groups');
|
|
||||||
schGroupSelect.innerHTML = '<option value="">Выберите группу</option>' +
|
|
||||||
groups.map(g => {
|
|
||||||
let text = escapeHtml(g.name);
|
|
||||||
if (g.groupSize) text += ` (числ: ${g.groupSize} чел.)`;
|
|
||||||
return `<option value="${g.id}">${text}</option>`;
|
|
||||||
}).join('');
|
|
||||||
} catch (e) { console.error('Ошибка загрузки групп:', e); }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadSubjects() {
|
|
||||||
try {
|
|
||||||
subjects = await api.get('/api/subjects');
|
|
||||||
schDisciplineSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
|
|
||||||
subjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
|
|
||||||
} catch (e) { console.error('Ошибка загрузки дисциплин:', e); }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadClassrooms() {
|
|
||||||
try {
|
|
||||||
classrooms = await api.get('/api/classrooms');
|
|
||||||
renderClassroomOptions();
|
|
||||||
} catch (e) { console.error('Ошибка загрузки аудиторий:', e); }
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadTeachers() {
|
|
||||||
try {
|
|
||||||
teachers = await api.get('/api/users/teachers');
|
|
||||||
schTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' +
|
|
||||||
teachers.map(t => `<option value="${t.id}">${escapeHtml(t.fullName || t.username)}</option>`).join('');
|
|
||||||
} catch (e) { console.error('Ошибка загрузки преподавателей:', e); }
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderClassroomOptions() {
|
|
||||||
if (!classrooms || classrooms.length === 0) {
|
|
||||||
schClassroomSelect.innerHTML = '<option value="">Нет доступных аудиторий</option>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selectedGroupId = schGroupSelect.value;
|
|
||||||
const selectedGroup = groups?.find(g => g.id == selectedGroupId);
|
|
||||||
const groupSize = selectedGroup?.groupSize || 0;
|
|
||||||
|
|
||||||
schClassroomSelect.innerHTML = '<option value="">Выберите аудиторию</option>' +
|
|
||||||
classrooms.map(c => {
|
|
||||||
let text = escapeHtml(c.name);
|
|
||||||
if (c.capacity) text += ` (вместимость: ${c.capacity} чел.)`;
|
|
||||||
if (c.isAvailable === false) {
|
|
||||||
text += ` ❌ Занята`;
|
|
||||||
} else if (selectedGroupId && groupSize > 0 && c.capacity && groupSize > c.capacity) {
|
|
||||||
text += ` ⚠️ Недостаточно места`;
|
|
||||||
}
|
|
||||||
return `<option value="${c.id}">${text}</option>`;
|
|
||||||
}).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
schGroupSelect.addEventListener('change', () => renderClassroomOptions());
|
|
||||||
|
|
||||||
function updateTimeOptions(dayValue) {
|
|
||||||
let times = [];
|
|
||||||
if (dayValue === "Суббота") {
|
|
||||||
times = saturdayTimes;
|
|
||||||
} else if (dayValue && dayValue !== '') {
|
|
||||||
times = weekdaysTimes;
|
|
||||||
} else {
|
|
||||||
schTimeSelect.innerHTML = '<option value="">Сначала выберите день</option>';
|
|
||||||
schTimeSelect.disabled = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
schTimeSelect.innerHTML = '<option value="">Выберите время</option>' +
|
|
||||||
times.map(t => `<option value="${t}">${t}</option>`).join('');
|
|
||||||
schTimeSelect.disabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
schDaySelect.addEventListener('change', function () {
|
|
||||||
updateTimeOptions(this.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== Автозаполнение преподавателя из фильтра =====
|
|
||||||
function getFilteredTeacherId() {
|
|
||||||
const teacherFilter = activeFilters['teacher'];
|
|
||||||
if (teacherFilter && teacherFilter.size === 1) {
|
|
||||||
const teacherName = [...teacherFilter][0];
|
|
||||||
// Сопоставляем по username, fullName и их комбинациям
|
|
||||||
const match = teachers.find(t =>
|
|
||||||
t.username === teacherName ||
|
|
||||||
t.fullName === teacherName ||
|
|
||||||
(t.fullName || t.username) === teacherName
|
|
||||||
);
|
|
||||||
return match ? String(match.id) : '';
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Загрузка занятий преподавателя =====
|
|
||||||
async function loadTeacherLessons(teacherId) {
|
|
||||||
const teacher = teachers.find(t => t.id == teacherId);
|
|
||||||
const name = teacher ? (teacher.fullName || teacher.username) : '';
|
|
||||||
schTeacherName.textContent = name
|
|
||||||
? `Занятия преподавателя: ${name}`
|
|
||||||
: 'Занятия преподавателя';
|
|
||||||
|
|
||||||
modalLessons.style.display = '';
|
|
||||||
schLessonsContainer.innerHTML = '<div class="loading-lessons">Загрузка занятий...</div>';
|
|
||||||
|
|
||||||
try {
|
|
||||||
const lessons = await api.get(`/api/users/lessons/${teacherId}`);
|
|
||||||
|
|
||||||
if (!lessons || !Array.isArray(lessons) || lessons.length === 0) {
|
|
||||||
schLessonsContainer.innerHTML = '<div class="no-lessons">У преподавателя пока нет занятий</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const daysOrder = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'];
|
|
||||||
const lessonsByDay = {};
|
|
||||||
lessons.forEach(l => {
|
|
||||||
if (!lessonsByDay[l.day]) lessonsByDay[l.day] = [];
|
|
||||||
lessonsByDay[l.day].push(l);
|
|
||||||
});
|
|
||||||
Object.keys(lessonsByDay).forEach(day => {
|
|
||||||
lessonsByDay[day].sort((a, b) => a.time.localeCompare(b.time));
|
|
||||||
});
|
|
||||||
|
|
||||||
let html = '';
|
|
||||||
daysOrder.forEach(day => {
|
|
||||||
if (!lessonsByDay[day]) return;
|
|
||||||
html += `<div class="lesson-day-divider">${day}</div>`;
|
|
||||||
lessonsByDay[day].forEach(lesson => {
|
|
||||||
html += `
|
|
||||||
<div class="lesson-card">
|
|
||||||
<div class="lesson-card-header">
|
|
||||||
<span class="lesson-group">${escapeHtml(lesson.groupName)}</span>
|
|
||||||
<span class="lesson-time">${escapeHtml(lesson.time)}</span>
|
|
||||||
</div>
|
|
||||||
<div class="lesson-card-body">
|
|
||||||
<div class="lesson-subject">${escapeHtml(lesson.subjectName)}</div>
|
|
||||||
<div class="lesson-details">
|
|
||||||
<span class="lesson-detail-item">${escapeHtml(lesson.typeLesson)}</span>
|
|
||||||
<span class="lesson-detail-item">${escapeHtml(lesson.lessonFormat)}</span>
|
|
||||||
<span class="lesson-detail-item">${escapeHtml(lesson.week)}</span>
|
|
||||||
<span class="lesson-detail-item">${escapeHtml(lesson.classroomName)}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
schLessonsContainer.innerHTML = html;
|
|
||||||
} catch (e) {
|
|
||||||
schLessonsContainer.innerHTML = `<div class="no-lessons">Ошибка загрузки: ${escapeHtml(e.message)}</div>`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== При смене преподавателя — подгрузить его занятия =====
|
|
||||||
schTeacherSelect.addEventListener('change', function () {
|
|
||||||
const teacherId = this.value;
|
|
||||||
if (teacherId) {
|
|
||||||
loadTeacherLessons(teacherId);
|
|
||||||
} else {
|
|
||||||
modalLessons.style.display = 'none';
|
|
||||||
schLessonsContainer.innerHTML = '<div class="no-lessons">Выберите преподавателя для просмотра занятий</div>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== Открытие / закрытие оверлея =====
|
|
||||||
function openOverlay() {
|
|
||||||
// Автозаполнение преподавателя из фильтра таблицы
|
|
||||||
const autoTeacherId = getFilteredTeacherId();
|
|
||||||
if (autoTeacherId) {
|
|
||||||
schTeacherSelect.value = autoTeacherId;
|
|
||||||
loadTeacherLessons(autoTeacherId);
|
|
||||||
}
|
|
||||||
|
|
||||||
overlay.classList.add('open');
|
|
||||||
}
|
|
||||||
|
|
||||||
function closeOverlay() {
|
|
||||||
overlay.classList.remove('open');
|
|
||||||
resetForm();
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetForm() {
|
|
||||||
addForm.reset();
|
|
||||||
schTeacherSelect.value = '';
|
|
||||||
schGroupSelect.value = '';
|
|
||||||
schDisciplineSelect.value = '';
|
|
||||||
schClassroomSelect.value = '';
|
|
||||||
schDaySelect.value = '';
|
|
||||||
schTypeSelect.value = '';
|
|
||||||
schTimeSelect.innerHTML = '<option value="">Сначала выберите день</option>';
|
|
||||||
schTimeSelect.disabled = true;
|
|
||||||
if (schWeekUpper) schWeekUpper.checked = false;
|
|
||||||
if (schWeekLower) schWeekLower.checked = false;
|
|
||||||
if (schFormatOffline) schFormatOffline.checked = true;
|
|
||||||
modalLessons.style.display = 'none';
|
|
||||||
schLessonsContainer.innerHTML = '<div class="no-lessons">Выберите преподавателя для просмотра занятий</div>';
|
|
||||||
hideAlert('sch-add-alert');
|
|
||||||
}
|
|
||||||
|
|
||||||
btnAddLesson.addEventListener('click', openOverlay);
|
|
||||||
btnClose.addEventListener('click', closeOverlay);
|
|
||||||
|
|
||||||
// Закрытие по клику на оверлей (мимо модалок)
|
|
||||||
overlay.addEventListener('click', (e) => {
|
|
||||||
if (e.target === overlay || e.target.classList.contains('cs-overlay-scroll')) {
|
|
||||||
closeOverlay();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Закрытие по Escape
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape' && overlay.classList.contains('open')) {
|
|
||||||
closeOverlay();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===== Отправка формы =====
|
|
||||||
addForm.addEventListener('submit', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
hideAlert('sch-add-alert');
|
|
||||||
|
|
||||||
const teacherId = schTeacherSelect.value;
|
|
||||||
const groupId = schGroupSelect.value;
|
|
||||||
const subjectId = schDisciplineSelect.value;
|
|
||||||
const classroomId = schClassroomSelect.value;
|
|
||||||
const lessonType = schTypeSelect.value;
|
|
||||||
const dayOfWeek = schDaySelect.value;
|
|
||||||
const timeSlot = schTimeSelect.value;
|
|
||||||
const lessonFormat = document.querySelector('input[name="schLessonFormat"]:checked')?.value;
|
|
||||||
|
|
||||||
if (!teacherId) { showAlert('sch-add-alert', 'Выберите преподавателя', 'error'); return; }
|
|
||||||
if (!groupId) { showAlert('sch-add-alert', 'Выберите группу', 'error'); return; }
|
|
||||||
if (!subjectId) { showAlert('sch-add-alert', 'Выберите дисциплину', 'error'); return; }
|
|
||||||
if (!classroomId) { showAlert('sch-add-alert', 'Выберите аудиторию', 'error'); return; }
|
|
||||||
if (!dayOfWeek) { showAlert('sch-add-alert', 'Выберите день недели', 'error'); return; }
|
|
||||||
if (!timeSlot) { showAlert('sch-add-alert', 'Выберите время', 'error'); return; }
|
|
||||||
|
|
||||||
const weekUpperChecked = schWeekUpper?.checked || false;
|
|
||||||
const weekLowerChecked = schWeekLower?.checked || false;
|
|
||||||
|
|
||||||
if (!weekUpperChecked && !weekLowerChecked) {
|
|
||||||
showAlert('sch-add-alert', 'Не выбран тип недели', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let weekType = null;
|
|
||||||
if (weekUpperChecked && weekLowerChecked) weekType = 'Обе';
|
|
||||||
else if (weekUpperChecked) weekType = 'Верхняя';
|
|
||||||
else if (weekLowerChecked) weekType = 'Нижняя';
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.post('/api/users/lessons/create', {
|
|
||||||
teacherId: parseInt(teacherId),
|
|
||||||
groupId: parseInt(groupId),
|
|
||||||
subjectId: parseInt(subjectId),
|
|
||||||
classroomId: parseInt(classroomId),
|
|
||||||
typeLesson: lessonType,
|
|
||||||
lessonFormat: lessonFormat,
|
|
||||||
day: dayOfWeek,
|
|
||||||
week: weekType,
|
|
||||||
time: timeSlot
|
|
||||||
});
|
|
||||||
|
|
||||||
showAlert('sch-add-alert', 'Занятие добавлено ✓', 'success');
|
|
||||||
|
|
||||||
// Очистить все поля кроме преподавателя (для массового добавления)
|
|
||||||
schGroupSelect.selectedIndex = 0;
|
|
||||||
schDisciplineSelect.selectedIndex = 0;
|
|
||||||
schClassroomSelect.selectedIndex = 0;
|
|
||||||
schTypeSelect.selectedIndex = 0;
|
|
||||||
schDaySelect.selectedIndex = 0;
|
|
||||||
schTimeSelect.innerHTML = '<option value="">Сначала выберите день</option>';
|
|
||||||
schTimeSelect.disabled = true;
|
|
||||||
schWeekUpper.checked = false;
|
|
||||||
schWeekLower.checked = false;
|
|
||||||
document.querySelector('input[name="schLessonFormat"][value="Очно"]').checked = true;
|
|
||||||
|
|
||||||
// Обновить занятия преподавателя в модалке 2
|
|
||||||
if (teacherId) {
|
|
||||||
await loadTeacherLessons(teacherId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Обновить основную таблицу
|
|
||||||
await loadSchedule();
|
await loadSchedule();
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
hideAlert('sch-add-alert');
|
|
||||||
}, 4000);
|
|
||||||
} catch (err) {
|
|
||||||
showAlert('sch-add-alert', err.message || 'Ошибка добавления занятия', 'error');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===================== Инициализация =====================
|
|
||||||
await Promise.all([
|
|
||||||
loadSchedule(),
|
|
||||||
loadGroups(),
|
|
||||||
loadSubjects(),
|
|
||||||
loadClassrooms(),
|
|
||||||
loadTeachers()
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
@@ -7,9 +7,7 @@ const ROLE_BADGE = { ADMIN: 'badge-admin', TEACHER: 'badge-teacher', STUDENT: 'b
|
|||||||
export async function initUsers() {
|
export async function initUsers() {
|
||||||
const usersTbody = document.getElementById('users-tbody');
|
const usersTbody = document.getElementById('users-tbody');
|
||||||
const createForm = document.getElementById('create-form');
|
const createForm = document.getElementById('create-form');
|
||||||
|
const modalBackdrop = document.getElementById('modal-backdrop');
|
||||||
// ===== Оверлей (cs-overlay) =====
|
|
||||||
const usersOverlay = document.getElementById('users-overlay');
|
|
||||||
|
|
||||||
// ===== 1-е модальное окно: Добавить занятие =====
|
// ===== 1-е модальное окно: Добавить занятие =====
|
||||||
const modalAddLesson = document.getElementById('modal-add-lesson');
|
const modalAddLesson = document.getElementById('modal-add-lesson');
|
||||||
@@ -30,6 +28,7 @@ export async function initUsers() {
|
|||||||
|
|
||||||
// ===== 2-е модальное окно: Просмотр занятий =====
|
// ===== 2-е модальное окно: Просмотр занятий =====
|
||||||
const modalViewLessons = document.getElementById('modal-view-lessons');
|
const modalViewLessons = document.getElementById('modal-view-lessons');
|
||||||
|
const modalViewLessonsClose = document.getElementById('modal-view-lessons-close');
|
||||||
const lessonsContainer = document.getElementById('lessons-container');
|
const lessonsContainer = document.getElementById('lessons-container');
|
||||||
const modalTeacherName = document.getElementById('modal-teacher-name');
|
const modalTeacherName = document.getElementById('modal-teacher-name');
|
||||||
|
|
||||||
@@ -57,6 +56,36 @@ export async function initUsers() {
|
|||||||
"13:20-14:50"
|
"13:20-14:50"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// СИНХРОНИЗАЦИЯ ВЫСОТЫ 1-й МОДАЛКИ -> CSS переменная
|
||||||
|
// =========================================================
|
||||||
|
const addLessonContent = document.querySelector('#modal-add-lesson .modal-content');
|
||||||
|
|
||||||
|
function setAddLessonHeightVar(px) {
|
||||||
|
const h = Math.max(0, Math.ceil(px || 0));
|
||||||
|
document.documentElement.style.setProperty('--add-lesson-height', `${h}px`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncAddLessonHeight() {
|
||||||
|
if (!addLessonContent) return;
|
||||||
|
|
||||||
|
if (!modalAddLesson?.classList.contains('open')) {
|
||||||
|
// если первая модалка закрыта — "шапки" нет
|
||||||
|
setAddLessonHeightVar(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setAddLessonHeightVar(addLessonContent.getBoundingClientRect().height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Авто-обновление при любом изменении размеров первой модалки
|
||||||
|
if (addLessonContent && 'ResizeObserver' in window) {
|
||||||
|
const ro = new ResizeObserver(() => syncAddLessonHeight());
|
||||||
|
ro.observe(addLessonContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => syncAddLessonHeight());
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// Загрузка справочников
|
// Загрузка справочников
|
||||||
// =========================================================
|
// =========================================================
|
||||||
@@ -196,15 +225,25 @@ export async function initUsers() {
|
|||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Открытие / закрытие оверлея =====
|
function updateBackdrop() {
|
||||||
function openOverlay() {
|
if(!modalBackdrop) return;
|
||||||
if (usersOverlay) usersOverlay.classList.add('open');
|
const anyOpen =
|
||||||
|
modalAddLesson?.classList.contains('open') ||
|
||||||
|
modalViewLessons?.classList.contains('open');
|
||||||
|
|
||||||
|
modalBackdrop.classList.toggle('open', anyOpen);
|
||||||
}
|
}
|
||||||
function closeOverlay() {
|
// Клик мимо модалок закроет их, если не надо, то закомментить этот код
|
||||||
if (usersOverlay) usersOverlay.classList.remove('open');
|
modalBackdrop?.addEventListener('click', () => {
|
||||||
if (modalViewLessons) modalViewLessons.style.display = 'none';
|
if (modalAddLesson?.classList.contains('open')) {
|
||||||
|
modalAddLesson.classList.remove('open');
|
||||||
resetLessonForm();
|
resetLessonForm();
|
||||||
|
syncAddLessonHeight();
|
||||||
}
|
}
|
||||||
|
if (modalViewLessons?.classList.contains('open')) {
|
||||||
|
closeViewLessonsModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// =========================================================
|
// =========================================================
|
||||||
// 1-я модалка: добавление занятия
|
// 1-я модалка: добавление занятия
|
||||||
@@ -231,7 +270,9 @@ export async function initUsers() {
|
|||||||
lessonDaySelect.value = '';
|
lessonDaySelect.value = '';
|
||||||
updateTimeOptions('');
|
updateTimeOptions('');
|
||||||
|
|
||||||
openOverlay();
|
modalAddLesson.classList.add('open');
|
||||||
|
updateBackdrop();
|
||||||
|
requestAnimationFrame(() => syncAddLessonHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
addLessonForm.addEventListener('submit', async (e) => {
|
addLessonForm.addEventListener('submit', async (e) => {
|
||||||
@@ -248,20 +289,15 @@ export async function initUsers() {
|
|||||||
|
|
||||||
const lessonFormat = document.querySelector('input[name="lessonFormat"]:checked')?.value;
|
const lessonFormat = document.querySelector('input[name="lessonFormat"]:checked')?.value;
|
||||||
|
|
||||||
if (!groupId) { showAlert('add-lesson-alert', 'Выберите группу', 'error'); return; }
|
if (!groupId) { showAlert('add-lesson-alert', 'Выберите группу', 'error'); requestAnimationFrame(() => syncAddLessonHeight()); return; }
|
||||||
if (!subjectId) { showAlert('add-lesson-alert', 'Выберите дисциплину', 'error'); return; }
|
if (!subjectId) { showAlert('add-lesson-alert', 'Выберите дисциплину', 'error'); requestAnimationFrame(() => syncAddLessonHeight()); return; }
|
||||||
if (!classroomId) { showAlert('add-lesson-alert', 'Выберите аудиторию', 'error'); return; }
|
if (!classroomId) { showAlert('add-lesson-alert', 'Выберите аудиторию', 'error'); requestAnimationFrame(() => syncAddLessonHeight()); return; }
|
||||||
if (!dayOfWeek) { showAlert('add-lesson-alert', 'Выберите день недели', 'error'); return; }
|
if (!dayOfWeek) { showAlert('add-lesson-alert', 'Выберите день недели', 'error'); requestAnimationFrame(() => syncAddLessonHeight()); return; }
|
||||||
if (!timeSlot) { showAlert('add-lesson-alert', 'Выберите время', 'error'); return; }
|
if (!timeSlot) { showAlert('add-lesson-alert', 'Выберите время', 'error'); requestAnimationFrame(() => syncAddLessonHeight()); return; }
|
||||||
|
|
||||||
const weekUpperChecked = weekUpper?.checked || false;
|
const weekUpperChecked = weekUpper?.checked || false;
|
||||||
const weekLowerChecked = weekLower?.checked || false;
|
const weekLowerChecked = weekLower?.checked || false;
|
||||||
|
|
||||||
if (!weekUpperChecked && !weekLowerChecked) {
|
|
||||||
showAlert('add-lesson-alert', 'Не выбран тип недели', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let weekType = null;
|
let weekType = null;
|
||||||
if (weekUpperChecked && weekLowerChecked) weekType = 'Обе';
|
if (weekUpperChecked && weekLowerChecked) weekType = 'Обе';
|
||||||
else if (weekUpperChecked) weekType = 'Верхняя';
|
else if (weekUpperChecked) weekType = 'Верхняя';
|
||||||
@@ -280,45 +316,57 @@ export async function initUsers() {
|
|||||||
time: timeSlot
|
time: timeSlot
|
||||||
});
|
});
|
||||||
|
|
||||||
if (modalViewLessons?.style.display !== 'none' && currentLessonsTeacherId == userId) {
|
if (modalViewLessons?.classList.contains('open') && currentLessonsTeacherId == userId) {
|
||||||
await loadTeacherLessons(currentLessonsTeacherId, currentLessonsTeacherName);
|
await loadTeacherLessons(currentLessonsTeacherId, currentLessonsTeacherName);
|
||||||
}
|
}
|
||||||
|
|
||||||
showAlert('add-lesson-alert', 'Занятие добавлено ✓', 'success');
|
showAlert('add-lesson-alert', 'Занятие добавлено', 'success');
|
||||||
|
|
||||||
lessonGroupSelect.selectedIndex = 0;
|
lessonGroupSelect.value = '';
|
||||||
lessonDisciplineSelect.selectedIndex = 0;
|
lessonDisciplineSelect.value = '';
|
||||||
lessonClassroomSelect.selectedIndex = 0;
|
lessonClassroomSelect.value = '';
|
||||||
lessonTypeSelect.selectedIndex = 0;
|
lessonTypeSelect.value = '';
|
||||||
lessonDaySelect.selectedIndex = 0;
|
lessonDaySelect.value = '';
|
||||||
lessonTimeSelect.innerHTML = '<option value="">Сначала выберите день</option>';
|
lessonTimeSelect.value = '';
|
||||||
lessonTimeSelect.disabled = true;
|
lessonTimeSelect.disabled = true;
|
||||||
|
|
||||||
weekUpper.checked = false;
|
weekUpper.checked = false;
|
||||||
weekLower.checked = false;
|
weekLower.checked = false;
|
||||||
document.querySelector('input[name="lessonFormat"][value="Очно"]').checked = true;
|
document.querySelector('input[name="lessonFormat"][value="Очно"]').checked = true;
|
||||||
|
|
||||||
|
requestAnimationFrame(() => syncAddLessonHeight());
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
hideAlert('add-lesson-alert');
|
hideAlert('add-lesson-alert');
|
||||||
|
syncAddLessonHeight();
|
||||||
}, 3000);
|
}, 3000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlert('add-lesson-alert', err.message || 'Ошибка добавления занятия', 'error');
|
showAlert('add-lesson-alert', err.message || 'Ошибка добавления занятия', 'error');
|
||||||
|
requestAnimationFrame(() => syncAddLessonHeight());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
lessonDaySelect.addEventListener('change', function () {
|
lessonDaySelect.addEventListener('change', function () {
|
||||||
updateTimeOptions(this.value);
|
updateTimeOptions(this.value);
|
||||||
|
requestAnimationFrame(() => syncAddLessonHeight());
|
||||||
});
|
});
|
||||||
|
|
||||||
if (modalAddLessonClose) {
|
if (modalAddLessonClose) {
|
||||||
modalAddLessonClose.addEventListener('click', () => closeOverlay());
|
modalAddLessonClose.addEventListener('click', () => {
|
||||||
|
modalAddLesson.classList.remove('open');
|
||||||
|
resetLessonForm();
|
||||||
|
syncAddLessonHeight();
|
||||||
|
updateBackdrop();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Клик по оверлею (мимо модалок) закрывает всё
|
if (modalAddLesson) {
|
||||||
if (usersOverlay) {
|
modalAddLesson.addEventListener('click', (e) => {
|
||||||
usersOverlay.querySelector('.cs-overlay-scroll')?.addEventListener('click', (e) => {
|
if (e.target === modalAddLesson) {
|
||||||
if (e.target.classList.contains('cs-overlay-scroll')) {
|
modalAddLesson.classList.remove('open');
|
||||||
closeOverlay();
|
resetLessonForm();
|
||||||
|
syncAddLessonHeight();
|
||||||
|
updateBackdrop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -433,20 +481,48 @@ export async function initUsers() {
|
|||||||
currentLessonsTeacherId = teacherId;
|
currentLessonsTeacherId = teacherId;
|
||||||
currentLessonsTeacherName = teacherName || '';
|
currentLessonsTeacherName = teacherName || '';
|
||||||
|
|
||||||
if (modalViewLessons) modalViewLessons.style.display = '';
|
|
||||||
loadTeacherLessons(teacherId, teacherName);
|
loadTeacherLessons(teacherId, teacherName);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => syncAddLessonHeight());
|
||||||
|
|
||||||
|
modalViewLessons.classList.add('open');
|
||||||
|
updateBackdrop();
|
||||||
|
// document.body.style.overflow = 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeViewLessonsModal() {
|
function closeViewLessonsModal() {
|
||||||
if (modalViewLessons) modalViewLessons.style.display = 'none';
|
modalViewLessons.classList.remove('open');
|
||||||
|
updateBackdrop();
|
||||||
|
// document.body.style.overflow = '';
|
||||||
|
|
||||||
currentLessonsTeacherId = null;
|
currentLessonsTeacherId = null;
|
||||||
currentLessonsTeacherName = '';
|
currentLessonsTeacherName = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (modalViewLessonsClose) {
|
||||||
|
modalViewLessonsClose.addEventListener('click', closeViewLessonsModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modalViewLessons) {
|
||||||
|
modalViewLessons.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modalViewLessons) closeViewLessonsModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key !== 'Escape') return;
|
if (e.key !== 'Escape') return;
|
||||||
if (usersOverlay?.classList.contains('open')) {
|
|
||||||
closeOverlay();
|
if (modalAddLesson?.classList.contains('open')) {
|
||||||
|
modalAddLesson.classList.remove('open');
|
||||||
|
resetLessonForm();
|
||||||
|
syncAddLessonHeight();
|
||||||
|
updateBackdrop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modalViewLessons?.classList.contains('open')) {
|
||||||
|
closeViewLessonsModal();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
<div class="card">
|
|
||||||
<div class="card-header-row" style="margin-bottom: 1.5rem;">
|
|
||||||
<h2>Загруженность аудиторий</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="filter-row" style="margin-bottom: 2rem; align-items: flex-end; gap: 1.5rem;">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Корпус</label>
|
|
||||||
<div class="custom-multi-select">
|
|
||||||
<div class="select-box" id="building-box">
|
|
||||||
<span class="select-text" id="building-text">Выберите корпуса...</span>
|
|
||||||
<svg class="dropdown-icon" width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown-menu" id="building-menu">
|
|
||||||
<div id="building-checkboxes" class="checkbox-group-vertical"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Вместимость</label>
|
|
||||||
<div class="custom-multi-select">
|
|
||||||
<div class="select-box" id="capacity-box">
|
|
||||||
<span class="select-text" id="capacity-text">Выберите вместимость...</span>
|
|
||||||
<svg class="dropdown-icon" width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown-menu" id="capacity-menu">
|
|
||||||
<div id="capacity-checkboxes" class="checkbox-group-vertical"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Оборудование</label>
|
|
||||||
<div class="custom-multi-select">
|
|
||||||
<div class="select-box" id="equipment-box">
|
|
||||||
<span class="select-text" id="equipment-text">Выберите оборудование...</span>
|
|
||||||
<svg class="dropdown-icon" width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="dropdown-menu" id="equipment-menu">
|
|
||||||
<div id="equipment-checkboxes" class="checkbox-group-vertical"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group" style="max-width: 200px;">
|
|
||||||
<label>Дата</label>
|
|
||||||
<input type="date" id="workload-date">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Table Container -->
|
|
||||||
<div class="workload-grid-container">
|
|
||||||
<table class="workload-table" id="workload-table">
|
|
||||||
<thead>
|
|
||||||
<tr id="workload-header-row">
|
|
||||||
<th class="top-left-cell">
|
|
||||||
<span class="top-label">Аудитория</span>
|
|
||||||
<span class="bottom-label">Время</span>
|
|
||||||
</th>
|
|
||||||
<!-- Rendered by JS -->
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="workload-tbody">
|
|
||||||
<!-- Rendered by JS -->
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -30,11 +30,11 @@
|
|||||||
<label for="filter-period">Учебный год</label>
|
<label for="filter-period">Учебный год</label>
|
||||||
<select id="filter-period" required>
|
<select id="filter-period" required>
|
||||||
<option value="">Выберите...</option>
|
<option value="">Выберите...</option>
|
||||||
<option value="2026-2027">2026/2027</option>
|
|
||||||
<option value="2025-2026">2025/2026</option>
|
|
||||||
<option value="2024-2025">2024/2025</option>
|
|
||||||
<option value="2023-2024">2023/2024</option>
|
|
||||||
<option value="2022-2023">2022/2023</option>
|
<option value="2022-2023">2022/2023</option>
|
||||||
|
<option value="2023-2024">2023/2024</option>
|
||||||
|
<option value="2024-2025">2024/2025</option>
|
||||||
|
<option value="2025-2026">2025/2026</option>
|
||||||
|
<option value="2026-2027">2026/2027</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -63,9 +63,9 @@
|
|||||||
<label for="cs-period">Учебный год</label>
|
<label for="cs-period">Учебный год</label>
|
||||||
<select id="cs-period" required>
|
<select id="cs-period" required>
|
||||||
<option value="">Выберите...</option>
|
<option value="">Выберите...</option>
|
||||||
<option value="2026-2027">2026/2027</option>
|
|
||||||
<option value="2025-2026">2025/2026</option>
|
|
||||||
<option value="2024-2025">2024/2025</option>
|
<option value="2024-2025">2024/2025</option>
|
||||||
|
<option value="2025-2026">2025/2026</option>
|
||||||
|
<option value="2026-2027">2026/2027</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -83,6 +83,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="flex: 1 1 150px;">
|
||||||
|
<label for="cs-semester">Курс/Семестр (номер)</label>
|
||||||
|
<input type="number" id="cs-semester" required min="1" max="12" placeholder="Например: 1">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group" style="flex: 1 1 180px;">
|
<div class="form-group" style="flex: 1 1 180px;">
|
||||||
<label for="cs-group">Группа</label>
|
<label for="cs-group">Группа</label>
|
||||||
<select id="cs-group" required>
|
<select id="cs-group" required>
|
||||||
@@ -154,6 +159,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Уч. год</th>
|
<th>Уч. год</th>
|
||||||
<th>Семестр</th>
|
<th>Семестр</th>
|
||||||
|
<th>№</th>
|
||||||
<th>Группа</th>
|
<th>Группа</th>
|
||||||
<th>Дисциплина</th>
|
<th>Дисциплина</th>
|
||||||
<th>Вид</th>
|
<th>Вид</th>
|
||||||
@@ -165,7 +171,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="prepared-schedules-tbody">
|
<tbody id="prepared-schedules-tbody">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="9" class="loading-row">Нет записей</td>
|
<td colspan="10" class="loading-row">Нет записей</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -22,12 +22,8 @@
|
|||||||
<input type="number" id="new-group-department" placeholder="ID" required>
|
<input type="number" id="new-group-department" placeholder="ID" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="new-group-yearStartStudy">Год начала обучения</label>
|
<label for="new-group-enrollment-year">Год начала обучения</label>
|
||||||
<input type="number" id="new-group-yearStartStudy" required pattern="^20\d{2}$" maxlength="3" placeholder="2026">
|
<input type="number" id="new-group-enrollment-year" placeholder="2023" min="2000" max="2100" required>
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="new-group-speciality-code">Код специальности</label>
|
|
||||||
<input type="text" id="new-group-speciality-code" placeholder="09.03.01" required>
|
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-primary">Создать</button>
|
<button type="submit" class="btn-primary">Создать</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,14 +50,15 @@
|
|||||||
<th>Численность (чел.)</th>
|
<th>Численность (чел.)</th>
|
||||||
<th>Форма обучения</th>
|
<th>Форма обучения</th>
|
||||||
<th>ID кафедры</th>
|
<th>ID кафедры</th>
|
||||||
|
<th>Год начала</th>
|
||||||
<th>Курс</th>
|
<th>Курс</th>
|
||||||
<th>Код специальности</th>
|
<th>Семестр</th>
|
||||||
<th>Действия</th>
|
<th>Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="groups-tbody">
|
<tbody id="groups-tbody">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="8" class="loading-row">Загрузка...</td>
|
<td colspan="9" class="loading-row">Загрузка...</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header-row">
|
|
||||||
<h2>Расписание занятий</h2>
|
<h2>Расписание занятий</h2>
|
||||||
<button class="btn-primary" id="sch-btn-add-lesson">Добавить занятие</button>
|
|
||||||
</div>
|
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
<table id="schedule-table">
|
<table id="schedule-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -38,142 +35,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody id="schedule-tbody">
|
<tbody id="schedule-tbody">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="11" class="loading-row">Загрузка...</td>
|
<td colspan="8" class="loading-row">Загрузка...</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== Оверлей для модалок добавления занятия ===== -->
|
|
||||||
<div class="cs-overlay" id="sch-overlay">
|
|
||||||
<div class="cs-overlay-scroll">
|
|
||||||
|
|
||||||
<!-- Модалка 1: Форма добавления -->
|
|
||||||
<div class="cs-modal cs-modal-form card" id="sch-modal-form">
|
|
||||||
<div class="cs-modal-header">
|
|
||||||
<h2>Добавить занятие</h2>
|
|
||||||
<button class="btn-close-panel" id="sch-modal-close">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form id="sch-add-lesson-form">
|
|
||||||
<div class="form-row" style="align-items: flex-end; gap: 1rem; flex-wrap: wrap; width: 100%; justify-content: space-between;">
|
|
||||||
|
|
||||||
<!-- Преподаватель -->
|
|
||||||
<div class="form-group" style="flex: 0 1 auto; max-width: 220px">
|
|
||||||
<label for="sch-teacher">Преподаватель</label>
|
|
||||||
<select id="sch-teacher" required>
|
|
||||||
<option value="">Выберите преподавателя</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Группа -->
|
|
||||||
<div class="form-group" style="flex: 0 1 auto; max-width: 190px">
|
|
||||||
<label for="sch-group">Группа</label>
|
|
||||||
<select id="sch-group" required>
|
|
||||||
<option value="">Выберите группу</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Дисциплина -->
|
|
||||||
<div class="form-group" style="flex: 0 1 auto; max-width: 220px">
|
|
||||||
<label for="sch-discipline">Дисциплина</label>
|
|
||||||
<select id="sch-discipline" required>
|
|
||||||
<option value="">Выберите дисциплину</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Аудитория -->
|
|
||||||
<div class="form-group" style="flex: 0 1 auto; max-width: 215px">
|
|
||||||
<label for="sch-classroom">Аудитория</label>
|
|
||||||
<select id="sch-classroom" required>
|
|
||||||
<option value="">Выберите аудиторию</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- День недели -->
|
|
||||||
<div class="form-group" style="flex: 0 1 auto; max-width: 170px">
|
|
||||||
<label for="sch-day">День недели</label>
|
|
||||||
<select id="sch-day" required>
|
|
||||||
<option value="">Выберите день</option>
|
|
||||||
<option value="Понедельник">Понедельник</option>
|
|
||||||
<option value="Вторник">Вторник</option>
|
|
||||||
<option value="Среда">Среда</option>
|
|
||||||
<option value="Четверг">Четверг</option>
|
|
||||||
<option value="Пятница">Пятница</option>
|
|
||||||
<option value="Суббота">Суббота</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Неделя -->
|
|
||||||
<div class="form-group" style="flex: 0 1 auto; max-width: 192px">
|
|
||||||
<label>Неделя</label>
|
|
||||||
<div style="display: flex; gap: 0.2rem;">
|
|
||||||
<label class="btn-checkbox">
|
|
||||||
<input type="checkbox" name="schWeekType" value="Верхняя" id="sch-week-upper">
|
|
||||||
<span class="checkbox-btn">Верхняя</span>
|
|
||||||
</label>
|
|
||||||
<label class="btn-checkbox">
|
|
||||||
<input type="checkbox" name="schWeekType" value="Нижняя" id="sch-week-lower">
|
|
||||||
<span class="checkbox-btn">Нижняя</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Тип занятия -->
|
|
||||||
<div class="form-group" style="flex: 0 1 auto; max-width: 160px">
|
|
||||||
<label for="sch-type">Тип занятия</label>
|
|
||||||
<select id="sch-type" required>
|
|
||||||
<option value="">Выберите тип</option>
|
|
||||||
<option value="Практическая работа">Практическая</option>
|
|
||||||
<option value="Лекция">Лекция</option>
|
|
||||||
<option value="Лабораторная работа">Лабораторная</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Формат занятия -->
|
|
||||||
<div class="form-group" style="flex: 0 1 auto; max-width: 170px">
|
|
||||||
<label>Формат занятия</label>
|
|
||||||
<div style="display: flex; gap: 0.2rem;">
|
|
||||||
<label class="btn-checkbox">
|
|
||||||
<input type="radio" name="schLessonFormat" value="Очно" id="sch-format-offline" checked>
|
|
||||||
<span class="checkbox-btn">Очно</span>
|
|
||||||
</label>
|
|
||||||
<label class="btn-checkbox">
|
|
||||||
<input type="radio" name="schLessonFormat" value="Онлайн" id="sch-format-online">
|
|
||||||
<span class="checkbox-btn">Онлайн</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Время занятия -->
|
|
||||||
<div class="form-group" style="flex: 0 0 auto; max-width: 235px">
|
|
||||||
<label for="sch-time">Время занятия</label>
|
|
||||||
<select id="sch-time" required disabled>
|
|
||||||
<option value="">Сначала выберите день</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Кнопка Сохранить -->
|
|
||||||
<div class="form-group" style="flex: 0 0 auto;">
|
|
||||||
<button type="submit" class="btn-primary" style="white-space: nowrap;">Сохранить</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-alert" id="sch-add-alert" role="alert" style="margin-top: 1rem;"></div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Модалка 2: Занятия выбранного преподавателя -->
|
|
||||||
<div class="cs-modal cs-modal-table card" id="sch-modal-lessons" style="display:none;">
|
|
||||||
<div class="cs-modal-header">
|
|
||||||
<h2 id="sch-teacher-name">Занятия преподавателя</h2>
|
|
||||||
</div>
|
|
||||||
<div class="lessons-container" id="sch-lessons-container">
|
|
||||||
<div class="no-lessons">Выберите преподавателя для просмотра занятий</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -61,16 +61,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== Оверлей для модалок добавления/просмотра занятий ===== -->
|
<!-- Add Lesson Modal -->
|
||||||
<div class="cs-overlay" id="users-overlay">
|
<div class="modal-overlay" id="modal-add-lesson">
|
||||||
<div class="cs-overlay-scroll">
|
<div class="modal-content card">
|
||||||
|
|
||||||
<!-- Модалка 1: Форма добавления -->
|
|
||||||
<div class="cs-modal cs-modal-form card" id="modal-add-lesson">
|
|
||||||
<div class="cs-modal-header">
|
|
||||||
<h2>Добавить занятие</h2>
|
<h2>Добавить занятие</h2>
|
||||||
<button class="btn-close-panel" id="modal-add-lesson-close">×</button>
|
<button class="modal-close" id="modal-add-lesson-close">×</button>
|
||||||
</div>
|
|
||||||
<form id="add-lesson-form">
|
<form id="add-lesson-form">
|
||||||
<input type="hidden" id="lesson-user-id">
|
<input type="hidden" id="lesson-user-id">
|
||||||
|
|
||||||
@@ -115,7 +110,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Тип недели -->
|
<!-- Тип недели (ВЕРТИКАЛЬНО) -->
|
||||||
<div class="form-group" style="flex: 0 1 auto; max-width: 192px">
|
<div class="form-group" style="flex: 0 1 auto; max-width: 192px">
|
||||||
<label>Неделя</label>
|
<label>Неделя</label>
|
||||||
<div style="display: flex; gap: 0.2rem;">
|
<div style="display: flex; gap: 0.2rem;">
|
||||||
@@ -141,7 +136,7 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Формат занятия -->
|
<!-- Формат занятия (ВЕРТИКАЛЬНО) -->
|
||||||
<div class="form-group" style="flex: 0 1 auto; max-width: 170px">
|
<div class="form-group" style="flex: 0 1 auto; max-width: 170px">
|
||||||
<label>Формат занятия</label>
|
<label>Формат занятия</label>
|
||||||
<div style="display: flex; gap: 0.2rem;">
|
<div style="display: flex; gap: 0.2rem;">
|
||||||
@@ -164,26 +159,29 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Кнопка Сохранить -->
|
<!-- Кнопка Сохранить (в том же ряду) -->
|
||||||
<div class="form-group" style="flex: 0 0 auto;">
|
<div class="form-group" style="flex: 0 0 auto;">
|
||||||
<button type="submit" class="btn-primary" style="white-space: nowrap;">Сохранить</button>
|
<button type="submit" class="btn-primary" style="white-space: nowrap;">Сохранить</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div> <!-- Закрытие form-row -->
|
||||||
|
|
||||||
<div class="form-alert" id="add-lesson-alert" role="alert" style="margin-top: 1rem;"></div>
|
<div class="form-alert" id="add-lesson-alert" role="alert" style="margin-top: 1rem;"></div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- View Teacher Lessons Modal -->
|
||||||
<!-- Модалка 2: Просмотр занятий преподавателя -->
|
<div class="modal-overlay" id="modal-view-lessons">
|
||||||
<div class="cs-modal cs-modal-table card" id="modal-view-lessons" style="display:none;">
|
<div class="modal-content view-lessons-modal">
|
||||||
<div class="cs-modal-header">
|
<div class="modal-header">
|
||||||
<h2 id="modal-teacher-name">Занятия преподавателя</h2>
|
<h2 id="modal-teacher-name">Занятия преподавателя</h2>
|
||||||
|
<button class="modal-close" id="modal-view-lessons-close">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="lessons-container" id="lessons-container">
|
<div class="lessons-container" id="lessons-container">
|
||||||
|
<!-- Фильтры по дням (добавим позже) -->
|
||||||
<div class="loading-lessons">Загрузка занятий...</div>
|
<div class="loading-lessons">Загрузка занятий...</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="modal-backdrop"></div>
|
||||||
120
tz2.md
Normal file
120
tz2.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# План выполнения работ по новым интерфейсам расписания
|
||||||
|
|
||||||
|
На основе предоставленного технического задания составлен следующий детализированный план разработки макетов и функционала новой подсистемы составления и просмотра расписания.
|
||||||
|
|
||||||
|
## 1. Вкладка "Загрузка аудиторий"
|
||||||
|
|
||||||
|
**Концепция:** Динамическая таблица, визуализирующая текущее использование аудиторного фонда в конкретную учебную неделю.
|
||||||
|
|
||||||
|
### Интерфейс
|
||||||
|
* **Сетка данных:**
|
||||||
|
* **Столбцы:** Аудитории.
|
||||||
|
* **Строки:** Время пар (расписание звонков).
|
||||||
|
* **Ячейки:** Информация о проходящем занятии (Группа, Преподаватель, Дисциплина).
|
||||||
|
* **Элементы управления:**
|
||||||
|
* **Календарь недель:** Выпадающий список или слайдер для переключения между учебными неделями семестра. Учитывает изменения в графике (сессии, приезд заочников и т.д.).
|
||||||
|
* **Фильтр аудиторий:** Чекбоксы, мультиселект или группировка по корпусам/типам, позволяющие скрывать неотображаемые аудитории для удобства просмотра.
|
||||||
|
|
||||||
|
### Функционал
|
||||||
|
* Отображение данных на основе сохраненного расписания из БД с привязкой к выбранной неделе.
|
||||||
|
* **Интерактивность:** Возможность клика по пустой ячейке для добавления нового занятия. Открывается модальное окно с предзаполненными полями `Аудитория`, `Время` и `Неделя`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Вкладка "Загруженность преподавателей"
|
||||||
|
|
||||||
|
**Концепция:** Интерфейс, дублирующий логику загрузки аудиторий, но с фокусом на профессорско-преподавательский состав (ППС).
|
||||||
|
|
||||||
|
### Интерфейс
|
||||||
|
* **Сетка данных:**
|
||||||
|
* **Столбцы:** Список преподавателей.
|
||||||
|
* **Строки:** Время пар.
|
||||||
|
* **Ячейки:** Информация о занятии (Группа, Аудитория, Дисциплина).
|
||||||
|
* **Элементы управления:**
|
||||||
|
* Календарь недель (аналогично аудиториям).
|
||||||
|
* Поиск/фильтрация по ФИО преподавателя или кафедре.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Рабочее окно составителя расписания
|
||||||
|
|
||||||
|
**Концепция:** Основной инструмент диспетчера. Интерактивная среда для распределения нагрузки.
|
||||||
|
|
||||||
|
### Интерфейс
|
||||||
|
* **Сетка расписания:**
|
||||||
|
* **Столбцы:** Учебные группы.
|
||||||
|
* **Строки:** Время пар.
|
||||||
|
* **Панель нагрузки:** Боковая панель или вызываемое окно со списком нераспределенных предметов для выбранной группы/курса.
|
||||||
|
|
||||||
|
### Алгоритм работы (User Flow)
|
||||||
|
1. **Старт:** Диспетчер кликает в конкретную ячейку сетки (выбирает группу и время проведения пары).
|
||||||
|
2. **Выбор предмета:** Появляется меню со списком предметов, которые необходимо поставить данной группе. Диспетчер выбирает нужный.
|
||||||
|
3. **Выбор преподавателя и проверка его занятости:**
|
||||||
|
* Система автоматически подтягивает преподавателя (или список возможных), закрепленного за этой дисциплиной.
|
||||||
|
* Отображается **карта свободных слотов преподавателя**, чтобы убедиться, что он не ведет пару в это же время у другой группы (предотвращение накладок).
|
||||||
|
4. **Выбор аудитории (Умный подбор):**
|
||||||
|
* Если преподаватель свободен, всплывает **карта загрузки аудиторий**.
|
||||||
|
* Аудитории отображаются с цветовой индикацией:
|
||||||
|
* 🟢 **Зеленый:** Аудитория свободна и её характеристики (тип, вместимость) полностью подходят для занятия.
|
||||||
|
* 🟡 **Желтый:** Аудитория свободна, но не подходит по требованиям (например, это лекционный зал для маленькой группы, или обычная аудитория для компьютерного практикума).
|
||||||
|
* 🔴 **Красный:** Аудитория занята (при наведении или клике показывается, кто именно там занимается).
|
||||||
|
* Диспетчер выбирает подходящую аудиторию.
|
||||||
|
5. **Финал:** Занятие фиксируется в сетке, предмет вычитается из пула нераспределенной нагрузки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этапы реализации и анализ архитектуры БД
|
||||||
|
|
||||||
|
На основе анализа существующей базы данных проекта (см. `docs/DATABASE.md`) выявлено, что значительная часть необходимых данных уже присутствует, однако для полного удовлетворения ТЗ требуются точечные доработки структуры БД.
|
||||||
|
|
||||||
|
### Анализ требований ТЗ и текущей БД
|
||||||
|
|
||||||
|
1. **"Аудитории: нет пункта о том, в каком корпусе она находится"**:
|
||||||
|
* **Текущее состояние в БД**: В таблице `classrooms` **уже существуют** поля `building` (Корпус) и `floor` (Этаж).
|
||||||
|
* **Вывод**: Добавление характеристик корпуса в БД не требуется. Информацию нужно просто вывести через Backend API на Frontend.
|
||||||
|
2. **Динамическое расписание и календарь недель ("закончился семестр у магистров", "заочники")**:
|
||||||
|
* **Текущее состояние в БД**: Таблица `lessons` содержит поле `week` (с текстовыми значениями `Верхняя / Нижняя / Обе`), что подразумевает статический цикличный график (раз в 2 недели).
|
||||||
|
* **Чего не хватает**: Текущая схема не позволяет гибко привязывать занятия к конкретным календарным датам или конкретным учебным неделям семестра (например, с 1 по 18 неделю).
|
||||||
|
* **Вывод**: Потребуется миграция БД для внедрения календаря (например, таблица `academic_weeks` или изменение структуры `lessons`).
|
||||||
|
3. **Умный подбор аудиторий (желтая индикация — "не подходит оборудование")**:
|
||||||
|
* **Текущее состояние в БД**: Есть таблица `classroom_equipments`, описывающая инвентарь аудитории.
|
||||||
|
* **Чего не хватает**: В системе отсутствует информация о том, какое оборудование **требуется** для конкретной дисциплины.
|
||||||
|
* **Вывод**: Необходимо добавить новую связующую таблицу (например, `subject_equipments` или `lesson_type_equipments`), чтобы алгоритм мог сопоставлять требования предмета с оснащением выбранной аудитории.
|
||||||
|
4. **Списки нагрузки для распределения**:
|
||||||
|
* **Текущее состояние в БД**: Присутствует таблица `schedule_data` со столбцом `number_of_hours` (часы, подлежащие распределению).
|
||||||
|
* **Вывод**: Архитектура готова. Потребуется лишь бизнес-логика для связывания созданных записей `lessons` с нераспределенной нагрузкой `schedule_data` (для вычета распределенных часов).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Детализированный план реализации
|
||||||
|
|
||||||
|
#### Этап 1: Доработка базы данных (Flyway миграции)
|
||||||
|
* **Миграция БД (Календарь):** Проектирование и создание механизма привязки расписания к конкретным неделям/датам, отход от жесткой привязки "Верхняя/Нижняя".
|
||||||
|
* **Миграция БД (Оборудование):** Создание таблицы для хранения технических требований дисциплин к аудиториям (`subject_equipments`), чтобы стала возможна "желтая" индикация.
|
||||||
|
* *(Напоминание: все миграции создаются как новые файлы `V2__...sql`, `V3__...sql` в директории `db/migration/`, изменение `V1__init.sql` запрещено).*
|
||||||
|
|
||||||
|
#### Этап 2: Разработка Backend API (Java Spring Boot)
|
||||||
|
* **Эндпоинты получения видов (View API):**
|
||||||
|
* API для сетки аудиторий: агрегация занятий по аудиториям с учетом выбранной недели.
|
||||||
|
* API для сетки преподавателей: агрегация занятий по преподавателям.
|
||||||
|
* API нераспределенной нагрузки: получение остатка часов из `schedule_data` для выбранной группы.
|
||||||
|
* **Интеллектуальные алгоритмы проверок (Service Layer):**
|
||||||
|
* Логика проверки накладок преподавателей.
|
||||||
|
* Алгоритм "Цветофор" для аудитории:
|
||||||
|
* Красный (занятость по времени).
|
||||||
|
* Желтый (сопоставление вместимости `capacity` с `group_size` + проверка наличия нужного оборудования).
|
||||||
|
* Зеленый (все проверки пройдены).
|
||||||
|
|
||||||
|
#### Этап 3: UI-разработка (Frontend)
|
||||||
|
* Верстка трех основных табличных сеток (Audience Load, Teacher Workload, Schedule Maker).
|
||||||
|
* Реализация календаря/селектора недель (влияющего на выводимые данные).
|
||||||
|
* Программирование интерактивного Flow диспетчера в Vanilla JS:
|
||||||
|
1. Клик в ячейку.
|
||||||
|
2. Вызов списка нагрузки -> выбор предмета.
|
||||||
|
3. Отображение свободных слотов преподавателя.
|
||||||
|
4. Вывод карты аудиторий с динамической цветовой индикацией.
|
||||||
|
5. Сохранение результата.
|
||||||
|
|
||||||
|
#### Этап 4: Интеграция и стабилизация
|
||||||
|
* Интеграция Front и Back-частей.
|
||||||
|
* Сквозное тестирование сценариев создания, редактирования и удаления занятий с пересчетом часов нагрузки.
|
||||||
Reference in New Issue
Block a user