Merge pull request 'Всё в прод' (#11) from dynamic_schedule into main
Reviewed-on: #11
This commit was merged in pull request #11.
This commit is contained in:
21
.codex/config.toml
Normal file
21
.codex/config.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
#:schema https://developers.openai.com/codex/config-schema.json
|
||||
|
||||
web_search = "live"
|
||||
|
||||
developer_instructions = """
|
||||
Для проекта /mnt/HDD/magistr/magistr используй корневой AGENTS.md как основной проектный регламент.
|
||||
Соблюдай русский язык для ответов, комментариев, UI, ошибок и логов.
|
||||
При конфликте проектных docs/skills с AGENTS.md считай AGENTS.md основным проектным источником, кроме инструкций более высокого уровня.
|
||||
Используй проектные скиллы из .agents/skills, когда задача соответствует их description.
|
||||
"""
|
||||
|
||||
[skills]
|
||||
include_instructions = true
|
||||
|
||||
[[skills.config]]
|
||||
path = "../.agents/skills/auto-update-docs"
|
||||
enabled = true
|
||||
|
||||
[[skills.config]]
|
||||
path = "../.agents/skills/frontend-design"
|
||||
enabled = true
|
||||
@@ -27,6 +27,8 @@ magistr/
|
||||
├── frontend/ # Статические файлы
|
||||
│ ├── admin/ # Интерфейс администратора
|
||||
│ │ └── settings/ # Страница настроек (отдельный SPA)
|
||||
│ ├── department/ # Redirect в кабинет кафедры внутри admin SPA
|
||||
│ ├── edu-office/ # Redirect в кабинет учебного отдела внутри admin SPA
|
||||
│ ├── teacher/ # Интерфейс преподавателя
|
||||
│ └── student/ # Интерфейс студента
|
||||
├── 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: Формат (Очно/Онлайн)
|
||||
- [ ] Визуальное предупреждение при конфликтах (аудитория/преподаватель уже заняты)
|
||||
- [ ] Удаление правила с подтверждением
|
||||
248
SUPERVISOR_TASKS_IMPLEMENTATION_PLAN.md
Normal file
248
SUPERVISOR_TASKS_IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# Отчёт об изменениях по задачам научного руководителя
|
||||
|
||||
Дата: 2026-05-19
|
||||
|
||||
## Краткий итог
|
||||
|
||||
Выполнен первый цельный релиз архитектурной основы:
|
||||
|
||||
- добавлены роли `EDUCATION_OFFICE`, `DEPARTMENT` и `SCHEDULE_VIEWER`;
|
||||
- добавлена backend-проверка bearer-токена и ролей через `AuthorizationInterceptor` и `@RequireRoles`;
|
||||
- добавлен жизненный цикл справочников: `ACTIVE` / `ARCHIVED`;
|
||||
- физическое удаление ключевых сущностей заменено архивированием там, где это влияет на историю;
|
||||
- добавлена история переводов преподавателей между кафедрами;
|
||||
- добавлены точечные изменения расписания: перенос, отмена, замена;
|
||||
- добавлен расширенный read-only поиск расписания по аудитории, преподавателю, кафедре, группе, дисциплине, типу занятия, паре и чётности;
|
||||
- добавлены отчёты загруженности по преподавателям, аудиториям, кафедрам, парам и свободным аудиториям;
|
||||
- просмотр расписаний переведён с плоского списка на совмещённые матричные таблицы чётной/нечётной недели с разрезом по группам, преподавателям или аудиториям;
|
||||
- вкладка загруженности стала общей для аудиторий, преподавателей и кафедр;
|
||||
- интерфейсы кафедры и учебного отдела переведены в общий стиль админ-панели через role-based вкладки;
|
||||
- отдельная миграция `V2` удалена, изменения схемы внесены в `V1__init.sql`;
|
||||
- обновлена документация проекта.
|
||||
|
||||
## Backend
|
||||
|
||||
### Роли и авторизация
|
||||
|
||||
Поддерживаются роли:
|
||||
|
||||
- `ADMIN`;
|
||||
- `EDUCATION_OFFICE`;
|
||||
- `DEPARTMENT`;
|
||||
- `SCHEDULE_VIEWER`;
|
||||
- `TEACHER`;
|
||||
- `STUDENT`.
|
||||
|
||||
Добавлены:
|
||||
|
||||
- `AuthSessionService` — in-memory UUID-сессии;
|
||||
- `AuthContext` — текущий пользователь в `ThreadLocal`;
|
||||
- `AuthorizationInterceptor` — проверка `Authorization: Bearer ...`;
|
||||
- `@RequireRoles` — ограничение доступа на уровне контроллеров и методов;
|
||||
- `GET /api/auth/me` — текущий пользователь по токену.
|
||||
|
||||
Redirect после входа:
|
||||
|
||||
- `ADMIN` -> `/admin/`;
|
||||
- `EDUCATION_OFFICE` -> `/admin/#schedule-view`;
|
||||
- `DEPARTMENT` -> `/admin/#department-workspace`;
|
||||
- `SCHEDULE_VIEWER` -> `/admin/#schedule-view`;
|
||||
- `TEACHER` -> `/teacher/`;
|
||||
- `STUDENT` -> `/student/`.
|
||||
|
||||
### Миграция БД
|
||||
|
||||
Отдельная миграция `V2__roles_lifecycle_temporal_schedule.sql` удалена по текущему решению проекта.
|
||||
|
||||
Все изменения схемы внесены в:
|
||||
|
||||
```text
|
||||
backend/src/main/resources/db/migration/V1__init.sql
|
||||
```
|
||||
|
||||
`V1__init.sql` теперь сразу создаёт:
|
||||
|
||||
- lifecycle-поля к справочникам и пользователям;
|
||||
- `teacher_department_assignments`;
|
||||
- `subject_comments`;
|
||||
- version-поля к `schedule_rules`;
|
||||
- lock-поля к `schedule_rule_slots`;
|
||||
- `schedule_overrides`;
|
||||
- стартовых пользователей `учебный_отдел`, `кафедра_иб` и `просмотр_расписаний`.
|
||||
|
||||
### Архивирование вместо удаления
|
||||
|
||||
Архивирование добавлено для пользователей, аудиторий, оборудования, кафедр, специальностей, групп, подгрупп, дисциплин и правил расписания.
|
||||
|
||||
Архивные аудитории, преподаватели, группы и дисциплины запрещены для новых назначений. Историческое расписание при этом не теряет ссылки.
|
||||
|
||||
### История кафедр преподавателя
|
||||
|
||||
Добавлены endpoint:
|
||||
|
||||
- `GET /api/users/{id}/department-history`;
|
||||
- `POST /api/users/{id}/department-transfer`;
|
||||
- `GET /api/users/teachers/by-department/{departmentId}?date=YYYY-MM-DD`.
|
||||
|
||||
При переводе преподавателя старая запись закрывается, новая открывается с `valid_from`, а `users.department_id` обновляется как текущая кафедра.
|
||||
|
||||
### Расписание и загруженность
|
||||
|
||||
Добавлены:
|
||||
|
||||
- `GET /api/schedule/search`;
|
||||
- `GET /api/workload/teachers`;
|
||||
- `GET /api/workload/classrooms`;
|
||||
- `GET /api/workload/departments`;
|
||||
- `GET /api/workload/time-slots`;
|
||||
- `GET /api/workload/free-classrooms`;
|
||||
- `GET/POST/PUT/DELETE /api/edu-office/schedule/overrides`.
|
||||
|
||||
`GET /api/schedule` теперь использует общий слой поиска, поэтому учитывает точечные изменения расписания.
|
||||
|
||||
Роль `SCHEDULE_VIEWER` имеет доступ только к read-only просмотру расписаний и справочникам-фильтрам.
|
||||
|
||||
### Кабинет кафедры API
|
||||
|
||||
Добавлены:
|
||||
|
||||
- `GET /api/department/subjects`;
|
||||
- `POST /api/department/subjects/import`;
|
||||
- `GET /api/department/subjects/{subjectId}/comments`;
|
||||
- `POST /api/department/subjects/{subjectId}/comments`;
|
||||
- `GET /api/department/teachers`;
|
||||
- `GET /api/department/schedule`.
|
||||
|
||||
Для роли `DEPARTMENT` кафедра берётся из текущего пользователя. Для администратора можно выбрать кафедру в UI.
|
||||
|
||||
## Frontend
|
||||
|
||||
### Единая панель по ролям
|
||||
|
||||
Отдельные интерфейсы `/department/` и `/edu-office/` заменены на redirect в общую админ-панель:
|
||||
|
||||
- `/department/` -> `/admin/#department-workspace`;
|
||||
- `/edu-office/` -> `/admin/#schedule-view`.
|
||||
|
||||
В `frontend/admin/js/main.js` добавлена фильтрация вкладок:
|
||||
|
||||
- `ADMIN` видит все вкладки;
|
||||
- `EDUCATION_OFFICE` видит просмотр расписаний, конструктор расписания, календарный график, загруженность, аудитории и оборудование;
|
||||
- `DEPARTMENT` видит кабинет кафедры и просмотр расписаний;
|
||||
- `SCHEDULE_VIEWER` видит только просмотр расписаний.
|
||||
|
||||
Backend не опирается только на скрытие вкладок. `AuthorizationInterceptor` проверяет bearer-токен для `/api/**`, а `@RequireRoles` ограничивает операции на контроллерах и методах. Для кафедры дополнительно закрыты обходные пути:
|
||||
|
||||
- общий `/api/subjects` оставлен для записи только администратору, кафедра загружает дисциплины через scoped `/api/department/subjects/import`;
|
||||
- `/api/teacher-subjects` для роли `DEPARTMENT` проверяет, что и преподаватель, и дисциплина относятся к кафедре текущего пользователя;
|
||||
- `SCHEDULE_VIEWER` имеет только read-only доступ к расписаниям и справочникам-фильтрам.
|
||||
|
||||
После браузерной проверки исправлено визуальное скрытие недоступных вкладок: `layout.css` теперь принудительно скрывает `.nav-item[hidden]` и скрытые пункты меню настроек, поэтому роли больше не видят чужие вкладки в sidebar.
|
||||
|
||||
### Просмотр расписаний
|
||||
|
||||
Добавлена вкладка:
|
||||
|
||||
```text
|
||||
frontend/admin/views/schedule-view.html
|
||||
frontend/admin/js/views/schedule-view.js
|
||||
```
|
||||
|
||||
Возможности:
|
||||
|
||||
- фильтры по одной дате в периоде, группе, преподавателю, аудитории, кафедре, дисциплине, типу занятия и чётности;
|
||||
- двухнедельный диапазон для таблиц автоматически строится от понедельника выбранной недели;
|
||||
- выбор разреза таблиц: автоматически, по группам, по преподавателям или по аудиториям;
|
||||
- совмещённые матричные таблицы найденных занятий: строки — пары, столбцы — дни недели;
|
||||
- нечётная неделя отображается в верхней половине ячейки, чётная — в нижней, одинаковые занятия в обе недели схлопываются в цельную ячейку;
|
||||
- карточки занятий внутри ячеек с дисциплиной, группами, подгруппами, преподавателем, аудиторией, типом занятия, чётностью и ID слота.
|
||||
|
||||
### Загруженность
|
||||
|
||||
Вкладка `auditorium-workload` оставлена техническим tab id, но в UI называется `Загруженность`.
|
||||
|
||||
Возможности:
|
||||
|
||||
- выбор типа загруженности: аудитории, преподаватели или кафедры;
|
||||
- сводная матрица по выбранной дате: строки — выбранные сущности, столбцы — пары;
|
||||
- кафедральная матрица группирует занятия по кафедре преподавателя;
|
||||
- для аудиторий сохранены фильтры корпуса, вместимости и оборудования;
|
||||
- выбор конкретной аудитории, преподавателя или кафедры заменяет обзор двухнедельной таблицей по дням недели и времени;
|
||||
- чётная и нечётная недели показываются в одной ячейке: если состояние одинаковое, ячейка цельная, если отличается — делится вертикально.
|
||||
|
||||
### Кабинет кафедры
|
||||
|
||||
Добавлена вкладка:
|
||||
|
||||
```text
|
||||
frontend/admin/views/department-workspace.html
|
||||
frontend/admin/js/views/department-workspace.js
|
||||
```
|
||||
|
||||
Возможности:
|
||||
|
||||
- просмотр дисциплин кафедры;
|
||||
- загрузка дисциплин из списка `код; название`;
|
||||
- просмотр и добавление комментариев к дисциплинам;
|
||||
- просмотр преподавателей кафедры;
|
||||
- просмотр нагрузки преподавателей кафедры за период.
|
||||
|
||||
### Админка
|
||||
|
||||
Обновлено:
|
||||
|
||||
- создание пользователей теперь поддерживает роли `DEPARTMENT`, `EDUCATION_OFFICE` и `SCHEDULE_VIEWER`;
|
||||
- удаление пользователей заменено на архивирование;
|
||||
- вкладка аудиторий показывает архивные записи и умеет восстанавливать аудиторию;
|
||||
- удаление аудитории заменено на вывод из эксплуатации;
|
||||
- настройки временных слотов доступны `ADMIN` и `EDUCATION_OFFICE`.
|
||||
|
||||
## Документация
|
||||
|
||||
Обновлены:
|
||||
|
||||
- `AGENTS.md`;
|
||||
- `docs/README.md`;
|
||||
- `docs/API.md`;
|
||||
- `docs/DATABASE.md`;
|
||||
- `docs/BUSINESS_LOGIC.md`;
|
||||
- `docs/FRONTEND.md`;
|
||||
- `docs/ARCHITECTURE.md`.
|
||||
|
||||
## Проверки
|
||||
|
||||
Статически проверены новые и изменённые JS-файлы:
|
||||
|
||||
```bash
|
||||
node --check frontend/admin/js/main.js
|
||||
node --check frontend/admin/js/views/schedule-view.js
|
||||
node --check frontend/admin/js/views/auditorium-workload.js
|
||||
node --check frontend/admin/js/views/department-workspace.js
|
||||
node --check frontend/admin/settings/js/main.js
|
||||
node --check frontend/admin/js/api.js
|
||||
node --check frontend/admin/js/views/users.js
|
||||
node --check frontend/admin/js/views/classrooms.js
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Backend-компиляция проверяется через Docker Maven:
|
||||
|
||||
```bash
|
||||
docker run --rm -v /mnt/HDD/magistr/magistr/backend:/app -w /app maven:3.9-eclipse-temurin-17 mvn -q -DskipTests compile
|
||||
```
|
||||
|
||||
Результат: компиляция прошла успешно.
|
||||
|
||||
Единая `V1__init.sql` проверена на пустой PostgreSQL 16 внутри Docker: SQL применился успешно.
|
||||
|
||||
Локальный `mvn` в окружении отсутствует, поэтому используется Docker.
|
||||
|
||||
## Что осталось следующим этапом
|
||||
|
||||
Остались задачи, которые требуют отдельного цикла проработки:
|
||||
|
||||
- UI для просмотра истории переводов преподавателя в админке;
|
||||
- UI для списка и редактирования уже созданных `schedule_overrides`;
|
||||
- более строгая проверка конфликтов перед сохранением переносов;
|
||||
- полноценный импорт XLSX/CSV для кафедры вместо текстовой загрузки;
|
||||
- браузерный smoke-test и проверка ролевых вкладок после запуска `localhost:80`.
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.magistr.app.config.auth;
|
||||
|
||||
public final class AuthContext {
|
||||
|
||||
private static final ThreadLocal<AuthenticatedUser> CURRENT_USER = new ThreadLocal<>();
|
||||
|
||||
private AuthContext() {
|
||||
}
|
||||
|
||||
public static void setCurrentUser(AuthenticatedUser user) {
|
||||
CURRENT_USER.set(user);
|
||||
}
|
||||
|
||||
public static AuthenticatedUser getCurrentUser() {
|
||||
return CURRENT_USER.get();
|
||||
}
|
||||
|
||||
public static Long currentUserId() {
|
||||
AuthenticatedUser user = getCurrentUser();
|
||||
return user == null ? null : user.id();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
CURRENT_USER.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.magistr.app.config.auth;
|
||||
|
||||
import com.magistr.app.model.User;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class AuthSessionService {
|
||||
|
||||
private final Map<String, AuthenticatedUser> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
public String createSession(User user) {
|
||||
String token = UUID.randomUUID().toString();
|
||||
sessions.put(token, new AuthenticatedUser(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getRole(),
|
||||
user.getDepartmentId()
|
||||
));
|
||||
return token;
|
||||
}
|
||||
|
||||
public Optional<AuthenticatedUser> findByToken(String token) {
|
||||
if (token == null || token.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(sessions.get(token));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.magistr.app.config.auth;
|
||||
|
||||
import com.magistr.app.model.Role;
|
||||
|
||||
public record AuthenticatedUser(Long id, String username, Role role, Long departmentId) {
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.magistr.app.config.auth;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.magistr.app.model.Role;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class AuthorizationInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final AuthSessionService sessionService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public AuthorizationInterceptor(AuthSessionService sessionService, ObjectMapper objectMapper) {
|
||||
this.sessionService = sessionService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
|
||||
if (!request.getRequestURI().startsWith("/api/") || "OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||
return true;
|
||||
}
|
||||
if (request.getRequestURI().equals("/api/auth/login")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String token = bearerToken(request.getHeader("Authorization"));
|
||||
AuthenticatedUser user = sessionService.findByToken(token).orElse(null);
|
||||
if (user == null) {
|
||||
writeError(response, HttpServletResponse.SC_UNAUTHORIZED, "Требуется вход в систему");
|
||||
return false;
|
||||
}
|
||||
|
||||
AuthContext.setCurrentUser(user);
|
||||
RequireRoles roles = resolveRoles(handler);
|
||||
if (roles != null && Arrays.stream(roles.value()).noneMatch(role -> role == user.role())) {
|
||||
writeError(response, HttpServletResponse.SC_FORBIDDEN, "Недостаточно прав для выполнения операции");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||
AuthContext.clear();
|
||||
}
|
||||
|
||||
private RequireRoles resolveRoles(Object handler) {
|
||||
if (!(handler instanceof HandlerMethod handlerMethod)) {
|
||||
return null;
|
||||
}
|
||||
RequireRoles methodRoles = handlerMethod.getMethodAnnotation(RequireRoles.class);
|
||||
if (methodRoles != null) {
|
||||
return methodRoles;
|
||||
}
|
||||
return handlerMethod.getBeanType().getAnnotation(RequireRoles.class);
|
||||
}
|
||||
|
||||
private String bearerToken(String header) {
|
||||
if (header == null || !header.startsWith("Bearer ")) {
|
||||
return null;
|
||||
}
|
||||
return header.substring("Bearer ".length()).trim();
|
||||
}
|
||||
|
||||
private void writeError(HttpServletResponse response, int status, String message) throws IOException {
|
||||
response.setStatus(status);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
objectMapper.writeValue(response.getOutputStream(), Map.of("message", message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.magistr.app.config.auth;
|
||||
|
||||
import com.magistr.app.model.Role;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
public @interface RequireRoles {
|
||||
Role[] value();
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.magistr.app.config.tenant;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.magistr.app.config.auth.AuthorizationInterceptor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -123,6 +124,9 @@ public class TenantDataSourceConfig implements WebMvcConfigurer {
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private TenantRoutingDataSource tenantRoutingDataSource;
|
||||
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private AuthorizationInterceptor authorizationInterceptor;
|
||||
|
||||
@Bean
|
||||
public TenantInterceptor tenantInterceptor(TenantRoutingDataSource routingDataSource) {
|
||||
TenantInterceptor interceptor = new TenantInterceptor();
|
||||
@@ -134,6 +138,7 @@ public class TenantDataSourceConfig implements WebMvcConfigurer {
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// Вызываем метод-бин с переданным параметром (будет перехвачен CGLIB)
|
||||
registry.addInterceptor(tenantInterceptor(tenantRoutingDataSource)).addPathPatterns("/**");
|
||||
registry.addInterceptor(authorizationInterceptor).addPathPatterns("/api/**");
|
||||
}
|
||||
|
||||
private List<TenantConfig> loadTenantsFromFile() {
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.*;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
import com.magistr.app.service.ScheduleGeneratorService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/calendar")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class AcademicCalendarAdminController {
|
||||
|
||||
private final AcademicYearRepository academicYearRepository;
|
||||
private final SemesterRepository semesterRepository;
|
||||
private final AcademicCalendarActivityTypeRepository activityTypeRepository;
|
||||
private final AcademicCalendarDayRepository calendarDayRepository;
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
|
||||
public AcademicCalendarAdminController(AcademicYearRepository academicYearRepository,
|
||||
SemesterRepository semesterRepository,
|
||||
AcademicCalendarActivityTypeRepository activityTypeRepository,
|
||||
AcademicCalendarDayRepository calendarDayRepository,
|
||||
ScheduleGeneratorService scheduleGeneratorService) {
|
||||
this.academicYearRepository = academicYearRepository;
|
||||
this.semesterRepository = semesterRepository;
|
||||
this.activityTypeRepository = activityTypeRepository;
|
||||
this.calendarDayRepository = calendarDayRepository;
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
}
|
||||
|
||||
@GetMapping("/years")
|
||||
public List<AcademicYearDto> getYears() {
|
||||
return academicYearRepository.findAll().stream()
|
||||
.sorted(Comparator.comparing(AcademicYear::getStartDate).reversed())
|
||||
.map(this::toAcademicYearDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping("/years")
|
||||
public ResponseEntity<?> createYear(@RequestBody AcademicYearDto request) {
|
||||
String validationError = validateYear(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
if (academicYearRepository.findByTitle(request.title()).isPresent()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год с таким названием уже существует"));
|
||||
}
|
||||
|
||||
AcademicYear year = new AcademicYear();
|
||||
applyYear(year, request);
|
||||
return ResponseEntity.ok(toAcademicYearDto(academicYearRepository.save(year)));
|
||||
}
|
||||
|
||||
@PutMapping("/years/{id}")
|
||||
public ResponseEntity<?> updateYear(@PathVariable Long id, @RequestBody AcademicYearDto request) {
|
||||
AcademicYear year = academicYearRepository.findById(id).orElse(null);
|
||||
if (year == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
String validationError = validateYear(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
applyYear(year, request);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toAcademicYearDto(academicYearRepository.save(year)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/years/{id}")
|
||||
public ResponseEntity<?> deleteYear(@PathVariable Long id) {
|
||||
if (!academicYearRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
academicYearRepository.deleteById(id);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Учебный год удалён"));
|
||||
}
|
||||
|
||||
@GetMapping("/years/{academicYearId}/semesters")
|
||||
public List<SemesterDto> getSemesters(@PathVariable Long academicYearId) {
|
||||
return semesterRepository.findByAcademicYearIdOrderByStartDateAsc(academicYearId).stream()
|
||||
.map(this::toSemesterDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping("/years/{academicYearId}/semesters")
|
||||
public ResponseEntity<?> createSemester(@PathVariable Long academicYearId, @RequestBody SemesterDto request) {
|
||||
AcademicYear year = academicYearRepository.findById(academicYearId).orElse(null);
|
||||
if (year == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год не найден"));
|
||||
}
|
||||
String validationError = validateSemester(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
if (semesterRepository.findByAcademicYearIdAndSemesterType(academicYearId, request.semesterType()).isPresent()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Такой семестр уже есть в учебном году"));
|
||||
}
|
||||
|
||||
Semester semester = new Semester();
|
||||
semester.setAcademicYear(year);
|
||||
applySemester(semester, request);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toSemesterDto(semesterRepository.save(semester)));
|
||||
}
|
||||
|
||||
@PutMapping("/semesters/{id}")
|
||||
public ResponseEntity<?> updateSemester(@PathVariable Long id, @RequestBody SemesterDto request) {
|
||||
Semester semester = semesterRepository.findById(id).orElse(null);
|
||||
if (semester == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
String validationError = validateSemester(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
applySemester(semester, request);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toSemesterDto(semesterRepository.save(semester)));
|
||||
}
|
||||
|
||||
@GetMapping("/activity-types")
|
||||
public List<AcademicCalendarActivityTypeDto> getActivityTypes() {
|
||||
return activityTypeRepository.findAllByOrderByDisplayOrderAscCodeAsc().stream()
|
||||
.map(this::toActivityTypeDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping("/activity-types")
|
||||
public ResponseEntity<?> createActivityType(@RequestBody AcademicCalendarActivityTypeDto request) {
|
||||
String validationError = validateActivityType(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
if (activityTypeRepository.findByCode(request.code().trim()).isPresent()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Код активности уже существует"));
|
||||
}
|
||||
AcademicCalendarActivityType activityType = new AcademicCalendarActivityType();
|
||||
applyActivityType(activityType, request);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toActivityTypeDto(activityTypeRepository.save(activityType)));
|
||||
}
|
||||
|
||||
@PutMapping("/activity-types/{id}")
|
||||
public ResponseEntity<?> updateActivityType(@PathVariable Long id,
|
||||
@RequestBody AcademicCalendarActivityTypeDto request) {
|
||||
AcademicCalendarActivityType activityType = activityTypeRepository.findById(id).orElse(null);
|
||||
if (activityType == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
String validationError = validateActivityType(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
if (activityTypeRepository.findByCode(request.code().trim())
|
||||
.filter(existing -> !existing.getId().equals(id))
|
||||
.isPresent()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Код активности уже существует"));
|
||||
}
|
||||
applyActivityType(activityType, request);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toActivityTypeDto(activityTypeRepository.save(activityType)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/activity-types/{id}")
|
||||
public ResponseEntity<?> deleteActivityType(@PathVariable Long id) {
|
||||
if (!activityTypeRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (calendarDayRepository.existsByActivityTypeId(id)) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Код активности используется в календарных графиках"));
|
||||
}
|
||||
activityTypeRepository.deleteById(id);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Код активности удалён"));
|
||||
}
|
||||
|
||||
private String validateYear(AcademicYearDto request) {
|
||||
if (request.title() == null || request.title().isBlank()) {
|
||||
return "Название учебного года обязательно";
|
||||
}
|
||||
if (request.startDate() == null || request.endDate() == null) {
|
||||
return "Даты начала и окончания учебного года обязательны";
|
||||
}
|
||||
if (request.endDate().isBefore(request.startDate())) {
|
||||
return "Дата окончания не может быть раньше даты начала";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String validateSemester(SemesterDto request) {
|
||||
if (request.semesterType() == null) {
|
||||
return "Тип семестра обязателен";
|
||||
}
|
||||
if (request.startDate() == null || request.endDate() == null) {
|
||||
return "Даты начала и окончания семестра обязательны";
|
||||
}
|
||||
if (request.endDate().isBefore(request.startDate())) {
|
||||
return "Дата окончания семестра не может быть раньше даты начала";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String validateActivityType(AcademicCalendarActivityTypeDto request) {
|
||||
if (request == null) {
|
||||
return "Передайте код активности";
|
||||
}
|
||||
if (request.code() == null || request.code().isBlank()) {
|
||||
return "Код активности обязателен";
|
||||
}
|
||||
if (request.name() == null || request.name().isBlank()) {
|
||||
return "Название активности обязательно";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyYear(AcademicYear year, AcademicYearDto request) {
|
||||
year.setTitle(request.title().trim());
|
||||
year.setStartDate(request.startDate());
|
||||
year.setEndDate(request.endDate());
|
||||
}
|
||||
|
||||
private void applySemester(Semester semester, SemesterDto request) {
|
||||
semester.setSemesterType(request.semesterType());
|
||||
semester.setStartDate(request.startDate());
|
||||
semester.setEndDate(request.endDate());
|
||||
}
|
||||
|
||||
private void applyActivityType(AcademicCalendarActivityType activityType, AcademicCalendarActivityTypeDto request) {
|
||||
activityType.setCode(request.code().trim());
|
||||
activityType.setName(request.name().trim());
|
||||
activityType.setAllowSchedule(Boolean.TRUE.equals(request.allowSchedule()));
|
||||
activityType.setColorCode(request.colorCode() == null || request.colorCode().isBlank()
|
||||
? "#64748b"
|
||||
: request.colorCode().trim());
|
||||
activityType.setDisplayOrder(request.displayOrder() == null ? 100 : request.displayOrder());
|
||||
activityType.setDescription(request.description() == null || request.description().isBlank()
|
||||
? null
|
||||
: request.description().trim());
|
||||
}
|
||||
|
||||
private AcademicYearDto toAcademicYearDto(AcademicYear year) {
|
||||
List<SemesterDto> semesters = semesterRepository.findByAcademicYearIdOrderByStartDateAsc(year.getId())
|
||||
.stream()
|
||||
.map(this::toSemesterDto)
|
||||
.toList();
|
||||
return new AcademicYearDto(year.getId(), year.getTitle(), year.getStartDate(), year.getEndDate(), semesters);
|
||||
}
|
||||
|
||||
private SemesterDto toSemesterDto(Semester semester) {
|
||||
return new SemesterDto(
|
||||
semester.getId(),
|
||||
semester.getAcademicYear().getId(),
|
||||
semester.getAcademicYear().getTitle(),
|
||||
semester.getSemesterType(),
|
||||
semester.getStartDate(),
|
||||
semester.getEndDate()
|
||||
);
|
||||
}
|
||||
|
||||
private AcademicCalendarActivityTypeDto toActivityTypeDto(AcademicCalendarActivityType activityType) {
|
||||
return new AcademicCalendarActivityTypeDto(
|
||||
activityType.getId(),
|
||||
activityType.getCode(),
|
||||
activityType.getName(),
|
||||
activityType.getAllowSchedule(),
|
||||
activityType.getColorCode(),
|
||||
activityType.getDisplayOrder(),
|
||||
activityType.getDescription()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.*;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
import com.magistr.app.service.ScheduleGeneratorService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/academic-calendars")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class AcademicCalendarController {
|
||||
|
||||
private final AcademicCalendarRepository calendarRepository;
|
||||
private final AcademicCalendarDayRepository calendarDayRepository;
|
||||
private final AcademicCalendarActivityTypeRepository activityTypeRepository;
|
||||
private final AcademicYearRepository academicYearRepository;
|
||||
private final SpecialtiesRepository specialtiesRepository;
|
||||
private final SpecialtyProfileRepository profileRepository;
|
||||
private final EducationFormRepository educationFormRepository;
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
|
||||
public AcademicCalendarController(AcademicCalendarRepository calendarRepository,
|
||||
AcademicCalendarDayRepository calendarDayRepository,
|
||||
AcademicCalendarActivityTypeRepository activityTypeRepository,
|
||||
AcademicYearRepository academicYearRepository,
|
||||
SpecialtiesRepository specialtiesRepository,
|
||||
SpecialtyProfileRepository profileRepository,
|
||||
EducationFormRepository educationFormRepository,
|
||||
ScheduleGeneratorService scheduleGeneratorService) {
|
||||
this.calendarRepository = calendarRepository;
|
||||
this.calendarDayRepository = calendarDayRepository;
|
||||
this.activityTypeRepository = activityTypeRepository;
|
||||
this.academicYearRepository = academicYearRepository;
|
||||
this.specialtiesRepository = specialtiesRepository;
|
||||
this.profileRepository = profileRepository;
|
||||
this.educationFormRepository = educationFormRepository;
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<AcademicCalendarDto> getCalendars(@RequestParam(required = false) Long academicYearId,
|
||||
@RequestParam(required = false) Long specialtyId,
|
||||
@RequestParam(required = false) Long profileId) {
|
||||
return calendarRepository.findAllWithDetails(academicYearId, specialtyId, profileId).stream()
|
||||
.map(this::toCalendarDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<?> getCalendar(@PathVariable Long id) {
|
||||
return calendarRepository.findByIdWithDetails(id)
|
||||
.<ResponseEntity<?>>map(calendar -> ResponseEntity.ok(toCalendarDto(calendar)))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> createCalendar(@RequestBody AcademicCalendarDto request) {
|
||||
try {
|
||||
AcademicCalendar calendar = new AcademicCalendar();
|
||||
applyCalendar(calendar, request);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toCalendarDto(calendarRepository.save(calendar)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> updateCalendar(@PathVariable Long id, @RequestBody AcademicCalendarDto request) {
|
||||
AcademicCalendar calendar = calendarRepository.findById(id).orElse(null);
|
||||
if (calendar == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
try {
|
||||
applyCalendar(calendar, request);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toCalendarDto(calendarRepository.save(calendar)));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteCalendar(@PathVariable Long id) {
|
||||
if (!calendarRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
try {
|
||||
calendarRepository.deleteById(id);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Календарный график удалён"));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "Нельзя удалить график, который назначен учебным группам"));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/grid")
|
||||
public ResponseEntity<?> getGrid(@PathVariable Long id) {
|
||||
if (!calendarRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(calendarDayRepository.findByCalendarIdWithActivity(id).stream()
|
||||
.map(this::toGridDayDto)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/grid")
|
||||
@Transactional
|
||||
public ResponseEntity<?> saveGrid(@PathVariable Long id, @RequestBody List<AcademicCalendarGridDayDto> rows) {
|
||||
AcademicCalendar calendar = calendarRepository.findByIdWithDetails(id).orElse(null);
|
||||
if (calendar == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Передайте хотя бы одну ячейку графика"));
|
||||
}
|
||||
|
||||
try {
|
||||
calendarDayRepository.deleteByCalendarId(id);
|
||||
for (AcademicCalendarGridDayDto row : rows) {
|
||||
AcademicCalendarDay day = buildGridDay(calendar, row);
|
||||
calendarDayRepository.save(day);
|
||||
}
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Календарный учебный график сохранён"));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private void applyCalendar(AcademicCalendar calendar, AcademicCalendarDto request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("Передайте данные календарного графика");
|
||||
}
|
||||
if (request.title() == null || request.title().isBlank()) {
|
||||
throw new IllegalArgumentException("Название графика обязательно");
|
||||
}
|
||||
if (request.courseCount() == null || request.courseCount() <= 0) {
|
||||
throw new IllegalArgumentException("Количество курсов должно быть больше нуля");
|
||||
}
|
||||
if (request.academicYearId() == null || request.specialtyId() == null
|
||||
|| request.specialtyProfileId() == null || request.studyFormId() == null) {
|
||||
throw new IllegalArgumentException("Учебный год, специальность, профиль и форма обучения обязательны");
|
||||
}
|
||||
|
||||
AcademicYear year = academicYearRepository.findById(request.academicYearId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Учебный год не найден"));
|
||||
Speciality speciality = specialtiesRepository.findById(request.specialtyId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Специальность не найдена"));
|
||||
SpecialtyProfile profile = profileRepository.findById(request.specialtyProfileId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Профиль обучения не найден"));
|
||||
if (!profile.getSpeciality().getId().equals(speciality.getId())) {
|
||||
throw new IllegalArgumentException("Профиль не относится к выбранной специальности");
|
||||
}
|
||||
EducationForm studyForm = educationFormRepository.findById(request.studyFormId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Форма обучения не найдена"));
|
||||
|
||||
calendar.setTitle(request.title().trim());
|
||||
calendar.setAcademicYear(year);
|
||||
calendar.setSpeciality(speciality);
|
||||
calendar.setSpecialtyProfile(profile);
|
||||
calendar.setStudyForm(studyForm);
|
||||
calendar.setCourseCount(request.courseCount());
|
||||
}
|
||||
|
||||
private AcademicCalendarDay buildGridDay(AcademicCalendar calendar, AcademicCalendarGridDayDto row) {
|
||||
if (row.courseNumber() == null || row.courseNumber() <= 0
|
||||
|| row.date() == null
|
||||
|| row.weekNumber() == null || row.weekNumber() <= 0
|
||||
|| row.dayOfWeek() == null || row.dayOfWeek() < 1 || row.dayOfWeek() > 7) {
|
||||
throw new IllegalArgumentException("Курс, дата, неделя и день недели обязательны");
|
||||
}
|
||||
if (row.date().isBefore(calendar.getAcademicYear().getStartDate())
|
||||
|| row.date().isAfter(calendar.getAcademicYear().getEndDate())) {
|
||||
throw new IllegalArgumentException("Дата выходит за пределы учебного года");
|
||||
}
|
||||
if (row.courseNumber() > calendar.getCourseCount()) {
|
||||
throw new IllegalArgumentException("Номер курса выходит за пределы графика");
|
||||
}
|
||||
|
||||
AcademicCalendarActivityType activityType = resolveActivityType(row);
|
||||
AcademicCalendarDay day = new AcademicCalendarDay();
|
||||
day.setAcademicCalendar(calendar);
|
||||
day.setCourseNumber(row.courseNumber());
|
||||
day.setDate(row.date());
|
||||
day.setWeekNumber(row.weekNumber());
|
||||
day.setDayOfWeek(row.dayOfWeek());
|
||||
day.setActivityType(activityType);
|
||||
return day;
|
||||
}
|
||||
|
||||
private AcademicCalendarActivityType resolveActivityType(AcademicCalendarGridDayDto row) {
|
||||
if (row.activityTypeId() != null) {
|
||||
return activityTypeRepository.findById(row.activityTypeId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Код активности не найден"));
|
||||
}
|
||||
if (row.activityCode() != null && !row.activityCode().isBlank()) {
|
||||
return activityTypeRepository.findByCode(row.activityCode().trim())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Код активности не найден"));
|
||||
}
|
||||
throw new IllegalArgumentException("Код активности обязателен");
|
||||
}
|
||||
|
||||
private AcademicCalendarDto toCalendarDto(AcademicCalendar calendar) {
|
||||
AcademicYear year = calendar.getAcademicYear();
|
||||
Speciality speciality = calendar.getSpeciality();
|
||||
SpecialtyProfile profile = calendar.getSpecialtyProfile();
|
||||
EducationForm studyForm = calendar.getStudyForm();
|
||||
return new AcademicCalendarDto(
|
||||
calendar.getId(),
|
||||
calendar.getTitle(),
|
||||
year.getId(),
|
||||
year.getTitle(),
|
||||
year.getStartDate(),
|
||||
year.getEndDate(),
|
||||
speciality.getId(),
|
||||
speciality.getSpecialityCode(),
|
||||
speciality.getSpecialityName(),
|
||||
profile.getId(),
|
||||
profile.getName(),
|
||||
studyForm.getId(),
|
||||
studyForm.getName(),
|
||||
calendar.getCourseCount()
|
||||
);
|
||||
}
|
||||
|
||||
private AcademicCalendarGridDayDto toGridDayDto(AcademicCalendarDay day) {
|
||||
AcademicCalendarActivityType activityType = day.getActivityType();
|
||||
return new AcademicCalendarGridDayDto(
|
||||
day.getId(),
|
||||
day.getAcademicCalendar().getId(),
|
||||
day.getCourseNumber(),
|
||||
day.getDate(),
|
||||
day.getWeekNumber(),
|
||||
day.getDayOfWeek(),
|
||||
activityType.getId(),
|
||||
activityType.getCode(),
|
||||
activityType.getName(),
|
||||
activityType.getAllowSchedule()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.dto.LoginRequest;
|
||||
import com.magistr.app.dto.LoginResponse;
|
||||
import com.magistr.app.config.auth.AuthSessionService;
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.model.User;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
@@ -10,7 +12,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@@ -18,16 +19,23 @@ public class AuthController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final BCryptPasswordEncoder passwordEncoder;
|
||||
private final AuthSessionService sessionService;
|
||||
|
||||
private static final Map<String, String> ROLE_REDIRECTS = Map.of(
|
||||
"ADMIN", "/admin/",
|
||||
"EDUCATION_OFFICE", "/admin/#schedule-view",
|
||||
"DEPARTMENT", "/admin/#department-workspace",
|
||||
"SCHEDULE_VIEWER", "/admin/#schedule-view",
|
||||
"TEACHER", "/teacher/",
|
||||
"STUDENT", "/student/"
|
||||
);
|
||||
|
||||
public AuthController(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
|
||||
public AuthController(UserRepository userRepository,
|
||||
BCryptPasswordEncoder passwordEncoder,
|
||||
AuthSessionService sessionService) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.sessionService = sessionService;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@@ -42,11 +50,30 @@ public class AuthController {
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
String token = UUID.randomUUID().toString();
|
||||
if (user.isArchivedRecord()) {
|
||||
return ResponseEntity
|
||||
.status(401)
|
||||
.body(new LoginResponse(false, "Пользователь архивирован", null, null, null, null));
|
||||
}
|
||||
String token = sessionService.createSession(user);
|
||||
String roleName = user.getRole().name();
|
||||
String redirect = ROLE_REDIRECTS.getOrDefault(roleName, "/");
|
||||
Long departmentId = user.getDepartmentId();
|
||||
|
||||
return ResponseEntity.ok(new LoginResponse(true, "OK", token, roleName, redirect, departmentId));
|
||||
return ResponseEntity.ok(new LoginResponse(true, "OK", token, roleName, redirect, departmentId, user.getId()));
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<?> me() {
|
||||
var user = AuthContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("message", "Требуется вход в систему"));
|
||||
}
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"userId", user.id(),
|
||||
"username", user.username(),
|
||||
"role", user.role().name(),
|
||||
"departmentId", user.departmentId()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.ClassroomRequest;
|
||||
import com.magistr.app.dto.ClassroomResponse;
|
||||
import com.magistr.app.model.Classroom;
|
||||
import com.magistr.app.model.Equipment;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.repository.ClassroomRepository;
|
||||
import com.magistr.app.repository.EquipmentRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -15,6 +18,7 @@ import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/classrooms")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class ClassroomController {
|
||||
|
||||
private final ClassroomRepository classroomRepository;
|
||||
@@ -26,8 +30,12 @@ public class ClassroomController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<ClassroomResponse> getAllClassrooms() {
|
||||
return classroomRepository.findAll().stream()
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public List<ClassroomResponse> getAllClassrooms(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
List<Classroom> classrooms = includeArchived
|
||||
? classroomRepository.findAll()
|
||||
: classroomRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
return classrooms.stream()
|
||||
.map(this::mapToResponse)
|
||||
.toList();
|
||||
}
|
||||
@@ -47,10 +55,14 @@ public class ClassroomController {
|
||||
Classroom classroom = new Classroom();
|
||||
classroom.setName(request.getName().trim());
|
||||
classroom.setCapacity(request.getCapacity());
|
||||
classroom.setBuilding(cleanText(request.getBuilding()));
|
||||
classroom.setFloor(request.getFloor());
|
||||
classroom.setIsAvailable(request.getIsAvailable() != null ? request.getIsAvailable() : true);
|
||||
|
||||
if (request.getEquipmentIds() != null && !request.getEquipmentIds().isEmpty()) {
|
||||
List<Equipment> equipments = equipmentRepository.findAllById(request.getEquipmentIds());
|
||||
List<Equipment> equipments = equipmentRepository.findAllById(request.getEquipmentIds()).stream()
|
||||
.filter(Equipment::isActiveRecord)
|
||||
.toList();
|
||||
classroom.setEquipments(new java.util.HashSet<>(equipments));
|
||||
}
|
||||
|
||||
@@ -80,12 +92,23 @@ public class ClassroomController {
|
||||
classroom.setCapacity(request.getCapacity());
|
||||
}
|
||||
|
||||
if (request.getBuilding() != null) {
|
||||
classroom.setBuilding(cleanText(request.getBuilding()));
|
||||
}
|
||||
|
||||
if (request.getFloor() != null) {
|
||||
classroom.setFloor(request.getFloor());
|
||||
}
|
||||
|
||||
if (request.getIsAvailable() != null) {
|
||||
classroom.setIsAvailable(request.getIsAvailable());
|
||||
}
|
||||
|
||||
if (request.getEquipmentIds() != null) {
|
||||
List<Equipment> equipments = equipmentRepository.findAllById(request.getEquipmentIds());
|
||||
equipments = equipments.stream()
|
||||
.filter(Equipment::isActiveRecord)
|
||||
.toList();
|
||||
classroom.setEquipments(new java.util.HashSet<>(equipments));
|
||||
}
|
||||
|
||||
@@ -95,15 +118,37 @@ public class ClassroomController {
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteClassroom(@PathVariable Long id) {
|
||||
if (!classroomRepository.existsById(id)) {
|
||||
Optional<Classroom> classroomOpt = classroomRepository.findById(id);
|
||||
if (classroomOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
classroomRepository.deleteById(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Аудитория удалена"));
|
||||
Classroom classroom = classroomOpt.get();
|
||||
classroom.archive("Выведена из эксплуатации");
|
||||
classroom.setIsAvailable(false);
|
||||
classroomRepository.save(classroom);
|
||||
return ResponseEntity.ok(Map.of("message", "Аудитория выведена из эксплуатации"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
public ResponseEntity<?> restoreClassroom(@PathVariable Long id) {
|
||||
Optional<Classroom> classroomOpt = classroomRepository.findById(id);
|
||||
if (classroomOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
Classroom classroom = classroomOpt.get();
|
||||
classroom.restore();
|
||||
classroom.setIsAvailable(true);
|
||||
classroomRepository.save(classroom);
|
||||
return ResponseEntity.ok(mapToResponse(classroom));
|
||||
}
|
||||
|
||||
private ClassroomResponse mapToResponse(Classroom c) {
|
||||
return new ClassroomResponse(c.getId(), c.getName(), c.getCapacity(), c.getIsAvailable(),
|
||||
return new ClassroomResponse(c.getId(), c.getName(), c.getCapacity(), c.getBuilding(), c.getFloor(), c.getIsAvailable(),
|
||||
c.getStatus(), c.getArchivedAt(), c.getArchiveReason(),
|
||||
new java.util.ArrayList<>(c.getEquipments()));
|
||||
}
|
||||
|
||||
private String cleanText(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.config.tenant.ConfigMapUpdater;
|
||||
import com.magistr.app.config.tenant.TenantConfig;
|
||||
import com.magistr.app.config.tenant.TenantConfigWatcher;
|
||||
import com.magistr.app.config.tenant.TenantContext;
|
||||
import com.magistr.app.config.tenant.TenantRoutingDataSource;
|
||||
import com.magistr.app.model.Role;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -24,6 +26,7 @@ import java.util.Map;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/database")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class DatabaseController {
|
||||
|
||||
private final TenantRoutingDataSource routingDataSource;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateDepartmentRequest;
|
||||
import com.magistr.app.dto.DepartmentResponse;
|
||||
import com.magistr.app.model.Department;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.repository.DepartmentRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -15,6 +18,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/departments")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class DepartmentController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DepartmentController.class);
|
||||
@@ -26,10 +30,13 @@ public class DepartmentController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Department> getAllDepartments() {
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public List<Department> getAllDepartments(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
logger.info("Получен запрос на получение списка кафедр");
|
||||
try {
|
||||
List<Department> departments = departmentRepository.findAll();
|
||||
List<Department> departments = includeArchived
|
||||
? departmentRepository.findAll()
|
||||
: departmentRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
List<Department> response = departments.stream()
|
||||
.map( d -> new Department(
|
||||
d.getId(),
|
||||
@@ -92,15 +99,87 @@ public class DepartmentController {
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/id")
|
||||
public ResponseEntity<?> deleteDepartment(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление кафедры с ID: {}", id);
|
||||
if (!departmentRepository.existsById(id)) {
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> updateDepartment(@PathVariable Long id, @RequestBody CreateDepartmentRequest request) {
|
||||
logger.info("Получен запрос на обновление кафедры с ID: {}", id);
|
||||
|
||||
Department department = departmentRepository.findById(id).orElse(null);
|
||||
if (department == null) {
|
||||
logger.info("Кафедра с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
departmentRepository.deleteById(id);
|
||||
logger.info("Кафедра с ID - {} успешно удалена", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Кафедра удалена"));
|
||||
|
||||
try {
|
||||
if (request.getDepartmentName() == null || request.getDepartmentName().isBlank()) {
|
||||
String errorMessage = "Название кафедры обязательно";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
if (request.getDepartmentCode() == null || request.getDepartmentCode() == 0) {
|
||||
String errorMessage = "Код кафедры обязателен";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
String departmentName = request.getDepartmentName().trim();
|
||||
if (departmentRepository.findByDepartmentName(departmentName)
|
||||
.filter(existing -> !existing.getId().equals(id))
|
||||
.isPresent()) {
|
||||
String errorMessage = "Кафедра с таким названием уже существует";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
if (departmentRepository.findByDepartmentCode(request.getDepartmentCode())
|
||||
.filter(existing -> !existing.getId().equals(id))
|
||||
.isPresent()) {
|
||||
String errorMessage = "Кафедра с таким кодом уже существует";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
department.setDepartmentName(departmentName);
|
||||
department.setDepartmentCode(request.getDepartmentCode());
|
||||
departmentRepository.save(department);
|
||||
|
||||
logger.info("Кафедра с ID - {} успешно обновлена", id);
|
||||
return ResponseEntity.ok(new DepartmentResponse(
|
||||
department.getId(),
|
||||
department.getDepartmentName(),
|
||||
department.getDepartmentCode()
|
||||
));
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при обновлении кафедры с ID - {}: {}", id, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message", "Произошла ошибка при обновлении кафедры " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteDepartment(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление кафедры с ID: {}", id);
|
||||
Department department = departmentRepository.findById(id).orElse(null);
|
||||
if (department == null) {
|
||||
logger.info("Кафедра с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
department.archive("Кафедра архивирована");
|
||||
departmentRepository.save(department);
|
||||
logger.info("Кафедра с ID - {} успешно архивирована", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Кафедра архивирована"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
public ResponseEntity<?> restoreDepartment(@PathVariable Long id) {
|
||||
Department department = departmentRepository.findById(id).orElse(null);
|
||||
if (department == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
department.restore();
|
||||
departmentRepository.save(department);
|
||||
return ResponseEntity.ok(new DepartmentResponse(
|
||||
department.getId(),
|
||||
department.getDepartmentName(),
|
||||
department.getDepartmentCode()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateSubjectRequest;
|
||||
import com.magistr.app.dto.SubjectCommentDto;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.SubjectCommentRepository;
|
||||
import com.magistr.app.repository.SubjectRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import com.magistr.app.service.ScheduleQueryService;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/department")
|
||||
@RequireRoles({Role.ADMIN, Role.DEPARTMENT})
|
||||
public class DepartmentWorkspaceController {
|
||||
|
||||
private final SubjectRepository subjectRepository;
|
||||
private final SubjectCommentRepository subjectCommentRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final ScheduleQueryService scheduleQueryService;
|
||||
|
||||
public DepartmentWorkspaceController(SubjectRepository subjectRepository,
|
||||
SubjectCommentRepository subjectCommentRepository,
|
||||
UserRepository userRepository,
|
||||
ScheduleQueryService scheduleQueryService) {
|
||||
this.subjectRepository = subjectRepository;
|
||||
this.subjectCommentRepository = subjectCommentRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.scheduleQueryService = scheduleQueryService;
|
||||
}
|
||||
|
||||
@GetMapping("/subjects")
|
||||
public List<Subject> getSubjects(@RequestParam(required = false) Long departmentId) {
|
||||
Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
|
||||
return subjectRepository.findByDepartmentIdAndStatusNot(effectiveDepartmentId, LifecycleEntity.STATUS_ARCHIVED);
|
||||
}
|
||||
|
||||
@PostMapping("/subjects/import")
|
||||
public ResponseEntity<?> importSubjects(@RequestBody List<CreateSubjectRequest> requests) {
|
||||
if (requests == null || requests.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Передайте список дисциплин для загрузки"));
|
||||
}
|
||||
Long requestedDepartmentId = requests.stream()
|
||||
.map(CreateSubjectRequest::getDepartmentId)
|
||||
.filter(id -> id != null && id > 0)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
Long departmentId = effectiveDepartmentId(requestedDepartmentId);
|
||||
if (departmentId == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Кафедра обязательна"));
|
||||
}
|
||||
List<Subject> saved = requests.stream()
|
||||
.filter(request -> request.getName() != null && !request.getName().isBlank())
|
||||
.map(request -> {
|
||||
Subject subject = subjectRepository.findByName(request.getName().trim()).orElseGet(Subject::new);
|
||||
subject.setName(request.getName().trim());
|
||||
subject.setCode(request.getCode() == null || request.getCode().isBlank() ? null : request.getCode().trim());
|
||||
subject.setDepartmentId(departmentId);
|
||||
if (subject.isArchivedRecord()) {
|
||||
subject.restore();
|
||||
}
|
||||
return subjectRepository.save(subject);
|
||||
})
|
||||
.toList();
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "Дисциплины загружены",
|
||||
"count", saved.size()
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/subjects/{subjectId}/comments")
|
||||
public ResponseEntity<?> getComments(@PathVariable Long subjectId) {
|
||||
Subject subject = subjectRepository.findById(subjectId).orElse(null);
|
||||
if (subject == null || !canAccessDepartment(subject.getDepartmentId())) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(subjectCommentRepository.findBySubjectIdOrderByCreatedAtDesc(subjectId).stream()
|
||||
.map(this::toCommentDto)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/subjects/{subjectId}/comments")
|
||||
public ResponseEntity<?> addComment(@PathVariable Long subjectId, @RequestBody Map<String, String> request) {
|
||||
Subject subject = subjectRepository.findById(subjectId).orElse(null);
|
||||
if (subject == null || !canAccessDepartment(subject.getDepartmentId())) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
String commentText = request == null ? null : request.get("comment");
|
||||
if (commentText == null || commentText.isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Комментарий обязателен"));
|
||||
}
|
||||
SubjectComment comment = new SubjectComment();
|
||||
comment.setSubject(subject);
|
||||
comment.setComment(commentText.trim());
|
||||
Long currentUserId = AuthContext.currentUserId();
|
||||
if (currentUserId != null) {
|
||||
userRepository.findById(currentUserId).ifPresent(comment::setAuthor);
|
||||
}
|
||||
return ResponseEntity.ok(toCommentDto(subjectCommentRepository.save(comment)));
|
||||
}
|
||||
|
||||
@GetMapping("/teachers")
|
||||
public List<?> getTeachers(@RequestParam(required = false) Long departmentId) {
|
||||
Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
|
||||
return userRepository.findByRoleAndDepartmentIdAndStatusNot(Role.TEACHER, effectiveDepartmentId, LifecycleEntity.STATUS_ARCHIVED);
|
||||
}
|
||||
|
||||
@GetMapping("/schedule")
|
||||
public ResponseEntity<?> getSchedule(
|
||||
@RequestParam(required = false) Long departmentId,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate
|
||||
) {
|
||||
Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
|
||||
return ResponseEntity.ok(scheduleQueryService.search(null, null, null, effectiveDepartmentId,
|
||||
null, null, null, null, startDate, endDate));
|
||||
}
|
||||
|
||||
private SubjectCommentDto toCommentDto(SubjectComment comment) {
|
||||
User author = comment.getAuthor();
|
||||
return new SubjectCommentDto(
|
||||
comment.getId(),
|
||||
comment.getSubject().getId(),
|
||||
author == null ? null : author.getId(),
|
||||
author == null ? null : author.getFullName(),
|
||||
comment.getComment(),
|
||||
comment.getCreatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private Long effectiveDepartmentId(Long requestedDepartmentId) {
|
||||
var user = AuthContext.getCurrentUser();
|
||||
if (user != null && user.role() == Role.DEPARTMENT) {
|
||||
return user.departmentId();
|
||||
}
|
||||
return requestedDepartmentId == null && user != null ? user.departmentId() : requestedDepartmentId;
|
||||
}
|
||||
|
||||
private boolean canAccessDepartment(Long departmentId) {
|
||||
var user = AuthContext.getCurrentUser();
|
||||
return user == null
|
||||
|| user.role() == Role.ADMIN
|
||||
|| (user.role() == Role.DEPARTMENT && departmentId.equals(user.departmentId()));
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.model.EducationForm;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.StudentGroup;
|
||||
import com.magistr.app.repository.AcademicCalendarRepository;
|
||||
import com.magistr.app.repository.EducationFormRepository;
|
||||
import com.magistr.app.repository.GroupRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -12,20 +15,25 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/education-forms")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class EducationFormController {
|
||||
|
||||
private final EducationFormRepository educationFormRepository;
|
||||
private final GroupRepository groupRepository;
|
||||
private final AcademicCalendarRepository academicCalendarRepository;
|
||||
|
||||
public EducationFormController(EducationFormRepository educationFormRepository,
|
||||
GroupRepository groupRepository) {
|
||||
GroupRepository groupRepository,
|
||||
AcademicCalendarRepository academicCalendarRepository) {
|
||||
this.educationFormRepository = educationFormRepository;
|
||||
this.groupRepository = groupRepository;
|
||||
this.academicCalendarRepository = academicCalendarRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public List<Map<String, Object>> getAll() {
|
||||
return educationFormRepository.findAll().stream()
|
||||
return educationFormRepository.findAllByOrderByNameAsc().stream()
|
||||
.map(ef -> Map.<String, Object>of("id", ef.getId(), "name", ef.getName()))
|
||||
.toList();
|
||||
}
|
||||
@@ -54,12 +62,17 @@ public class EducationFormController {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
// Check if any groups use this education form
|
||||
// Нельзя удалить форму обучения, пока она используется группами или графиками.
|
||||
List<StudentGroup> linked = groupRepository.findByEducationFormId(id);
|
||||
if (!linked.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"message", "Невозможно удалить: есть привязанные группы (" + linked.size() + ")"));
|
||||
}
|
||||
long linkedCalendars = academicCalendarRepository.countByStudyFormId(id);
|
||||
if (linkedCalendars > 0) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"message", "Невозможно удалить: есть привязанные календарные графики (" + linkedCalendars + ")"));
|
||||
}
|
||||
|
||||
educationFormRepository.deleteById(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Форма обучения удалена"));
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.Equipment;
|
||||
import com.magistr.app.repository.EquipmentRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -10,6 +13,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/equipments")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class EquipmentController {
|
||||
|
||||
private final EquipmentRepository equipmentRepository;
|
||||
@@ -19,8 +23,8 @@ public class EquipmentController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Equipment> getAllEquipments() {
|
||||
return equipmentRepository.findAll();
|
||||
public List<Equipment> getAllEquipments(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
return includeArchived ? equipmentRepository.findAll() : equipmentRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@@ -42,10 +46,23 @@ public class EquipmentController {
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteEquipment(@PathVariable Long id) {
|
||||
if (!equipmentRepository.existsById(id)) {
|
||||
Equipment equipment = equipmentRepository.findById(id).orElse(null);
|
||||
if (equipment == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
equipmentRepository.deleteById(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Оборудование удалено"));
|
||||
equipment.archive("Оборудование архивировано");
|
||||
equipmentRepository.save(equipment);
|
||||
return ResponseEntity.ok(Map.of("message", "Оборудование архивировано"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
public ResponseEntity<?> restoreEquipment(@PathVariable Long id) {
|
||||
Equipment equipment = equipmentRepository.findById(id).orElse(null);
|
||||
if (equipment == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
equipment.restore();
|
||||
equipmentRepository.save(equipment);
|
||||
return ResponseEntity.ok(equipment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateGroupRequest;
|
||||
import com.magistr.app.dto.GroupCalendarAssignmentDto;
|
||||
import com.magistr.app.dto.GroupResponse;
|
||||
import com.magistr.app.model.AcademicCalendar;
|
||||
import com.magistr.app.model.AcademicYear;
|
||||
import com.magistr.app.model.EducationForm;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.Speciality;
|
||||
import com.magistr.app.model.SpecialtyProfile;
|
||||
import com.magistr.app.model.StudentGroup;
|
||||
import com.magistr.app.repository.EducationFormRepository;
|
||||
import com.magistr.app.repository.GroupRepository;
|
||||
import com.magistr.app.model.StudentGroupCalendarAssignment;
|
||||
import com.magistr.app.repository.*;
|
||||
import com.magistr.app.service.ScheduleGeneratorService;
|
||||
import com.magistr.app.utils.CourseAndSemesterCalculator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -13,49 +22,55 @@ import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Year;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/groups")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER, Role.STUDENT})
|
||||
public class GroupController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GroupController.class);
|
||||
|
||||
private final GroupRepository groupRepository;
|
||||
private final EducationFormRepository educationFormRepository;
|
||||
private final SpecialtiesRepository specialtiesRepository;
|
||||
private final SpecialtyProfileRepository specialtyProfileRepository;
|
||||
private final AcademicYearRepository academicYearRepository;
|
||||
private final AcademicCalendarRepository academicCalendarRepository;
|
||||
private final StudentGroupCalendarAssignmentRepository assignmentRepository;
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
|
||||
public GroupController(GroupRepository groupRepository,
|
||||
EducationFormRepository educationFormRepository) {
|
||||
EducationFormRepository educationFormRepository,
|
||||
SpecialtiesRepository specialtiesRepository,
|
||||
SpecialtyProfileRepository specialtyProfileRepository,
|
||||
AcademicYearRepository academicYearRepository,
|
||||
AcademicCalendarRepository academicCalendarRepository,
|
||||
StudentGroupCalendarAssignmentRepository assignmentRepository,
|
||||
ScheduleGeneratorService scheduleGeneratorService) {
|
||||
this.groupRepository = groupRepository;
|
||||
this.educationFormRepository = educationFormRepository;
|
||||
this.specialtiesRepository = specialtiesRepository;
|
||||
this.specialtyProfileRepository = specialtyProfileRepository;
|
||||
this.academicYearRepository = academicYearRepository;
|
||||
this.academicCalendarRepository = academicCalendarRepository;
|
||||
this.assignmentRepository = assignmentRepository;
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<GroupResponse> getAllGroups() {
|
||||
public List<GroupResponse> getAllGroups(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
logger.info("Получен запрос на получение всех групп");
|
||||
|
||||
try {
|
||||
List<StudentGroup> groups = groupRepository.findAll();
|
||||
List<StudentGroup> groups = includeArchived
|
||||
? groupRepository.findAll()
|
||||
: groupRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
|
||||
List<GroupResponse> response = groups.stream()
|
||||
.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()
|
||||
);
|
||||
})
|
||||
.map(this::mapToResponse)
|
||||
.toList();
|
||||
logger.info("Получено {} групп", response.size());
|
||||
return response;
|
||||
@@ -69,7 +84,7 @@ public class GroupController {
|
||||
public ResponseEntity<?> getGroupsByDepartmentId(@PathVariable Long departmentId) {
|
||||
logger.info("Получен запрос на получение списка групп для кафедры с ID - {}", departmentId);
|
||||
try {
|
||||
List<StudentGroup> groups = groupRepository.findByDepartmentId(departmentId);
|
||||
List<StudentGroup> groups = groupRepository.findByDepartmentIdAndStatusNot(departmentId, LifecycleEntity.STATUS_ARCHIVED);
|
||||
|
||||
if(groups.isEmpty()) {
|
||||
logger.info("Группы для кафедры с ID - {} не найдены", departmentId);
|
||||
@@ -78,21 +93,7 @@ public class GroupController {
|
||||
}
|
||||
|
||||
List<GroupResponse> response = groups.stream()
|
||||
.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()
|
||||
);
|
||||
})
|
||||
.map(this::mapToResponse)
|
||||
.toList();
|
||||
|
||||
logger.info("Найдено {} групп для кафедры с ID - {}", response.size(), departmentId);
|
||||
@@ -106,55 +107,30 @@ public class GroupController {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public ResponseEntity<?> createGroup(@RequestBody CreateGroupRequest request) {
|
||||
logger.info("Получен запрос на создание новой группы: name = {}, groupSize = {}, educationFormId = {}, departmentId = {}, yearStartStudy = {}",
|
||||
request.getName(), request.getGroupSize(), request.getEducationFormId(), request.getDepartmentId(), request.getYearStartStudy());
|
||||
try {
|
||||
if (request.getName() == null || request.getName().isBlank()) {
|
||||
String errorMessage = "Название группы обязательно";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
if (groupRepository.findByName(request.getName().trim()).isPresent()) {
|
||||
String errorMessage = "Группа с таким названием уже существует";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
if (request.getGroupSize() == null) {
|
||||
String errorMessage = "Численность группы обязательна";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
if (request.getEducationFormId() == null) {
|
||||
String errorMessage = "Форма обучения обязательна";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
if (request.getDepartmentId() == null || request.getDepartmentId() == 0) {
|
||||
String errorMessage = "ID кафедры обязателен";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
// if (request.getCourse() == null || request.getCourse() == 0) {
|
||||
// String errorMessage = "Курс обязателен";
|
||||
// logger.error("Ошибка валидации: {}", errorMessage);
|
||||
// return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
// }
|
||||
if (request.getYearStartStudy() == null || request.getYearStartStudy() == 0) {
|
||||
String errorMessage = "Год начала обучения обязателен";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
if (request.getSpecialityCode() == null || request.getSpecialityCode() == 0) {
|
||||
String errorMessage = "Код специальности обязателен";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
ResponseEntity<?> validationError = validateGroupRequest(request);
|
||||
if (validationError != null) {
|
||||
return validationError;
|
||||
}
|
||||
|
||||
Optional<EducationForm> efOpt = educationFormRepository.findById(request.getEducationFormId());
|
||||
if (efOpt.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Форма обучения не найдена"));
|
||||
}
|
||||
Speciality speciality = specialtiesRepository.findById(request.getEffectiveSpecialtyId())
|
||||
.orElse(null);
|
||||
if (speciality == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Специальность не найдена"));
|
||||
}
|
||||
SpecialtyProfile profile = specialtyProfileRepository.findById(request.getSpecialtyProfileId())
|
||||
.orElse(null);
|
||||
if (profile == null || !profile.getSpeciality().getId().equals(speciality.getId())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Профиль обучения не найден для выбранной специальности"));
|
||||
}
|
||||
|
||||
StudentGroup group = new StudentGroup();
|
||||
group.setName(request.getName().trim());
|
||||
@@ -162,20 +138,13 @@ public class GroupController {
|
||||
group.setEducationForm(efOpt.get());
|
||||
group.setDepartmentId(request.getDepartmentId());
|
||||
group.setYearStartStudy(request.getYearStartStudy());
|
||||
group.setSpecialityCode(request.getSpecialityCode());
|
||||
group.setSpeciality(speciality);
|
||||
group.setSpecialtyProfile(profile);
|
||||
groupRepository.save(group);
|
||||
|
||||
logger.info("Группа успешно создана с ID - {}", group.getId());
|
||||
|
||||
return ResponseEntity.ok(new GroupResponse(
|
||||
group.getId(),
|
||||
group.getName(),
|
||||
group.getGroupSize(),
|
||||
group.getEducationForm().getId(),
|
||||
group.getEducationForm().getName(),
|
||||
group.getDepartmentId(),
|
||||
group.getYearStartStudy(),
|
||||
group.getSpecialityCode()));
|
||||
return ResponseEntity.ok(mapToResponse(group));
|
||||
} catch (Exception e ) {
|
||||
logger.error("Ошибка при создании группы: {}", e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
@@ -183,15 +152,208 @@ public class GroupController {
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public ResponseEntity<?> updateGroup(@PathVariable Long id, @RequestBody CreateGroupRequest request) {
|
||||
logger.info("Получен запрос на обновление группы с ID - {}", id);
|
||||
try {
|
||||
Optional<StudentGroup> groupOpt = groupRepository.findById(id);
|
||||
if (groupOpt.isEmpty()) {
|
||||
logger.info("Группа с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
ResponseEntity<?> validationError = validateGroupRequest(request);
|
||||
if (validationError != null) {
|
||||
return validationError;
|
||||
}
|
||||
|
||||
Optional<EducationForm> efOpt = educationFormRepository.findById(request.getEducationFormId());
|
||||
if (efOpt.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Форма обучения не найдена"));
|
||||
}
|
||||
Speciality speciality = specialtiesRepository.findById(request.getEffectiveSpecialtyId())
|
||||
.orElse(null);
|
||||
if (speciality == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Специальность не найдена"));
|
||||
}
|
||||
SpecialtyProfile profile = specialtyProfileRepository.findById(request.getSpecialtyProfileId())
|
||||
.orElse(null);
|
||||
if (profile == null || !profile.getSpeciality().getId().equals(speciality.getId())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Профиль обучения не найден для выбранной специальности"));
|
||||
}
|
||||
|
||||
StudentGroup group = groupOpt.get();
|
||||
group.setName(request.getName().trim());
|
||||
group.setGroupSize(request.getGroupSize());
|
||||
group.setEducationForm(efOpt.get());
|
||||
group.setDepartmentId(request.getDepartmentId());
|
||||
group.setYearStartStudy(request.getYearStartStudy());
|
||||
group.setSpeciality(speciality);
|
||||
group.setSpecialtyProfile(profile);
|
||||
groupRepository.save(group);
|
||||
|
||||
logger.info("Группа с ID - {} успешно обновлена", id);
|
||||
return ResponseEntity.ok(mapToResponse(group));
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при обновлении группы с ID - {}: {}", id, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message", "Произошла ошибка при обновлении группы: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public ResponseEntity<?> deleteGroup(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление группы с ID - {}", id);
|
||||
if (!groupRepository.existsById(id)) {
|
||||
StudentGroup group = groupRepository.findById(id).orElse(null);
|
||||
if (group == null) {
|
||||
logger.info("Группа с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
groupRepository.deleteById(id);
|
||||
logger.info("Группа с ID - {} успешно удалена", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Группа удалена"));
|
||||
group.archive("Группа архивирована");
|
||||
groupRepository.save(group);
|
||||
logger.info("Группа с ID - {} успешно архивирована", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Группа архивирована"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public ResponseEntity<?> restoreGroup(@PathVariable Long id) {
|
||||
StudentGroup group = groupRepository.findById(id).orElse(null);
|
||||
if (group == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
group.restore();
|
||||
groupRepository.save(group);
|
||||
return ResponseEntity.ok(mapToResponse(group));
|
||||
}
|
||||
|
||||
private ResponseEntity<?> validateGroupRequest(CreateGroupRequest request) {
|
||||
if (request.getName() == null || request.getName().isBlank()) {
|
||||
return validationError("Название группы обязательно");
|
||||
}
|
||||
if (request.getGroupSize() == null) {
|
||||
return validationError("Численность группы обязательна");
|
||||
}
|
||||
if (request.getEducationFormId() == null) {
|
||||
return validationError("Форма обучения обязательна");
|
||||
}
|
||||
if (request.getDepartmentId() == null || request.getDepartmentId() == 0) {
|
||||
return validationError("ID кафедры обязателен");
|
||||
}
|
||||
if (request.getYearStartStudy() == null || request.getYearStartStudy() == 0) {
|
||||
return validationError("Год начала обучения обязателен");
|
||||
}
|
||||
if (request.getEffectiveSpecialtyId() == null || request.getEffectiveSpecialtyId() == 0) {
|
||||
return validationError("Код специальности обязателен");
|
||||
}
|
||||
if (request.getSpecialtyProfileId() == null || request.getSpecialtyProfileId() == 0) {
|
||||
return validationError("Профиль обучения обязателен");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ResponseEntity<?> validationError(String errorMessage) {
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
private GroupResponse mapToResponse(StudentGroup group) {
|
||||
int course = CourseAndSemesterCalculator.getActualCourse(group.getYearStartStudy());
|
||||
int semester = CourseAndSemesterCalculator.getActualSemester(group.getYearStartStudy());
|
||||
Speciality speciality = group.getSpeciality();
|
||||
SpecialtyProfile profile = group.getSpecialtyProfile();
|
||||
return new GroupResponse(
|
||||
group.getId(),
|
||||
group.getName(),
|
||||
group.getGroupSize(),
|
||||
group.getEducationForm().getId(),
|
||||
group.getEducationForm().getName(),
|
||||
group.getDepartmentId(),
|
||||
group.getYearStartStudy(),
|
||||
course,
|
||||
semester,
|
||||
speciality.getId(),
|
||||
speciality.getSpecialityCode(),
|
||||
speciality.getSpecialityName(),
|
||||
profile.getId(),
|
||||
profile.getName()
|
||||
);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/calendar-assignments")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> getCalendarAssignments(@PathVariable Long id) {
|
||||
if (!groupRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(assignmentRepository.findByGroupIdWithDetails(id).stream()
|
||||
.map(this::toAssignmentDto)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/calendar-assignments")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> saveCalendarAssignment(@PathVariable Long id,
|
||||
@RequestBody GroupCalendarAssignmentDto request) {
|
||||
if (request == null || request.academicYearId() == null || request.calendarId() == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год и календарный график обязательны"));
|
||||
}
|
||||
StudentGroup group = groupRepository.findById(id).orElse(null);
|
||||
if (group == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
AcademicYear academicYear = academicYearRepository.findById(request.academicYearId()).orElse(null);
|
||||
if (academicYear == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год не найден"));
|
||||
}
|
||||
AcademicCalendar calendar = academicCalendarRepository.findByIdWithDetails(request.calendarId()).orElse(null);
|
||||
if (calendar == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Календарный график не найден"));
|
||||
}
|
||||
if (!calendar.getAcademicYear().getId().equals(academicYear.getId())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "График относится к другому учебному году"));
|
||||
}
|
||||
if (!calendar.getSpeciality().getId().equals(group.getSpeciality().getId())
|
||||
|| !calendar.getSpecialtyProfile().getId().equals(group.getSpecialtyProfile().getId())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "График не соответствует специальности и профилю группы"));
|
||||
}
|
||||
if (!calendar.getStudyForm().getId().equals(group.getEducationForm().getId())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "График не соответствует форме обучения группы"));
|
||||
}
|
||||
|
||||
StudentGroupCalendarAssignment assignment = assignmentRepository
|
||||
.findByGroupIdAndAcademicYearIdWithDetails(group.getId(), academicYear.getId())
|
||||
.orElseGet(StudentGroupCalendarAssignment::new);
|
||||
assignment.setStudentGroup(group);
|
||||
assignment.setAcademicYear(academicYear);
|
||||
assignment.setAcademicCalendar(calendar);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toAssignmentDto(assignmentRepository.save(assignment)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/calendar-assignments/{assignmentId}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> deleteCalendarAssignment(@PathVariable Long id, @PathVariable Long assignmentId) {
|
||||
StudentGroupCalendarAssignment assignment = assignmentRepository.findById(assignmentId).orElse(null);
|
||||
if (assignment == null || !assignment.getStudentGroup().getId().equals(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
assignmentRepository.delete(assignment);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Назначение календарного графика удалено"));
|
||||
}
|
||||
|
||||
private GroupCalendarAssignmentDto toAssignmentDto(StudentGroupCalendarAssignment assignment) {
|
||||
return new GroupCalendarAssignmentDto(
|
||||
assignment.getId(),
|
||||
assignment.getStudentGroup().getId(),
|
||||
assignment.getStudentGroup().getName(),
|
||||
assignment.getAcademicYear().getId(),
|
||||
assignment.getAcademicYear().getTitle(),
|
||||
assignment.getAcademicCalendar().getId(),
|
||||
assignment.getAcademicCalendar().getTitle()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.LessonTypeResponse;
|
||||
import com.magistr.app.model.LessonType;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.repository.LessonTypesRepository;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/lesson-types")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public class LessonTypeController {
|
||||
|
||||
private final LessonTypesRepository lessonTypesRepository;
|
||||
|
||||
public LessonTypeController(LessonTypesRepository lessonTypesRepository) {
|
||||
this.lessonTypesRepository = lessonTypesRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<LessonTypeResponse> getAllLessonTypes() {
|
||||
return lessonTypesRepository.findAll().stream()
|
||||
.sorted(Comparator.comparing(LessonType::getLessonType))
|
||||
.map(lessonType -> new LessonTypeResponse(lessonType.getId(), lessonType.getLessonType()))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -1,507 +0,0 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.dto.CreateLessonRequest;
|
||||
import com.magistr.app.dto.LessonResponse;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
import com.magistr.app.utils.DayAndWeekValidator;
|
||||
import com.magistr.app.utils.TypeAndFormatLessonValidator;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users/lessons")
|
||||
public class LessonsController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LessonsController.class);
|
||||
|
||||
private final LessonRepository lessonRepository;
|
||||
private final UserRepository teacherRepository;
|
||||
private final GroupRepository groupRepository;
|
||||
private final SubjectRepository subjectRepository;
|
||||
private final EducationFormRepository educationFormRepository;
|
||||
private final ClassroomRepository classroomRepository;
|
||||
|
||||
public LessonsController(LessonRepository lessonRepository, UserRepository teacherRepository, GroupRepository groupRepository, SubjectRepository subjectRepository, EducationFormRepository educationForm, ClassroomRepository classroomRepository) {
|
||||
this.lessonRepository = lessonRepository;
|
||||
this.teacherRepository = teacherRepository;
|
||||
this.groupRepository = groupRepository;
|
||||
this.subjectRepository = subjectRepository;
|
||||
this.educationFormRepository = educationForm;
|
||||
this.classroomRepository = classroomRepository;
|
||||
}
|
||||
|
||||
//Создание нового занятия
|
||||
@PostMapping("/create")
|
||||
public ResponseEntity<?> createLesson(@RequestBody CreateLessonRequest request) {
|
||||
//Полное логирование входящего запроса
|
||||
logger.info("Получен запрос на создание занятия: teacherId={}, groupId={}, subjectId={}, day={}, week={}, time={}",
|
||||
request.getTeacherId(), request.getGroupId(), request.getSubjectId(), request.getDay(), request.getWeek(), request.getTime());
|
||||
|
||||
//Проверка teacherId
|
||||
if (request.getTeacherId() == null || request.getTeacherId() == 0) {
|
||||
String errorMessage = "ID преподавателя обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка groupId
|
||||
if (request.getGroupId() == null || request.getGroupId() == 0) {
|
||||
String errorMessage = "ID группы обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка subjectId
|
||||
if (request.getSubjectId() == null || request.getSubjectId() == 0) {
|
||||
String errorMessage = "ID предмета обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка lessonFormat
|
||||
if (request.getLessonFormat() == null || request.getLessonFormat().isBlank()) {
|
||||
String errorMessage = "Выбор формата занятия обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
} else if(!TypeAndFormatLessonValidator.isValidFormat(request.getLessonFormat())){
|
||||
String errorMessage = "Некорректный формат занятий. " + TypeAndFormatLessonValidator.getValidFormatsMessage();
|
||||
logger.info("Ошибка валидации формата: '{}' - {}", request.getLessonFormat(), errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка typeLesson
|
||||
if (request.getTypeLesson() == null || request.getTypeLesson().isBlank()) {
|
||||
String errorMessage = "Выбор типа занятия обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
} else if(!TypeAndFormatLessonValidator.isValidType(request.getTypeLesson())){
|
||||
String errorMessage = "Некорректный тип занятия. " + TypeAndFormatLessonValidator.getValidTypesMessage();
|
||||
logger.info("Ошибка валидации типа: '{}' - {}", request.getTypeLesson(), errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка classroomId
|
||||
if (request.getClassroomId() == null || request.getClassroomId() == 0) {
|
||||
String errorMessage = "ID аудитории обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка day
|
||||
if (request.getDay() == null || request.getDay().isBlank()) {
|
||||
String errorMessage = "Выбор дня обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
} else if(!DayAndWeekValidator.isValidDay(request.getDay())){
|
||||
String errorMessage = "Некорректный день недели. " + DayAndWeekValidator.getValidDaysMessage();
|
||||
logger.info("Ошибка валидации дня: '{}' - {}", request.getDay(), errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка week
|
||||
if (request.getWeek() == null || request.getWeek().isBlank()) {
|
||||
String errorMessage = "Выбор недели обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
} else if(!DayAndWeekValidator.isValidWeek(request.getWeek())){
|
||||
String errorMessage = "Некорректная неделя. " + DayAndWeekValidator.getValidWeekMessage();
|
||||
logger.info("Ошибка валидации недели: '{}' - {}", request.getWeek(), errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка time
|
||||
if (request.getTime() == null || request.getTime().isBlank()) {
|
||||
String errorMessage = "Время обязательно";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Сохранение полученных данных и формирование ответа клиенту
|
||||
try {
|
||||
Lesson lesson = new Lesson();
|
||||
lesson.setTeacherId(request.getTeacherId());
|
||||
lesson.setSubjectId(request.getSubjectId());
|
||||
lesson.setGroupId(request.getGroupId());
|
||||
lesson.setLessonFormat(request.getLessonFormat());
|
||||
lesson.setTypeLesson(request.getTypeLesson());
|
||||
lesson.setClassroomId(request.getClassroomId());
|
||||
lesson.setDay(request.getDay());
|
||||
lesson.setWeek(request.getWeek());
|
||||
lesson.setTime(request.getTime());
|
||||
|
||||
Lesson savedLesson = lessonRepository.save(lesson);
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("id", savedLesson.getId());
|
||||
response.put("teacherId", savedLesson.getTeacherId());
|
||||
response.put("groupId", savedLesson.getGroupId());
|
||||
response.put("subjectId", savedLesson.getSubjectId());
|
||||
response.put("formatLesson", savedLesson.getLessonFormat());
|
||||
response.put("typeLesson", savedLesson.getTypeLesson());
|
||||
response.put("classroomId", savedLesson.getClassroomId());
|
||||
response.put("day", savedLesson.getDay());
|
||||
response.put("week", savedLesson.getWeek());
|
||||
response.put("time", savedLesson.getTime());
|
||||
|
||||
logger.info("Занятие успешно создано с ID: {}", savedLesson.getId());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при сохранении занятия: {}", e.getMessage(),e);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message", "Произошла ошибка при создании занятия: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
//Запрос для получения всего списка занятий
|
||||
@GetMapping
|
||||
public List<LessonResponse> getAllLessons() {
|
||||
logger.info("Запрос на получение всех занятий");
|
||||
|
||||
try {
|
||||
List<Lesson> lessons = lessonRepository.findAll();
|
||||
|
||||
List<LessonResponse> response = lessons.stream()
|
||||
.map(lesson -> {
|
||||
String teacherName = teacherRepository.findById(lesson.getTeacherId())
|
||||
.map(User::getUsername)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
StudentGroup group = groupRepository.findById(lesson.getGroupId()).orElse(null);
|
||||
String groupName = groupRepository.findById(lesson.getGroupId())
|
||||
.map(StudentGroup::getName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
String educationFormName = "Неизвестно";
|
||||
if(group != null && group.getEducationForm() != null) {
|
||||
Long educationFormId = group.getEducationForm().getId();
|
||||
educationFormName = educationFormRepository.findById(educationFormId)
|
||||
.map(EducationForm::getName)
|
||||
.orElse("Неизвестно");
|
||||
}
|
||||
|
||||
String subjectName = subjectRepository.findById(lesson.getSubjectId())
|
||||
.map(Subject::getName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
String classroomName = classroomRepository.findById(lesson.getClassroomId())
|
||||
.map(Classroom::getName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
return new LessonResponse(
|
||||
lesson.getId(),
|
||||
teacherName,
|
||||
groupName,
|
||||
classroomName,
|
||||
educationFormName,
|
||||
subjectName,
|
||||
lesson.getTypeLesson(),
|
||||
lesson.getLessonFormat(),
|
||||
lesson.getDay(),
|
||||
lesson.getWeek(),
|
||||
lesson.getTime()
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
|
||||
logger.info("Получено {} занятий", lessons.size());
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при получении списка всех занятий: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
//Запрос на получение всех занятий для конкретного преподавателя
|
||||
@GetMapping("/{teacherId}")
|
||||
public ResponseEntity<?> getLessonsById(@PathVariable Long teacherId) {
|
||||
logger.info("Запрос на получение занятий для преподавателя с ID: {}", teacherId);
|
||||
try {
|
||||
List<Lesson> lessons = lessonRepository.findByTeacherId(teacherId);
|
||||
|
||||
if(lessons.isEmpty()) {
|
||||
logger.info("У преподавателя с ID {} нет занятий", teacherId);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "У преподавателя с ID " + teacherId +" нет занятий.",
|
||||
"lessons", Collections.emptyList()
|
||||
));
|
||||
}
|
||||
|
||||
List<LessonResponse> lessonResponses = lessons.stream()
|
||||
.map(lesson -> {
|
||||
String teacherName = teacherRepository.findById(lesson.getTeacherId())
|
||||
.map(User::getUsername)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
StudentGroup group = groupRepository.findById(lesson.getGroupId()).orElse(null);
|
||||
String groupName = groupRepository.findById(lesson.getGroupId())
|
||||
.map(StudentGroup::getName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
String educationFormName = "Неизвестно";
|
||||
if(group != null && group.getEducationForm() != null) {
|
||||
Long educationFormId = group.getEducationForm().getId();
|
||||
educationFormName = educationFormRepository.findById(educationFormId)
|
||||
.map(EducationForm::getName)
|
||||
.orElse("Неизвестно");
|
||||
}
|
||||
|
||||
String subjectName = subjectRepository.findById(lesson.getSubjectId())
|
||||
.map(Subject::getName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
String classroomName = classroomRepository.findById(lesson.getClassroomId())
|
||||
.map(Classroom::getName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
return new LessonResponse(
|
||||
lesson.getId(),
|
||||
teacherName,
|
||||
groupName,
|
||||
classroomName,
|
||||
educationFormName,
|
||||
subjectName,
|
||||
lesson.getTypeLesson(),
|
||||
lesson.getLessonFormat(),
|
||||
lesson.getDay(),
|
||||
lesson.getWeek(),
|
||||
lesson.getTime()
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
|
||||
logger.info("Найдено {} занятий для преподавателя с ID: {}", lessonResponses.size(), teacherId);
|
||||
return ResponseEntity.ok(lessonResponses);
|
||||
} catch (Exception e ){
|
||||
logger.error("Ошибка при получении занятий для преподавателя с ID {}: {}", teacherId, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message", "Ошибка при поиске занятий: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
//Тестовый запрос на проверку доступности контроллера
|
||||
@GetMapping("/ping")
|
||||
public String ping() {
|
||||
logger.debug("Получен ping запрос");
|
||||
String response = "pong";
|
||||
logger.debug("Ответ на ping: {}", response);
|
||||
return response;
|
||||
}
|
||||
|
||||
//Удаление занятия по его ID
|
||||
@DeleteMapping("/delete/{lessonId}")
|
||||
public ResponseEntity<?> deleteLessonById(@PathVariable Long lessonId){
|
||||
logger.info("Запрос на удаление занятия по ID: {}", lessonId);
|
||||
if(!lessonRepository.existsById(lessonId)) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Занятие не найдено"));
|
||||
}
|
||||
lessonRepository.deleteById(lessonId);
|
||||
logger.info("Занятие с ID - {} успешно удалено", lessonId);
|
||||
return ResponseEntity.ok(Map.of("message", "Занятие успешно удалено"));
|
||||
|
||||
}
|
||||
|
||||
//Обновление занятия по его ID
|
||||
@PutMapping("/update/{lessonId}")
|
||||
public ResponseEntity<?> updateLessonById(@PathVariable Long lessonId, @RequestBody CreateLessonRequest request) {
|
||||
logger.info("Получен запрос на обновление занятия с ID - {}", lessonId);
|
||||
logger.info("Данные для обновления: teacherId={}, groupId={}, subjectId={}, lessonFormat={}, typeLesson={}, classroomId={}, day={}, week={}, time={}",
|
||||
request.getTeacherId(), request.getGroupId(), request.getSubjectId(), request.getLessonFormat(), request.getTypeLesson(), request.getClassroomId(),
|
||||
request.getDay(), request.getWeek(), request.getTime());
|
||||
|
||||
try {
|
||||
//Проверка на наличие записи
|
||||
Lesson existingLesson = lessonRepository.findById(lessonId).orElse(null);
|
||||
|
||||
if(existingLesson == null) {
|
||||
String errorMessage = "Занятие с ID " + lessonId + " не найдено";
|
||||
logger.info("Ошибка: {}", errorMessage);
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
boolean hasChanges = false;
|
||||
Map<String, Object> changes = new LinkedHashMap<>();
|
||||
|
||||
//Проверка и обновление teacherId, если он передан и отличается
|
||||
if(request.getTeacherId() != null) {
|
||||
if(!request.getTeacherId().equals(existingLesson.getTeacherId())) {
|
||||
if(request.getTeacherId() == 0) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "ID преподавателя не может быть равен 0"));
|
||||
}
|
||||
existingLesson.setTeacherId(request.getTeacherId());
|
||||
changes.put("teacherId", request.getTeacherId());
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Проверка и обновление groupId, если он передан и отличается
|
||||
if(request.getGroupId() != null) {
|
||||
if(!request.getGroupId().equals(existingLesson.getGroupId())) {
|
||||
if(request.getGroupId() == 0) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "ID группы не может быть равен 0"));
|
||||
}
|
||||
existingLesson.setGroupId(request.getGroupId());
|
||||
changes.put("groupId", request.getGroupId());
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Проверка и обновление subjectId, если он передан и отличается
|
||||
if(request.getSubjectId() != null) {
|
||||
if(!request.getSubjectId().equals(existingLesson.getSubjectId())) {
|
||||
if(request.getSubjectId() == 0) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "ID дисциплины не может быть равен 0"));
|
||||
}
|
||||
existingLesson.setSubjectId(request.getSubjectId());
|
||||
changes.put("subjectId", request.getSubjectId());
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Проверка и обновление lessonFormat, если он передан и отличается
|
||||
if(request.getLessonFormat() != null) {
|
||||
if(!request.getLessonFormat().equals(existingLesson.getLessonFormat())) {
|
||||
if(request.getLessonFormat().isBlank()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "Формат занятия не может быть пустым"));
|
||||
}
|
||||
if(!TypeAndFormatLessonValidator.isValidFormat(request.getLessonFormat())) {
|
||||
String errorMessage = "Некорректный формат занятий. " + TypeAndFormatLessonValidator.getValidFormatsMessage();
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
existingLesson.setLessonFormat(request.getLessonFormat());
|
||||
changes.put("lessonFormat", request.getLessonFormat());
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Проверка и обновление typeLesson, если он передан и отличается
|
||||
if(request.getTypeLesson() != null) {
|
||||
if(!request.getTypeLesson().equals(existingLesson.getTypeLesson())) {
|
||||
if(request.getTypeLesson().isBlank()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "Тип занятия не может быть пустым"));
|
||||
}
|
||||
if(!TypeAndFormatLessonValidator.isValidType(request.getTypeLesson())) {
|
||||
String errorMessage = "Некорректный тип занятий. " + TypeAndFormatLessonValidator.getValidTypesMessage();
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
existingLesson.setLessonFormat(request.getTypeLesson());
|
||||
changes.put("typeLesson", request.getTypeLesson());
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Проверка и обновление classroomId, если он передан и отличается
|
||||
if(request.getClassroomId() != null) {
|
||||
if(!request.getClassroomId().equals(existingLesson.getClassroomId())) {
|
||||
if(request.getClassroomId() == 0) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "ID аудитории не можеть быть равен 0"));
|
||||
}
|
||||
existingLesson.setClassroomId(request.getClassroomId());
|
||||
changes.put("classroomId", request.getClassroomId());
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Проверка и обновление day, если он передан и отличается
|
||||
if(request.getDay() != null){
|
||||
if(!request.getDay().equals(existingLesson.getDay())) {
|
||||
if(request.getDay().isBlank()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "Поле \"День\" не может быть пустым"));
|
||||
}
|
||||
if(!DayAndWeekValidator.isValidDay(request.getDay())) {
|
||||
String errorMessage = "Некорректный день. " + DayAndWeekValidator.getValidDaysMessage();
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", errorMessage));
|
||||
}
|
||||
existingLesson.setDay(request.getDay());
|
||||
changes.put("day", request.getDay());
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Проверка и обновление week, если он передан и отличается
|
||||
if(request.getWeek() != null) {
|
||||
if(!request.getWeek().equals(existingLesson.getWeek())) {
|
||||
if (request.getWeek().isBlank()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "Поле \"Неделя\" не может быть пустым"));
|
||||
}
|
||||
if (!DayAndWeekValidator.isValidWeek(request.getWeek())) {
|
||||
String errorMessage = "Некорректная неделя. " + DayAndWeekValidator.getValidWeekMessage();
|
||||
return ResponseEntity.badRequest()
|
||||
.body((Map.of("message", errorMessage)));
|
||||
}
|
||||
existingLesson.setWeek(request.getWeek());
|
||||
changes.put("week", request.getWeek());
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
//Проверка и обновление time, если он передан и отличается
|
||||
if(request.getTime() != null) {
|
||||
if(!request.getTime().equals(existingLesson.getTime())) {
|
||||
if(request.getTime().isBlank()){
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "Поле \"Время\" не может быть пустым"));
|
||||
}
|
||||
existingLesson.setTime(request.getTime());
|
||||
changes.put("time", request.getTime());
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!hasChanges) {
|
||||
logger.info("Обновление не требуется - все полня идентичны существующим для занятия с ID: {}", lessonId);
|
||||
|
||||
Map<String, Object> response = buildResponse(existingLesson);
|
||||
response.put("message", "Изменений не обнаружено");
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
Lesson updatedLesson = lessonRepository.save(existingLesson);
|
||||
|
||||
Map<String, Object> response = buildResponse(updatedLesson);
|
||||
response.put("updatedFields", changes);
|
||||
response.put("message", "Занятие успешно обновлено");
|
||||
|
||||
logger.info("Занятие с ID - {} успешно обновлено. Изменения: {}", lessonId, changes);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при обновлении занятия с ID {}: {}", lessonId, e.getMessage(),e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message", "Произошла ошибка при обновлении занятия: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> buildResponse(Lesson lesson) {
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("id", lesson.getId());
|
||||
response.put("teacherId", lesson.getTeacherId());
|
||||
response.put("groupId", lesson.getGroupId());
|
||||
response.put("subjectId", lesson.getSubjectId());
|
||||
response.put("LessonFormat", lesson.getLessonFormat());
|
||||
response.put("typeLesson", lesson.getTypeLesson());
|
||||
response.put("classroomId", lesson.getClassroomId());
|
||||
response.put("day", lesson.getDay());
|
||||
response.put("week", lesson.getWeek());
|
||||
response.put("time", lesson.getTime());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.RenderedLessonDto;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.service.ScheduleQueryService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/schedule")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER, Role.TEACHER, Role.STUDENT})
|
||||
public class ScheduleController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScheduleController.class);
|
||||
|
||||
private final ScheduleQueryService scheduleQueryService;
|
||||
|
||||
public ScheduleController(ScheduleQueryService scheduleQueryService) {
|
||||
this.scheduleQueryService = scheduleQueryService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> getSchedule(
|
||||
@RequestParam(required = false) Long groupId,
|
||||
@RequestParam(required = false) Long teacherId,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate
|
||||
) {
|
||||
logger.info("Запрос динамического расписания: groupId={}, teacherId={}, startDate={}, endDate={}",
|
||||
groupId, teacherId, startDate, endDate);
|
||||
|
||||
if ((groupId == null && teacherId == null) || (groupId != null && teacherId != null)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "Передайте ровно один параметр: groupId или teacherId"));
|
||||
}
|
||||
|
||||
try {
|
||||
List<RenderedLessonDto> lessons = scheduleQueryService.search(
|
||||
groupId,
|
||||
teacherId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
startDate,
|
||||
endDate
|
||||
);
|
||||
return ResponseEntity.ok(lessons);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.info("Ошибка запроса динамического расписания: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка генерации динамического расписания: {}", e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("message", "Произошла ошибка при генерации расписания"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.dto.CreateScheduleDataRequest;
|
||||
import com.magistr.app.dto.ScheduleResponse;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
import com.magistr.app.utils.CourseAndSemesterCalculator;
|
||||
import com.magistr.app.utils.SemesterTypeValidator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/department/schedule")
|
||||
public class ScheduleDataController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScheduleDataController.class);
|
||||
|
||||
private final ScheduleDataRepository scheduleDataRepository;
|
||||
private final GroupRepository groupRepository;
|
||||
private final SpecialtiesRepository specialtiesRepository;
|
||||
private final SubjectRepository subjectRepository;
|
||||
private final LessonTypesRepository lessonTypesRepository;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public ScheduleDataController(ScheduleDataRepository scheduleDataRepository, GroupRepository groupRepository, SpecialtiesRepository specialtiesRepository, SubjectRepository subjectRepository, LessonTypesRepository lessonTypesRepository, UserRepository userRepository) {
|
||||
this.scheduleDataRepository = scheduleDataRepository;
|
||||
this.groupRepository = groupRepository;
|
||||
this.specialtiesRepository = specialtiesRepository;
|
||||
this.subjectRepository = subjectRepository;
|
||||
this.lessonTypesRepository = lessonTypesRepository;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/allList")
|
||||
public List<ScheduleData> getAllScheduleDataList() {
|
||||
logger.info("Получен запрос на получение списка данных расписаний");
|
||||
try {
|
||||
List<ScheduleData> scheduleData = scheduleDataRepository.findAll();
|
||||
List<ScheduleData> response = scheduleData.stream()
|
||||
.map(s -> new ScheduleData(
|
||||
s.getId(),
|
||||
s.getDepartmentId(),
|
||||
s.getGroupId(),
|
||||
s.getSubjectsId(),
|
||||
s.getLessonTypeId(),
|
||||
s.getNumberOfHours(),
|
||||
s.getDivision(),
|
||||
s.getTeacherId(),
|
||||
s.getSemesterType(),
|
||||
s.getPeriod()
|
||||
))
|
||||
.toList();
|
||||
logger.info("Получено {} записей", response.size());
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при получении списка данных расписаний: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> getSingleScheduleData(
|
||||
@RequestParam Long departmentId,
|
||||
@RequestParam SemesterType semesterType,
|
||||
@RequestParam String period
|
||||
) {
|
||||
logger.info("Получен запрос на получение списка данных расписания по конкретным данным: departmentId = {}, semester = {}, period = {}",
|
||||
departmentId, semesterType, period);
|
||||
try {
|
||||
List<ScheduleData> scheduleData = scheduleDataRepository.findByDepartmentIdAndSemesterTypeAndPeriod(departmentId, semesterType, period );
|
||||
|
||||
if(scheduleData.isEmpty()){
|
||||
logger.info("По параметрам: departmentId = {}, semester = {}, period = {} не найдено записей", departmentId, semesterType, period);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "Записей не найдено"
|
||||
));
|
||||
}
|
||||
|
||||
List<ScheduleResponse> response = scheduleData.stream()
|
||||
.map( s -> {
|
||||
String groupName = groupRepository.findById(s.getGroupId())
|
||||
.map(StudentGroup::getName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
int groupSemester = 0;
|
||||
int groupCourse = 0;
|
||||
String specialityCode = "Неизвестно";
|
||||
|
||||
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) {
|
||||
Long specialityId = group.getSpecialityCode();
|
||||
specialityCode = specialtiesRepository.findById(specialityId).
|
||||
map(Speciality::getSpecialityCode)
|
||||
.orElse("Неизвестно");
|
||||
}
|
||||
|
||||
String subjectName = subjectRepository.findById(s.getSubjectsId())
|
||||
.map(Subject::getName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
String lessonType = lessonTypesRepository.findById(s.getLessonTypeId())
|
||||
.map(LessonType::getLessonType)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
String teacherName = userRepository.findById(s.getTeacherId())
|
||||
.map(User::getFullName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
String teacherjobTitle = userRepository.findById(s.getTeacherId())
|
||||
.map(User::getJobTitle)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
return new ScheduleResponse(
|
||||
s.getId(),
|
||||
s.getDepartmentId(),
|
||||
specialityCode,
|
||||
groupName,
|
||||
groupCourse,
|
||||
groupSemester,
|
||||
subjectName,
|
||||
lessonType,
|
||||
s.getNumberOfHours(),
|
||||
s.getDivision(),
|
||||
teacherName,
|
||||
teacherjobTitle,
|
||||
s.getSemesterType(),
|
||||
s.getPeriod());
|
||||
}
|
||||
)
|
||||
.toList();
|
||||
logger.info("Получено {} записей для кафедры с ID - {}", response.size(), departmentId);
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при получении списка данных расписаний для кафедры с ID - {}, semester - {}, period - {}: {}", departmentId, semesterType, period, e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Доделать проверки получаемых полей!!!
|
||||
@PostMapping("/create")
|
||||
public ResponseEntity<?> createScheduleData(@RequestBody CreateScheduleDataRequest request) {
|
||||
logger.info("Получен запрос на создание записи данных для расписаний: departmentId={}, 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());
|
||||
try {
|
||||
if (request.getDepartmentId() == null || request.getDepartmentId() == 0) {
|
||||
String errorMessage = "ID кафедры обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
} else if(!scheduleDataRepository.existsById(request.getDepartmentId())) {
|
||||
String errorMessage = "Кафедра не найдена";
|
||||
logger.info("Кафедра не найдена");
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
if (request.getGroupId() == null || request.getGroupId() == 0) {
|
||||
String errorMessage = "ID группы обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
if (request.getSubjectsId() == null || request.getSubjectsId() == 0) {
|
||||
String errorMessage = "ID дисциплины обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
if (request.getLessonTypeId() == null || request.getLessonTypeId() == 0) {
|
||||
String errorMessage = "ID типа занятия обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
if (request.getNumberOfHours() == null) {
|
||||
request.setNumberOfHours(0L);
|
||||
}
|
||||
|
||||
if (request.getTeacherId() == null || request.getTeacherId() == 0) {
|
||||
String errorMessage = "ID преподавателя обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
if (request.getSemesterType() == null) {
|
||||
String errorMessage = "Семестр обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
} else if (!SemesterTypeValidator.isValidTypeSemester(request.getSemesterType().toString())) {
|
||||
String errorMessage = "Некорректный формат семестра. Допустимые форматы: " + SemesterTypeValidator.getValidTypes();
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
if (request.getPeriod() == null || request.getPeriod().isBlank()) {
|
||||
String errorMessage = "Период обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
boolean existsRecord = scheduleDataRepository.existsByDepartmentIdAndGroupIdAndSubjectsIdAndLessonTypeIdAndNumberOfHoursAndDivisionAndTeacherIdAndSemesterTypeAndPeriod(
|
||||
request.getDepartmentId(),
|
||||
request.getGroupId(),
|
||||
request.getSubjectsId(),
|
||||
request.getLessonTypeId(),
|
||||
request.getNumberOfHours(),
|
||||
request.getDivision(),
|
||||
request.getTeacherId(),
|
||||
request.getSemesterType(),
|
||||
request.getPeriod()
|
||||
);
|
||||
|
||||
if(existsRecord) {
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT)
|
||||
.body(Map.of("message", "Такая запись уже существует"));
|
||||
}
|
||||
|
||||
ScheduleData scheduleData = new ScheduleData();
|
||||
scheduleData.setDepartmentId(request.getDepartmentId());
|
||||
scheduleData.setGroupId(request.getGroupId());
|
||||
scheduleData.setSubjectsId(request.getSubjectsId());
|
||||
scheduleData.setLessonTypeId(request.getLessonTypeId());
|
||||
scheduleData.setNumberOfHours(request.getNumberOfHours());
|
||||
scheduleData.setDivision(request.getDivision());
|
||||
scheduleData.setTeacherId(request.getTeacherId());
|
||||
scheduleData.setSemesterType(request.getSemesterType());
|
||||
scheduleData.setPeriod(request.getPeriod());
|
||||
|
||||
ScheduleData savedSchedule = scheduleDataRepository.save(scheduleData);
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("id", savedSchedule.getId());
|
||||
response.put("departmentId", savedSchedule.getDepartmentId());
|
||||
response.put("groupId", savedSchedule.getGroupId());
|
||||
response.put("subjectId", savedSchedule.getSubjectsId());
|
||||
response.put("lessonTypeId", savedSchedule.getLessonTypeId());
|
||||
response.put("numberOfHours", savedSchedule.getNumberOfHours());
|
||||
response.put("isDivision", savedSchedule.getDivision());
|
||||
response.put("teacherId", savedSchedule.getTeacherId());
|
||||
response.put("semesterType", savedSchedule.getSemesterType());
|
||||
response.put("period", savedSchedule.getPeriod());
|
||||
|
||||
logger.info("Запись успешно создана с ID: {}", savedSchedule.getId());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (org.springframework.dao.DataIntegrityViolationException e) {
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT)
|
||||
.body(Map.of("message", "Такая запись уже существует"));
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при создании записи: {}", e.getMessage(), e);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message", "Произошла ошибка при создании записи: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteById(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление записи с ID: {}", id);
|
||||
if(!scheduleDataRepository.existsById(id)) {
|
||||
logger.info("Запись с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
scheduleDataRepository.deleteById(id);
|
||||
logger.info("Запись с ID - {} успешно удалена", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Запись удалена"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.ScheduleOverrideDto;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
import com.magistr.app.service.ScheduleGeneratorService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/edu-office/schedule/overrides")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class ScheduleOverrideController {
|
||||
|
||||
private final ScheduleOverrideRepository scheduleOverrideRepository;
|
||||
private final ScheduleRuleSlotRepository scheduleRuleSlotRepository;
|
||||
private final TimeSlotRepository timeSlotRepository;
|
||||
private final ClassroomRepository classroomRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
|
||||
public ScheduleOverrideController(ScheduleOverrideRepository scheduleOverrideRepository,
|
||||
ScheduleRuleSlotRepository scheduleRuleSlotRepository,
|
||||
TimeSlotRepository timeSlotRepository,
|
||||
ClassroomRepository classroomRepository,
|
||||
UserRepository userRepository,
|
||||
ScheduleGeneratorService scheduleGeneratorService) {
|
||||
this.scheduleOverrideRepository = scheduleOverrideRepository;
|
||||
this.scheduleRuleSlotRepository = scheduleRuleSlotRepository;
|
||||
this.timeSlotRepository = timeSlotRepository;
|
||||
this.classroomRepository = classroomRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<ScheduleOverrideDto> getAll() {
|
||||
return scheduleOverrideRepository.findAll().stream()
|
||||
.map(this::toDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> create(@RequestBody ScheduleOverrideDto request) {
|
||||
return save(null, request);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> update(@PathVariable Long id, @RequestBody ScheduleOverrideDto request) {
|
||||
if (!scheduleOverrideRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return save(id, request);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> delete(@PathVariable Long id) {
|
||||
if (!scheduleOverrideRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
scheduleOverrideRepository.deleteById(id);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Точечное изменение расписания удалено"));
|
||||
}
|
||||
|
||||
private ResponseEntity<?> save(Long id, ScheduleOverrideDto request) {
|
||||
String validationError = validate(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
ScheduleOverride override = id == null
|
||||
? scheduleOverrideRepository
|
||||
.findByBaseRuleSlotIdAndLessonDate(request.baseRuleSlotId(), request.lessonDate())
|
||||
.orElseGet(ScheduleOverride::new)
|
||||
: scheduleOverrideRepository.findById(id).orElseGet(ScheduleOverride::new);
|
||||
|
||||
override.setBaseRuleSlot(scheduleRuleSlotRepository.findById(request.baseRuleSlotId()).orElseThrow());
|
||||
override.setLessonDate(request.lessonDate());
|
||||
override.setAction(request.action());
|
||||
override.setNewTimeSlot(request.newTimeSlotId() == null ? null : timeSlotRepository.findById(request.newTimeSlotId()).orElse(null));
|
||||
override.setNewClassroom(request.newClassroomId() == null ? null : classroomRepository.findById(request.newClassroomId()).orElse(null));
|
||||
override.setNewTeacher(request.newTeacherId() == null ? null : userRepository.findById(request.newTeacherId()).orElse(null));
|
||||
override.setNewLessonFormat(clean(request.newLessonFormat()));
|
||||
override.setComment(clean(request.comment()));
|
||||
override.setCreatedBy(AuthContext.currentUserId());
|
||||
|
||||
ScheduleOverride saved = scheduleOverrideRepository.save(override);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toDto(saved));
|
||||
}
|
||||
|
||||
private String validate(ScheduleOverrideDto request) {
|
||||
if (request == null) {
|
||||
return "Передайте данные изменения расписания";
|
||||
}
|
||||
if (request.baseRuleSlotId() == null || scheduleRuleSlotRepository.findById(request.baseRuleSlotId()).isEmpty()) {
|
||||
return "Базовый слот расписания не найден";
|
||||
}
|
||||
if (request.lessonDate() == null) {
|
||||
return "Дата пары обязательна";
|
||||
}
|
||||
if (request.action() == null || !List.of("MOVE", "CANCEL", "REPLACE").contains(request.action())) {
|
||||
return "Недопустимое действие изменения расписания";
|
||||
}
|
||||
if (request.newClassroomId() != null) {
|
||||
Classroom classroom = classroomRepository.findById(request.newClassroomId()).orElse(null);
|
||||
if (classroom == null || classroom.isArchivedRecord() || Boolean.FALSE.equals(classroom.getIsAvailable())) {
|
||||
return "Активная аудитория не найдена";
|
||||
}
|
||||
}
|
||||
if (request.newTeacherId() != null) {
|
||||
User teacher = userRepository.findById(request.newTeacherId()).orElse(null);
|
||||
if (teacher == null || teacher.getRole() != Role.TEACHER || teacher.isArchivedRecord()) {
|
||||
return "Активный преподаватель не найден";
|
||||
}
|
||||
}
|
||||
if (request.newTimeSlotId() != null && timeSlotRepository.findById(request.newTimeSlotId()).isEmpty()) {
|
||||
return "Временной слот не найден";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ScheduleOverrideDto toDto(ScheduleOverride override) {
|
||||
return new ScheduleOverrideDto(
|
||||
override.getId(),
|
||||
override.getBaseRuleSlot().getId(),
|
||||
override.getLessonDate(),
|
||||
override.getAction(),
|
||||
override.getNewTimeSlot() == null ? null : override.getNewTimeSlot().getId(),
|
||||
override.getNewClassroom() == null ? null : override.getNewClassroom().getId(),
|
||||
override.getNewTeacher() == null ? null : override.getNewTeacher().getId(),
|
||||
override.getNewLessonFormat(),
|
||||
override.getComment(),
|
||||
override.getCreatedBy(),
|
||||
override.getCreatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private String clean(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.ScheduleRuleDto;
|
||||
import com.magistr.app.dto.ScheduleRuleSlotDto;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
import com.magistr.app.service.ScheduleGeneratorService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/schedule-rules")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class ScheduleRuleAdminController {
|
||||
|
||||
private static final String APPLY_MODE_DEFAULT = "DEFAULT";
|
||||
|
||||
private final ScheduleRuleRepository scheduleRuleRepository;
|
||||
private final SubjectRepository subjectRepository;
|
||||
private final SemesterRepository semesterRepository;
|
||||
private final GroupRepository groupRepository;
|
||||
private final TimeSlotRepository timeSlotRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final ClassroomRepository classroomRepository;
|
||||
private final LessonTypesRepository lessonTypesRepository;
|
||||
private final SubgroupRepository subgroupRepository;
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
|
||||
public ScheduleRuleAdminController(ScheduleRuleRepository scheduleRuleRepository,
|
||||
SubjectRepository subjectRepository,
|
||||
SemesterRepository semesterRepository,
|
||||
GroupRepository groupRepository,
|
||||
TimeSlotRepository timeSlotRepository,
|
||||
UserRepository userRepository,
|
||||
ClassroomRepository classroomRepository,
|
||||
LessonTypesRepository lessonTypesRepository,
|
||||
SubgroupRepository subgroupRepository,
|
||||
ScheduleGeneratorService scheduleGeneratorService) {
|
||||
this.scheduleRuleRepository = scheduleRuleRepository;
|
||||
this.subjectRepository = subjectRepository;
|
||||
this.semesterRepository = semesterRepository;
|
||||
this.groupRepository = groupRepository;
|
||||
this.timeSlotRepository = timeSlotRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.classroomRepository = classroomRepository;
|
||||
this.lessonTypesRepository = lessonTypesRepository;
|
||||
this.subgroupRepository = subgroupRepository;
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<ScheduleRuleDto> getAll(@RequestParam(required = false) Long semesterId,
|
||||
@RequestParam(required = false) Long groupId) {
|
||||
return scheduleRuleRepository.findAllWithDetails().stream()
|
||||
.filter(rule -> semesterId == null || Objects.equals(rule.getSemester().getId(), semesterId))
|
||||
.filter(rule -> groupId == null || rule.getGroups().stream().anyMatch(group -> Objects.equals(group.getId(), groupId)))
|
||||
.map(this::toDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<?> getById(@PathVariable Long id) {
|
||||
return scheduleRuleRepository.findByIdWithDetails(id)
|
||||
.<ResponseEntity<?>>map(rule -> ResponseEntity.ok(toDto(rule)))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Transactional
|
||||
public ResponseEntity<?> create(@RequestBody ScheduleRuleDto request) {
|
||||
String validationError = validateRule(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
try {
|
||||
ScheduleRule rule = new ScheduleRule();
|
||||
applyRule(rule, request);
|
||||
ScheduleRule saved = scheduleRuleRepository.save(rule);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toDto(saved));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Transactional
|
||||
public ResponseEntity<?> update(@PathVariable Long id, @RequestBody ScheduleRuleDto request) {
|
||||
ScheduleRule rule = scheduleRuleRepository.findByIdWithDetails(id).orElse(null);
|
||||
if (rule == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
String validationError = validateRule(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
try {
|
||||
applyRule(rule, request);
|
||||
ScheduleRule saved = scheduleRuleRepository.save(rule);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toDto(saved));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> delete(@PathVariable Long id) {
|
||||
ScheduleRule rule = scheduleRuleRepository.findById(id).orElse(null);
|
||||
if (rule == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
rule.setStatus(LifecycleEntity.STATUS_ARCHIVED);
|
||||
scheduleRuleRepository.save(rule);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Правило расписания архивировано"));
|
||||
}
|
||||
|
||||
private String validateRule(ScheduleRuleDto request) {
|
||||
if (request.subjectId() == null) {
|
||||
return "Дисциплина обязательна";
|
||||
}
|
||||
if (request.semesterId() == null) {
|
||||
return "Семестр обязателен";
|
||||
}
|
||||
String hoursValidationError = validateTypeHours(request);
|
||||
if (hoursValidationError != null) {
|
||||
return hoursValidationError;
|
||||
}
|
||||
if (request.groupIds() == null || request.groupIds().isEmpty()) {
|
||||
return "Нужно выбрать хотя бы одну группу";
|
||||
}
|
||||
if (request.slots() == null || request.slots().isEmpty()) {
|
||||
return "Добавьте хотя бы один слот занятия";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyRule(ScheduleRule rule, ScheduleRuleDto request) {
|
||||
Subject subject = subjectRepository.findById(request.subjectId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Дисциплина не найдена"));
|
||||
if (subject.isArchivedRecord()) {
|
||||
throw new IllegalArgumentException("Архивную дисциплину нельзя использовать в новых правилах");
|
||||
}
|
||||
Semester semester = semesterRepository.findById(request.semesterId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Семестр не найден"));
|
||||
List<StudentGroup> groups = groupRepository.findAllById(request.groupIds());
|
||||
if (groups.size() != new HashSet<>(request.groupIds()).size()) {
|
||||
throw new IllegalArgumentException("Одна или несколько групп не найдены");
|
||||
}
|
||||
if (groups.stream().anyMatch(StudentGroup::isArchivedRecord)) {
|
||||
throw new IllegalArgumentException("Архивные группы нельзя использовать в новых правилах");
|
||||
}
|
||||
|
||||
rule.setSubject(subject);
|
||||
rule.setSemester(semester);
|
||||
rule.setLectureAcademicHours(request.lectureAcademicHours());
|
||||
rule.setLaboratoryAcademicHours(request.laboratoryAcademicHours());
|
||||
rule.setPracticeAcademicHours(request.practiceAcademicHours());
|
||||
rule.setLectureStartWeek(request.lectureStartWeek());
|
||||
rule.setLaboratoryStartWeek(request.laboratoryStartWeek());
|
||||
rule.setPracticeStartWeek(request.practiceStartWeek());
|
||||
rule.getGroups().clear();
|
||||
rule.getGroups().addAll(groups);
|
||||
|
||||
List<ScheduleRuleSlot> slots = new ArrayList<>();
|
||||
for (ScheduleRuleSlotDto slotDto : request.slots()) {
|
||||
slots.add(buildSlot(rule, slotDto));
|
||||
}
|
||||
validateTypeCoverage(rule, slots);
|
||||
|
||||
rule.getSlots().clear();
|
||||
rule.getSlots().addAll(slots);
|
||||
}
|
||||
|
||||
private ScheduleRuleSlot buildSlot(ScheduleRule rule, ScheduleRuleSlotDto slotDto) {
|
||||
if (slotDto.dayOfWeek() == null || slotDto.dayOfWeek() < 1 || slotDto.dayOfWeek() > 7) {
|
||||
throw new IllegalArgumentException("День недели должен быть от 1 до 7");
|
||||
}
|
||||
if (slotDto.parity() == null) {
|
||||
throw new IllegalArgumentException("Чётность недели обязательна");
|
||||
}
|
||||
TimeSlot timeSlot = timeSlotRepository.findById(slotDto.timeSlotId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Временной слот не найден"));
|
||||
if (timeSlot.getTimeSlotScope() == null
|
||||
|| !APPLY_MODE_DEFAULT.equals(timeSlot.getTimeSlotScope().getApplyMode())) {
|
||||
throw new IllegalArgumentException("В правилах расписания можно выбирать только базовые временные слоты");
|
||||
}
|
||||
User teacher = userRepository.findById(slotDto.teacherId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Преподаватель не найден"));
|
||||
if (teacher.isArchivedRecord()) {
|
||||
throw new IllegalArgumentException("Архивного преподавателя нельзя назначать в расписание");
|
||||
}
|
||||
Classroom classroom = classroomRepository.findById(slotDto.classroomId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Аудитория не найдена"));
|
||||
if (classroom.isArchivedRecord() || Boolean.FALSE.equals(classroom.getIsAvailable())) {
|
||||
throw new IllegalArgumentException("Аудитория недоступна для новых назначений");
|
||||
}
|
||||
LessonType lessonType = lessonTypesRepository.findById(slotDto.lessonTypeId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Тип занятия не найден"));
|
||||
ScheduleLessonCategory category = ScheduleLessonCategory.fromLessonType(lessonType);
|
||||
Set<Subgroup> subgroups = resolveSubgroups(rule, slotDto, category);
|
||||
if (slotDto.lessonFormat() == null || slotDto.lessonFormat().isBlank()) {
|
||||
throw new IllegalArgumentException("Формат занятия обязателен");
|
||||
}
|
||||
|
||||
ScheduleRuleSlot slot = new ScheduleRuleSlot();
|
||||
slot.setScheduleRule(rule);
|
||||
slot.setDayOfWeek(slotDto.dayOfWeek());
|
||||
slot.setParity(slotDto.parity());
|
||||
slot.setTimeSlot(timeSlot);
|
||||
slot.setSubgroups(subgroups);
|
||||
slot.setTeacher(teacher);
|
||||
slot.setClassroom(classroom);
|
||||
slot.setLessonType(lessonType);
|
||||
slot.setLessonFormat(slotDto.lessonFormat().trim());
|
||||
return slot;
|
||||
}
|
||||
|
||||
private Set<Subgroup> resolveSubgroups(ScheduleRule rule, ScheduleRuleSlotDto slotDto, ScheduleLessonCategory category) {
|
||||
LinkedHashSet<Long> subgroupIds = new LinkedHashSet<>();
|
||||
if (slotDto.subgroupIds() != null) {
|
||||
slotDto.subgroupIds().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(subgroupIds::add);
|
||||
}
|
||||
if (slotDto.subgroupId() != null) {
|
||||
subgroupIds.add(slotDto.subgroupId());
|
||||
}
|
||||
if (subgroupIds.isEmpty()) {
|
||||
return new LinkedHashSet<>();
|
||||
}
|
||||
|
||||
if (category != ScheduleLessonCategory.LABORATORY) {
|
||||
throw new IllegalArgumentException("Подгруппы можно выбирать только для лабораторных занятий");
|
||||
}
|
||||
|
||||
Set<Long> ruleGroupIds = rule.getGroups().stream()
|
||||
.map(StudentGroup::getId)
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
Set<Long> subgroupGroupIds = new HashSet<>();
|
||||
LinkedHashSet<Subgroup> subgroups = new LinkedHashSet<>();
|
||||
|
||||
for (Long subgroupId : subgroupIds) {
|
||||
Subgroup subgroup = subgroupRepository.findById(subgroupId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Подгруппа не найдена"));
|
||||
Long subgroupGroupId = subgroup.getStudentGroup().getId();
|
||||
if (!ruleGroupIds.contains(subgroupGroupId)) {
|
||||
throw new IllegalArgumentException("Подгруппа должна относиться к одной из групп правила");
|
||||
}
|
||||
if (!subgroupGroupIds.add(subgroupGroupId)) {
|
||||
throw new IllegalArgumentException("В одном слоте можно выбрать не больше одной подгруппы каждой группы");
|
||||
}
|
||||
subgroups.add(subgroup);
|
||||
}
|
||||
|
||||
return subgroups;
|
||||
}
|
||||
|
||||
private String validateTypeHours(ScheduleRuleDto request) {
|
||||
if (isMissing(request.lectureAcademicHours())
|
||||
|| isMissing(request.laboratoryAcademicHours())
|
||||
|| isMissing(request.practiceAcademicHours())) {
|
||||
return "Укажите количество часов для всех типов занятий";
|
||||
}
|
||||
if (isNegative(request.lectureAcademicHours())
|
||||
|| isNegative(request.laboratoryAcademicHours())
|
||||
|| isNegative(request.practiceAcademicHours())) {
|
||||
return "Количество часов по типам занятий не может быть отрицательным";
|
||||
}
|
||||
if (isInvalidStartWeek(request.lectureStartWeek())
|
||||
|| isInvalidStartWeek(request.laboratoryStartWeek())
|
||||
|| isInvalidStartWeek(request.practiceStartWeek())) {
|
||||
return "Неделя начала занятий должна быть больше нуля";
|
||||
}
|
||||
int totalHours = valueOrZero(request.lectureAcademicHours())
|
||||
+ valueOrZero(request.laboratoryAcademicHours())
|
||||
+ valueOrZero(request.practiceAcademicHours());
|
||||
if (totalHours <= 0) {
|
||||
return "Укажите часы хотя бы для одного типа занятий";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void validateTypeCoverage(ScheduleRule rule, List<ScheduleRuleSlot> slots) {
|
||||
EnumSet<ScheduleLessonCategory> categoriesWithSlots = EnumSet.noneOf(ScheduleLessonCategory.class);
|
||||
for (ScheduleRuleSlot slot : slots) {
|
||||
categoriesWithSlots.add(ScheduleLessonCategory.fromLessonType(slot.getLessonType()));
|
||||
}
|
||||
|
||||
for (ScheduleLessonCategory category : ScheduleLessonCategory.values()) {
|
||||
int hours = rule.academicHoursFor(category);
|
||||
boolean hasSlots = categoriesWithSlots.contains(category);
|
||||
if (hours > 0 && !hasSlots) {
|
||||
throw new IllegalArgumentException("Для типа \"" + category.getDisplayName() + "\" добавьте хотя бы один слот");
|
||||
}
|
||||
if (hours == 0 && hasSlots) {
|
||||
throw new IllegalArgumentException("Для типа \"" + category.getDisplayName() + "\" укажите часы или удалите слоты этого типа");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMissing(Integer value) {
|
||||
return value == null;
|
||||
}
|
||||
|
||||
private boolean isNegative(Integer value) {
|
||||
return value < 0;
|
||||
}
|
||||
|
||||
private boolean isInvalidStartWeek(Integer value) {
|
||||
return value == null || value <= 0;
|
||||
}
|
||||
|
||||
private int valueOrZero(Integer value) {
|
||||
return value == null ? 0 : value;
|
||||
}
|
||||
|
||||
private ScheduleRuleDto toDto(ScheduleRule rule) {
|
||||
List<StudentGroup> groups = rule.getGroups().stream()
|
||||
.sorted(Comparator.comparing(StudentGroup::getName))
|
||||
.toList();
|
||||
List<ScheduleRuleSlotDto> slots = rule.getSlots().stream()
|
||||
.sorted(Comparator
|
||||
.comparing(ScheduleRuleSlot::getDayOfWeek)
|
||||
.thenComparing(slot -> slot.getTimeSlot().getOrderNumber())
|
||||
.thenComparing(ScheduleRuleSlot::getId))
|
||||
.map(this::toSlotDto)
|
||||
.toList();
|
||||
|
||||
return new ScheduleRuleDto(
|
||||
rule.getId(),
|
||||
rule.getSubject().getId(),
|
||||
rule.getSubject().getName(),
|
||||
rule.getSemester().getId(),
|
||||
rule.getSemester().getAcademicYear().getTitle(),
|
||||
rule.getSemester().getSemesterType(),
|
||||
rule.getLectureAcademicHours(),
|
||||
rule.getLaboratoryAcademicHours(),
|
||||
rule.getPracticeAcademicHours(),
|
||||
rule.getLectureStartWeek(),
|
||||
rule.getLaboratoryStartWeek(),
|
||||
rule.getPracticeStartWeek(),
|
||||
groups.stream().map(StudentGroup::getId).toList(),
|
||||
groups.stream().map(StudentGroup::getName).toList(),
|
||||
slots
|
||||
);
|
||||
}
|
||||
|
||||
private ScheduleRuleSlotDto toSlotDto(ScheduleRuleSlot slot) {
|
||||
TimeSlot timeSlot = slot.getTimeSlot();
|
||||
List<Subgroup> sortedSubgroups = sortedSubgroups(slot);
|
||||
return new ScheduleRuleSlotDto(
|
||||
slot.getId(),
|
||||
slot.getDayOfWeek(),
|
||||
dayName(slot.getDayOfWeek()),
|
||||
slot.getParity(),
|
||||
timeSlot.getId(),
|
||||
timeSlot.getOrderNumber(),
|
||||
formatTimeSlot(timeSlot),
|
||||
sortedSubgroups.isEmpty() ? null : sortedSubgroups.get(0).getId(),
|
||||
sortedSubgroups.isEmpty() ? null : formatSubgroup(sortedSubgroups.get(0)),
|
||||
sortedSubgroups.stream().map(Subgroup::getId).toList(),
|
||||
sortedSubgroups.stream().map(this::formatSubgroup).toList(),
|
||||
slot.getTeacher().getId(),
|
||||
ScheduleGeneratorService.displayUserName(slot.getTeacher()),
|
||||
slot.getClassroom().getId(),
|
||||
slot.getClassroom().getName(),
|
||||
slot.getLessonType().getId(),
|
||||
slot.getLessonType().getLessonType(),
|
||||
slot.getLessonFormat()
|
||||
);
|
||||
}
|
||||
|
||||
private String dayName(Integer dayOfWeek) {
|
||||
if (dayOfWeek == null || dayOfWeek < 1 || dayOfWeek > 7) {
|
||||
return "Неизвестно";
|
||||
}
|
||||
return ScheduleGeneratorService.dayName(DayOfWeek.of(dayOfWeek));
|
||||
}
|
||||
|
||||
private String formatTimeSlot(TimeSlot timeSlot) {
|
||||
return timeSlot.getStartTime() + " - " + timeSlot.getEndTime();
|
||||
}
|
||||
|
||||
private String formatSubgroup(Subgroup subgroup) {
|
||||
return subgroup.getStudentGroup().getName() + ": " + subgroup.getName();
|
||||
}
|
||||
|
||||
private List<Subgroup> sortedSubgroups(ScheduleRuleSlot slot) {
|
||||
return slot.getSubgroups().stream()
|
||||
.sorted(Comparator
|
||||
.comparing((Subgroup subgroup) -> subgroup.getStudentGroup().getName())
|
||||
.thenComparing(Subgroup::getName))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.service.ScheduleQueryService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/schedule/search")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER, Role.TEACHER, Role.STUDENT})
|
||||
public class ScheduleSearchController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScheduleSearchController.class);
|
||||
|
||||
private final ScheduleQueryService scheduleQueryService;
|
||||
|
||||
public ScheduleSearchController(ScheduleQueryService scheduleQueryService) {
|
||||
this.scheduleQueryService = scheduleQueryService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> search(
|
||||
@RequestParam(required = false) Long groupId,
|
||||
@RequestParam(required = false) Long teacherId,
|
||||
@RequestParam(required = false) Long classroomId,
|
||||
@RequestParam(required = false) Long departmentId,
|
||||
@RequestParam(required = false) Long subjectId,
|
||||
@RequestParam(required = false) Long lessonTypeId,
|
||||
@RequestParam(required = false) Long timeSlotId,
|
||||
@RequestParam(required = false) String parity,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate
|
||||
) {
|
||||
logger.info("Расширенный поиск расписания: groupId={}, teacherId={}, classroomId={}, departmentId={}, startDate={}, endDate={}",
|
||||
groupId, teacherId, classroomId, departmentId, startDate, endDate);
|
||||
try {
|
||||
return ResponseEntity.ok(scheduleQueryService.search(
|
||||
groupId,
|
||||
teacherId,
|
||||
classroomId,
|
||||
departmentId,
|
||||
subjectId,
|
||||
lessonTypeId,
|
||||
timeSlotId,
|
||||
parity,
|
||||
startDate,
|
||||
endDate
|
||||
));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка расширенного поиска расписания: {}", e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError().body(Map.of("message", "Произошла ошибка при поиске расписания"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateSpecialityRequest;
|
||||
import com.magistr.app.dto.SpecialityResponse;
|
||||
import com.magistr.app.dto.SpecialtyProfileDto;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.Speciality;
|
||||
import com.magistr.app.model.SpecialtyProfile;
|
||||
import com.magistr.app.repository.SpecialtiesRepository;
|
||||
import com.magistr.app.repository.SpecialtyProfileRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -15,21 +21,27 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/specialties")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class SpecialityController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SpecialityController.class);
|
||||
|
||||
private final SpecialtiesRepository specialtiesRepository;
|
||||
private final SpecialtyProfileRepository specialtyProfileRepository;
|
||||
|
||||
public SpecialityController(SpecialtiesRepository specialtiesRepository) {
|
||||
public SpecialityController(SpecialtiesRepository specialtiesRepository,
|
||||
SpecialtyProfileRepository specialtyProfileRepository) {
|
||||
this.specialtiesRepository = specialtiesRepository;
|
||||
this.specialtyProfileRepository = specialtyProfileRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Speciality> getAllSpecialties() {
|
||||
public List<Speciality> getAllSpecialties(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
logger.info("Получен запрос на получение списка специальностей");
|
||||
try {
|
||||
List<Speciality> specialities = specialtiesRepository.findAll();
|
||||
List<Speciality> specialities = includeArchived
|
||||
? specialtiesRepository.findAll()
|
||||
: specialtiesRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
List<Speciality> response = specialities.stream()
|
||||
.map( s -> new Speciality(
|
||||
s.getId(),
|
||||
@@ -75,6 +87,10 @@ public class SpecialityController {
|
||||
speciality.setSpecialityName(request.getSpecialityName());
|
||||
speciality.setSpecialityCode(request.getSpecialityCode());
|
||||
specialtiesRepository.save(speciality);
|
||||
SpecialtyProfile defaultProfile = new SpecialtyProfile();
|
||||
defaultProfile.setSpeciality(speciality);
|
||||
defaultProfile.setName("Без профиля");
|
||||
specialtyProfileRepository.save(defaultProfile);
|
||||
|
||||
logger.info("Специальность успешно создана с ID: {}", speciality.getId());
|
||||
|
||||
@@ -92,15 +108,182 @@ public class SpecialityController {
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/id")
|
||||
public ResponseEntity<?> deleteSpeciality(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление специальности с ID: {}", id);
|
||||
if (!specialtiesRepository.existsById(id)) {
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> updateSpeciality(@PathVariable Long id, @RequestBody CreateSpecialityRequest request) {
|
||||
logger.info("Получен запрос на обновление специальности с ID: {}", id);
|
||||
|
||||
Speciality speciality = specialtiesRepository.findById(id).orElse(null);
|
||||
if (speciality == null) {
|
||||
logger.info("Специальность с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
specialtiesRepository.deleteById(id);
|
||||
logger.info("Специальность с ID - {} успешно удалена", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Специальнсть удалена"));
|
||||
|
||||
try {
|
||||
if (request.getSpecialityName() == null || request.getSpecialityName().isBlank()) {
|
||||
String errorMessage = "Название специальности обязательно";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
if (request.getSpecialityCode() == null || request.getSpecialityCode().isBlank()) {
|
||||
String errorMessage = "Код специальности обязателен";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
String specialityName = request.getSpecialityName().trim();
|
||||
String specialityCode = request.getSpecialityCode().trim();
|
||||
if (specialtiesRepository.findBySpecialityName(specialityName)
|
||||
.filter(existing -> !existing.getId().equals(id))
|
||||
.isPresent()) {
|
||||
String errorMessage = "Специальность с таким названием уже существует";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
if (specialtiesRepository.findBySpecialityCode(specialityCode)
|
||||
.filter(existing -> !existing.getId().equals(id))
|
||||
.isPresent()) {
|
||||
String errorMessage = "Специальность с таким кодом уже существует";
|
||||
logger.error("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
speciality.setSpecialityName(specialityName);
|
||||
speciality.setSpecialityCode(specialityCode);
|
||||
specialtiesRepository.save(speciality);
|
||||
|
||||
logger.info("Специальность с ID - {} успешно обновлена", id);
|
||||
return ResponseEntity.ok(new SpecialityResponse(
|
||||
speciality.getId(),
|
||||
speciality.getSpecialityName(),
|
||||
speciality.getSpecialityCode()
|
||||
));
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при обновлении специальности с ID - {}: {}", id, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message", "Произошла ошибка при обновлении специальности " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteSpeciality(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление специальности с ID: {}", id);
|
||||
Speciality speciality = specialtiesRepository.findById(id).orElse(null);
|
||||
if (speciality == null) {
|
||||
logger.info("Специальность с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
speciality.archive("Специальность архивирована");
|
||||
specialtiesRepository.save(speciality);
|
||||
logger.info("Специальность с ID - {} успешно архивирована", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Специальность архивирована"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
public ResponseEntity<?> restoreSpeciality(@PathVariable Long id) {
|
||||
Speciality speciality = specialtiesRepository.findById(id).orElse(null);
|
||||
if (speciality == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
speciality.restore();
|
||||
specialtiesRepository.save(speciality);
|
||||
return ResponseEntity.ok(new SpecialityResponse(
|
||||
speciality.getId(),
|
||||
speciality.getSpecialityName(),
|
||||
speciality.getSpecialityCode()
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/{specialtyId}/profiles")
|
||||
public ResponseEntity<?> getProfiles(@PathVariable Long specialtyId) {
|
||||
if (!specialtiesRepository.existsById(specialtyId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(specialtyProfileRepository.findBySpecialityIdOrderByNameAsc(specialtyId).stream()
|
||||
.map(this::toProfileDto)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/{specialtyId}/profiles")
|
||||
public ResponseEntity<?> createProfile(@PathVariable Long specialtyId, @RequestBody SpecialtyProfileDto request) {
|
||||
Speciality speciality = specialtiesRepository.findById(specialtyId).orElse(null);
|
||||
if (speciality == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
String validationError = validateProfile(specialtyId, null, request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
SpecialtyProfile profile = new SpecialtyProfile();
|
||||
profile.setSpeciality(speciality);
|
||||
profile.setName(request.name().trim());
|
||||
profile.setDescription(trimToNull(request.description()));
|
||||
return ResponseEntity.ok(toProfileDto(specialtyProfileRepository.save(profile)));
|
||||
}
|
||||
|
||||
@PutMapping("/{specialtyId}/profiles/{profileId}")
|
||||
public ResponseEntity<?> updateProfile(@PathVariable Long specialtyId,
|
||||
@PathVariable Long profileId,
|
||||
@RequestBody SpecialtyProfileDto request) {
|
||||
SpecialtyProfile profile = specialtyProfileRepository.findById(profileId).orElse(null);
|
||||
if (profile == null || !profile.getSpeciality().getId().equals(specialtyId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
String validationError = validateProfile(specialtyId, profileId, request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
profile.setName(request.name().trim());
|
||||
profile.setDescription(trimToNull(request.description()));
|
||||
return ResponseEntity.ok(toProfileDto(specialtyProfileRepository.save(profile)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{specialtyId}/profiles/{profileId}")
|
||||
public ResponseEntity<?> deleteProfile(@PathVariable Long specialtyId, @PathVariable Long profileId) {
|
||||
SpecialtyProfile profile = specialtyProfileRepository.findById(profileId).orElse(null);
|
||||
if (profile == null || !profile.getSpeciality().getId().equals(specialtyId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
try {
|
||||
specialtyProfileRepository.delete(profile);
|
||||
return ResponseEntity.ok(Map.of("message", "Профиль обучения удалён"));
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при удалении профиля обучения с ID - {}: {}", profileId, e.getMessage(), e);
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "Нельзя удалить профиль, который используется группами или календарными графиками"));
|
||||
}
|
||||
}
|
||||
|
||||
private String validateProfile(Long specialtyId, Long profileId, SpecialtyProfileDto request) {
|
||||
if (request == null) {
|
||||
return "Передайте данные профиля";
|
||||
}
|
||||
if (request.name() == null || request.name().isBlank()) {
|
||||
return "Название профиля обязательно";
|
||||
}
|
||||
return specialtyProfileRepository.findBySpecialityIdAndName(specialtyId, request.name().trim())
|
||||
.filter(existing -> !existing.getId().equals(profileId))
|
||||
.map(existing -> "Профиль с таким названием уже существует")
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private SpecialtyProfileDto toProfileDto(SpecialtyProfile profile) {
|
||||
Speciality speciality = profile.getSpeciality();
|
||||
return new SpecialtyProfileDto(
|
||||
profile.getId(),
|
||||
speciality.getId(),
|
||||
speciality.getSpecialityCode(),
|
||||
speciality.getSpecialityName(),
|
||||
profile.getName(),
|
||||
profile.getDescription()
|
||||
);
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (value == null || value.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.SubgroupDto;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.StudentGroup;
|
||||
import com.magistr.app.model.Subgroup;
|
||||
import com.magistr.app.repository.GroupRepository;
|
||||
import com.magistr.app.repository.ScheduleRuleSlotRepository;
|
||||
import com.magistr.app.repository.SubgroupRepository;
|
||||
import com.magistr.app.service.ScheduleGeneratorService;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.STUDENT})
|
||||
public class SubgroupController {
|
||||
|
||||
private final SubgroupRepository subgroupRepository;
|
||||
private final GroupRepository groupRepository;
|
||||
private final ScheduleRuleSlotRepository scheduleRuleSlotRepository;
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
|
||||
public SubgroupController(SubgroupRepository subgroupRepository,
|
||||
GroupRepository groupRepository,
|
||||
ScheduleRuleSlotRepository scheduleRuleSlotRepository,
|
||||
ScheduleGeneratorService scheduleGeneratorService) {
|
||||
this.subgroupRepository = subgroupRepository;
|
||||
this.groupRepository = groupRepository;
|
||||
this.scheduleRuleSlotRepository = scheduleRuleSlotRepository;
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
}
|
||||
|
||||
@GetMapping("/api/subgroups")
|
||||
public List<SubgroupDto> getAll() {
|
||||
return subgroupRepository.findAllWithGroupsOrderByGroupNameAndName().stream()
|
||||
.map(this::toDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping("/api/groups/{groupId}/subgroups")
|
||||
public ResponseEntity<?> getByGroup(@PathVariable Long groupId) {
|
||||
if (!groupRepository.existsById(groupId)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(subgroupRepository.findByStudentGroupIdOrderByNameAsc(groupId).stream()
|
||||
.map(this::toDto)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/api/groups/{groupId}/subgroups")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> create(@PathVariable Long groupId, @RequestBody SubgroupDto request) {
|
||||
StudentGroup group = groupRepository.findById(groupId).orElse(null);
|
||||
if (group == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
String validationError = validate(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
if (subgroupRepository.existsByStudentGroupIdAndNameIgnoreCase(groupId, request.name().trim())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа с таким названием уже есть в группе"));
|
||||
}
|
||||
|
||||
Subgroup subgroup = new Subgroup();
|
||||
subgroup.setStudentGroup(group);
|
||||
subgroup.setName(request.name().trim());
|
||||
subgroup.setStudentCapacity(request.studentCapacity());
|
||||
Subgroup saved = subgroupRepository.save(subgroup);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toDto(saved));
|
||||
}
|
||||
|
||||
@PutMapping("/api/groups/{groupId}/subgroups/{id}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> update(@PathVariable Long groupId,
|
||||
@PathVariable Long id,
|
||||
@RequestBody SubgroupDto request) {
|
||||
Subgroup subgroup = subgroupRepository.findByIdAndStudentGroupId(id, groupId).orElse(null);
|
||||
if (subgroup == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
String validationError = validate(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
String newName = request.name().trim();
|
||||
boolean duplicateName = subgroupRepository.existsByStudentGroupIdAndNameIgnoreCase(groupId, newName)
|
||||
&& !subgroup.getName().equalsIgnoreCase(newName);
|
||||
if (duplicateName) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа с таким названием уже есть в группе"));
|
||||
}
|
||||
|
||||
subgroup.setName(newName);
|
||||
subgroup.setStudentCapacity(request.studentCapacity());
|
||||
Subgroup saved = subgroupRepository.save(subgroup);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toDto(saved));
|
||||
}
|
||||
|
||||
@DeleteMapping("/api/groups/{groupId}/subgroups/{id}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> delete(@PathVariable Long groupId, @PathVariable Long id) {
|
||||
Subgroup subgroup = subgroupRepository.findByIdAndStudentGroupId(id, groupId).orElse(null);
|
||||
if (subgroup == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (scheduleRuleSlotRepository.existsBySubgroupId(id)) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа используется в расписании"));
|
||||
}
|
||||
try {
|
||||
subgroup.archive("Подгруппа архивирована");
|
||||
subgroupRepository.save(subgroup);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Подгруппа архивирована"));
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа используется в расписании"));
|
||||
}
|
||||
}
|
||||
|
||||
private String validate(SubgroupDto request) {
|
||||
if (request == null || request.name() == null || request.name().isBlank()) {
|
||||
return "Название подгруппы обязательно";
|
||||
}
|
||||
if (request.studentCapacity() != null && request.studentCapacity() < 0) {
|
||||
return "Численность подгруппы не может быть отрицательной";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private SubgroupDto toDto(Subgroup subgroup) {
|
||||
StudentGroup group = subgroup.getStudentGroup();
|
||||
return new SubgroupDto(
|
||||
subgroup.getId(),
|
||||
group.getId(),
|
||||
group.getName(),
|
||||
subgroup.getName(),
|
||||
subgroup.getStudentCapacity()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateSubjectRequest;
|
||||
import com.magistr.app.dto.SubjectResponse;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.Subject;
|
||||
import com.magistr.app.repository.SubjectRepository;
|
||||
import org.slf4j.Logger;
|
||||
@@ -16,6 +19,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/subjects")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class SubjectController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SubjectController.class);
|
||||
@@ -27,10 +31,13 @@ public class SubjectController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Subject> getAllSubjects() {
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public List<Subject> getAllSubjects(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
logger.info("Получен запрос на получение всех дисциплин");
|
||||
try {
|
||||
List<Subject> subjects = subjectRepository.findAll();
|
||||
List<Subject> subjects = includeArchived
|
||||
? subjectRepository.findAll()
|
||||
: subjectRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
List<Subject> response = subjects.stream()
|
||||
.map(s -> new Subject(
|
||||
s.getId(),
|
||||
@@ -48,10 +55,11 @@ public class SubjectController {
|
||||
}
|
||||
|
||||
@GetMapping("/{departmentId}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public ResponseEntity<?> getSubjectsByDepartmentId(@PathVariable Long departmentId) {
|
||||
logger.info("Получен запрос на получение дисциплин для кафедры с ID - {}", departmentId);
|
||||
try{
|
||||
List<Subject> subjects = subjectRepository.findByDepartmentId(departmentId);
|
||||
List<Subject> subjects = subjectRepository.findByDepartmentIdAndStatusNot(departmentId, LifecycleEntity.STATUS_ARCHIVED);
|
||||
|
||||
if(subjects.isEmpty()){
|
||||
logger.info("Дисциплины для кафедры с ID - {} не найдены", departmentId);
|
||||
@@ -121,12 +129,31 @@ public class SubjectController {
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteSubject(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление дисциплины с ID: {}", id);
|
||||
if (!subjectRepository.existsById(id)) {
|
||||
Subject subject = subjectRepository.findById(id).orElse(null);
|
||||
if (subject == null) {
|
||||
logger.info("Дисциплина с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
subjectRepository.deleteById(id);
|
||||
logger.info("Дисциплина с ID - {} успешно удалена", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Дисциплина удалена"));
|
||||
subject.archive("Дисциплина архивирована");
|
||||
subjectRepository.save(subject);
|
||||
logger.info("Дисциплина с ID - {} успешно архивирована", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Дисциплина архивирована"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public ResponseEntity<?> restoreSubject(@PathVariable Long id) {
|
||||
Subject subject = subjectRepository.findById(id).orElse(null);
|
||||
if (subject == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
subject.restore();
|
||||
subjectRepository.save(subject);
|
||||
return ResponseEntity.ok(new SubjectResponse(
|
||||
subject.getId(),
|
||||
subject.getName(),
|
||||
subject.getCode(),
|
||||
subject.getDepartmentId()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.TeacherSubjectResponse;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.Subject;
|
||||
import com.magistr.app.model.TeacherSubject;
|
||||
import com.magistr.app.model.TeacherSubjectId;
|
||||
import com.magistr.app.model.User;
|
||||
import com.magistr.app.repository.SubjectRepository;
|
||||
import com.magistr.app.repository.TeacherSubjectRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
@@ -14,6 +19,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/teacher-subjects")
|
||||
@RequireRoles({Role.ADMIN, Role.DEPARTMENT})
|
||||
public class TeacherSubjectController {
|
||||
|
||||
private final TeacherSubjectRepository teacherSubjectRepository;
|
||||
@@ -31,6 +37,7 @@ public class TeacherSubjectController {
|
||||
@GetMapping
|
||||
public List<TeacherSubjectResponse> getAll() {
|
||||
return teacherSubjectRepository.findAll().stream()
|
||||
.filter(ts -> canManage(ts.getUser(), ts.getSubject()))
|
||||
.map(ts -> new TeacherSubjectResponse(
|
||||
ts.getUserId(),
|
||||
ts.getUser().getUsername(),
|
||||
@@ -48,12 +55,17 @@ public class TeacherSubjectController {
|
||||
if (userId == null || subjectId == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "userId и subjectId обязательны"));
|
||||
}
|
||||
if (!userRepository.existsById(userId)) {
|
||||
User user = userRepository.findById(userId).orElse(null);
|
||||
if (user == null || user.getRole() != Role.TEACHER || user.isArchivedRecord()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Преподаватель не найден"));
|
||||
}
|
||||
if (!subjectRepository.existsById(subjectId)) {
|
||||
Subject subject = subjectRepository.findById(subjectId).orElse(null);
|
||||
if (subject == null || subject.isArchivedRecord()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Дисциплина не найдена"));
|
||||
}
|
||||
if (!canManage(user, subject)) {
|
||||
return ResponseEntity.status(403).body(Map.of("message", "Кафедра может связывать только своих преподавателей и свои дисциплины"));
|
||||
}
|
||||
|
||||
TeacherSubjectId id = new TeacherSubjectId(userId, subjectId);
|
||||
if (teacherSubjectRepository.existsById(id)) {
|
||||
@@ -76,10 +88,30 @@ public class TeacherSubjectController {
|
||||
}
|
||||
|
||||
TeacherSubjectId id = new TeacherSubjectId(userId, subjectId);
|
||||
if (!teacherSubjectRepository.existsById(id)) {
|
||||
TeacherSubject teacherSubject = teacherSubjectRepository.findById(id).orElse(null);
|
||||
if (teacherSubject == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (!canManage(teacherSubject.getUser(), teacherSubject.getSubject())) {
|
||||
return ResponseEntity.status(403).body(Map.of("message", "Кафедра может удалять только свои привязки"));
|
||||
}
|
||||
teacherSubjectRepository.deleteById(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Привязка удалена"));
|
||||
}
|
||||
|
||||
private boolean canManage(User teacher, Subject subject) {
|
||||
var currentUser = AuthContext.getCurrentUser();
|
||||
if (currentUser == null || currentUser.role() == Role.ADMIN) {
|
||||
return true;
|
||||
}
|
||||
if (currentUser.role() != Role.DEPARTMENT) {
|
||||
return false;
|
||||
}
|
||||
Long departmentId = currentUser.departmentId();
|
||||
return departmentId != null
|
||||
&& teacher != null
|
||||
&& subject != null
|
||||
&& departmentId.equals(teacher.getDepartmentId())
|
||||
&& departmentId.equals(subject.getDepartmentId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,306 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.TimeSlotDateAssignmentDto;
|
||||
import com.magistr.app.dto.TimeSlotDto;
|
||||
import com.magistr.app.dto.TimeSlotScopeDto;
|
||||
import com.magistr.app.model.TimeSlot;
|
||||
import com.magistr.app.model.TimeSlotDateAssignment;
|
||||
import com.magistr.app.model.TimeSlotScope;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.repository.TimeSlotDateAssignmentRepository;
|
||||
import com.magistr.app.repository.TimeSlotRepository;
|
||||
import com.magistr.app.repository.TimeSlotScopeRepository;
|
||||
import org.springframework.dao.DataIntegrityViolationException;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/time-slots")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class TimeSlotAdminController {
|
||||
|
||||
private static final String APPLY_MODE_DEFAULT = "DEFAULT";
|
||||
private static final String APPLY_MODE_WEEKDAY = "WEEKDAY";
|
||||
private static final String APPLY_MODE_MANUAL = "MANUAL";
|
||||
|
||||
private final TimeSlotRepository timeSlotRepository;
|
||||
private final TimeSlotScopeRepository timeSlotScopeRepository;
|
||||
private final TimeSlotDateAssignmentRepository dateAssignmentRepository;
|
||||
|
||||
public TimeSlotAdminController(TimeSlotRepository timeSlotRepository,
|
||||
TimeSlotScopeRepository timeSlotScopeRepository,
|
||||
TimeSlotDateAssignmentRepository dateAssignmentRepository) {
|
||||
this.timeSlotRepository = timeSlotRepository;
|
||||
this.timeSlotScopeRepository = timeSlotScopeRepository;
|
||||
this.dateAssignmentRepository = dateAssignmentRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<TimeSlotDto> getAll() {
|
||||
return timeSlotRepository.findAllByOrderByTimeSlotScopeDisplayOrderAscOrderNumberAscStartTimeAsc().stream()
|
||||
.map(this::toDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping("/effective")
|
||||
public ResponseEntity<?> getEffective(@RequestParam LocalDate date) {
|
||||
return ResponseEntity.ok(effectiveSlots(date).stream().map(this::toDto).toList());
|
||||
}
|
||||
|
||||
@GetMapping("/scopes")
|
||||
public List<TimeSlotScopeDto> getScopes() {
|
||||
return timeSlotScopeRepository.findAllByOrderByDisplayOrderAscNameAsc().stream()
|
||||
.map(this::toScopeDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping("/scopes")
|
||||
public ResponseEntity<?> createScope(@RequestBody TimeSlotScopeDto request) {
|
||||
if (request.name() == null || request.name().isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Название сетки обязательно"));
|
||||
}
|
||||
|
||||
TimeSlotScope scope = new TimeSlotScope();
|
||||
scope.setCode("manual_" + UUID.randomUUID().toString().replace("-", ""));
|
||||
scope.setName(request.name().trim());
|
||||
scope.setApplyMode(APPLY_MODE_MANUAL);
|
||||
scope.setDayOfWeek(null);
|
||||
scope.setSystemScope(false);
|
||||
scope.setDisplayOrder(nextManualDisplayOrder());
|
||||
return ResponseEntity.ok(toScopeDto(timeSlotScopeRepository.save(scope)));
|
||||
}
|
||||
|
||||
@PutMapping("/scopes/{id}")
|
||||
public ResponseEntity<?> updateScope(@PathVariable Long id, @RequestBody TimeSlotScopeDto request) {
|
||||
TimeSlotScope scope = timeSlotScopeRepository.findById(id).orElse(null);
|
||||
if (scope == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (Boolean.TRUE.equals(scope.getSystemScope())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Системную сетку нельзя изменять"));
|
||||
}
|
||||
if (request.name() == null || request.name().isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Название сетки обязательно"));
|
||||
}
|
||||
scope.setName(request.name().trim());
|
||||
return ResponseEntity.ok(toScopeDto(timeSlotScopeRepository.save(scope)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/scopes/{id}")
|
||||
public ResponseEntity<?> deleteScope(@PathVariable Long id) {
|
||||
TimeSlotScope scope = timeSlotScopeRepository.findById(id).orElse(null);
|
||||
if (scope == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (Boolean.TRUE.equals(scope.getSystemScope())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Системную сетку нельзя удалить"));
|
||||
}
|
||||
timeSlotScopeRepository.delete(scope);
|
||||
return ResponseEntity.ok(Map.of("message", "Сетка времени удалена"));
|
||||
}
|
||||
|
||||
@GetMapping("/date-assignments")
|
||||
public List<TimeSlotDateAssignmentDto> getDateAssignments(@RequestParam(required = false) LocalDate startDate,
|
||||
@RequestParam(required = false) LocalDate endDate) {
|
||||
List<TimeSlotDateAssignment> assignments = startDate != null && endDate != null
|
||||
? dateAssignmentRepository.findByDateBetweenOrderByDateAsc(startDate, endDate)
|
||||
: dateAssignmentRepository.findAllByOrderByDateAsc();
|
||||
return assignments.stream().map(this::toAssignmentDto).toList();
|
||||
}
|
||||
|
||||
@PostMapping("/date-assignments")
|
||||
public ResponseEntity<?> saveDateAssignment(@RequestBody TimeSlotDateAssignmentDto request) {
|
||||
if (request.date() == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Дата обязательна"));
|
||||
}
|
||||
if (request.scopeId() == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Сетка времени обязательна"));
|
||||
}
|
||||
TimeSlotScope scope = timeSlotScopeRepository.findById(request.scopeId()).orElse(null);
|
||||
if (scope == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Сетка времени не найдена"));
|
||||
}
|
||||
if (!APPLY_MODE_MANUAL.equals(scope.getApplyMode())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Вручную можно применять только пользовательские сетки"));
|
||||
}
|
||||
|
||||
TimeSlotDateAssignment assignment = dateAssignmentRepository.findByDate(request.date())
|
||||
.orElseGet(TimeSlotDateAssignment::new);
|
||||
assignment.setDate(request.date());
|
||||
assignment.setTimeSlotScope(scope);
|
||||
return ResponseEntity.ok(toAssignmentDto(dateAssignmentRepository.save(assignment)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/date-assignments/{id}")
|
||||
public ResponseEntity<?> deleteDateAssignment(@PathVariable Long id) {
|
||||
if (!dateAssignmentRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
dateAssignmentRepository.deleteById(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Ручное назначение удалено"));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> create(@RequestBody TimeSlotDto request) {
|
||||
String validationError = validate(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
if (hasDuplicate(request, null)) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "В выбранной сетке уже есть слот с таким номером пары"));
|
||||
}
|
||||
|
||||
TimeSlot timeSlot = new TimeSlot();
|
||||
apply(timeSlot, request);
|
||||
return ResponseEntity.ok(toDto(timeSlotRepository.save(timeSlot)));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> update(@PathVariable Long id, @RequestBody TimeSlotDto request) {
|
||||
TimeSlot timeSlot = timeSlotRepository.findById(id).orElse(null);
|
||||
if (timeSlot == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
String validationError = validate(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
if (hasDuplicate(request, id)) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "В выбранной сетке уже есть слот с таким номером пары"));
|
||||
}
|
||||
|
||||
apply(timeSlot, request);
|
||||
return ResponseEntity.ok(toDto(timeSlotRepository.save(timeSlot)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> delete(@PathVariable Long id) {
|
||||
if (!timeSlotRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
try {
|
||||
timeSlotRepository.deleteById(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Временной слот удалён"));
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Нельзя удалить слот, который используется в правилах расписания"));
|
||||
}
|
||||
}
|
||||
|
||||
private String validate(TimeSlotDto request) {
|
||||
if (request.orderNumber() == null || request.orderNumber() <= 0) {
|
||||
return "Номер пары должен быть больше нуля";
|
||||
}
|
||||
if (request.scopeId() == null || timeSlotScopeRepository.findById(request.scopeId()).isEmpty()) {
|
||||
return "Выберите сетку времени";
|
||||
}
|
||||
if (request.startTime() == null || request.endTime() == null) {
|
||||
return "Время начала и окончания обязательно";
|
||||
}
|
||||
if (!request.startTime().isBefore(request.endTime())) {
|
||||
return "Время начала должно быть раньше времени окончания";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void apply(TimeSlot timeSlot, TimeSlotDto request) {
|
||||
timeSlot.setOrderNumber(request.orderNumber());
|
||||
timeSlot.setTimeSlotScope(timeSlotScopeRepository.findById(request.scopeId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Сетка времени не найдена")));
|
||||
timeSlot.setStartTime(request.startTime());
|
||||
timeSlot.setEndTime(request.endTime());
|
||||
int duration = request.durationMinutes() != null && request.durationMinutes() > 0
|
||||
? request.durationMinutes()
|
||||
: (int) Duration.between(request.startTime(), request.endTime()).toMinutes();
|
||||
timeSlot.setDurationMinutes(duration);
|
||||
}
|
||||
|
||||
private boolean hasDuplicate(TimeSlotDto request, Long currentId) {
|
||||
return timeSlotRepository.findByTimeSlotScopeIdAndOrderNumber(request.scopeId(), request.orderNumber())
|
||||
.filter(existing -> currentId == null || !existing.getId().equals(currentId))
|
||||
.isPresent();
|
||||
}
|
||||
|
||||
private List<TimeSlot> effectiveSlots(LocalDate date) {
|
||||
TimeSlotScope defaultScope = defaultScope();
|
||||
TimeSlotScope effectiveScope = effectiveScope(date);
|
||||
List<TimeSlot> defaultSlots = timeSlotRepository.findAllByTimeSlotScopeIdOrderByOrderNumberAsc(defaultScope.getId());
|
||||
if (effectiveScope.getId().equals(defaultScope.getId())) {
|
||||
return defaultSlots;
|
||||
}
|
||||
|
||||
Map<Integer, TimeSlot> slotsByOrder = new LinkedHashMap<>();
|
||||
defaultSlots.forEach(slot -> slotsByOrder.put(slot.getOrderNumber(), slot));
|
||||
timeSlotRepository.findAllByTimeSlotScopeIdOrderByOrderNumberAsc(effectiveScope.getId())
|
||||
.forEach(slot -> slotsByOrder.put(slot.getOrderNumber(), slot));
|
||||
return new ArrayList<>(slotsByOrder.values());
|
||||
}
|
||||
|
||||
private TimeSlotScope effectiveScope(LocalDate date) {
|
||||
return dateAssignmentRepository.findByDate(date)
|
||||
.map(TimeSlotDateAssignment::getTimeSlotScope)
|
||||
.or(() -> timeSlotScopeRepository.findByApplyModeAndDayOfWeek(
|
||||
APPLY_MODE_WEEKDAY,
|
||||
date.getDayOfWeek().getValue()))
|
||||
.orElseGet(this::defaultScope);
|
||||
}
|
||||
|
||||
private TimeSlotScope defaultScope() {
|
||||
return timeSlotScopeRepository.findFirstByApplyModeOrderByDisplayOrderAsc(APPLY_MODE_DEFAULT)
|
||||
.orElseThrow(() -> new IllegalStateException("Базовая сетка времени не настроена"));
|
||||
}
|
||||
|
||||
private int nextManualDisplayOrder() {
|
||||
return timeSlotScopeRepository.findAllByOrderByDisplayOrderAscNameAsc().stream()
|
||||
.map(TimeSlotScope::getDisplayOrder)
|
||||
.filter(value -> value != null)
|
||||
.max(Integer::compareTo)
|
||||
.orElse(100) + 10;
|
||||
}
|
||||
|
||||
private TimeSlotDto toDto(TimeSlot timeSlot) {
|
||||
TimeSlotScope scope = timeSlot.getTimeSlotScope();
|
||||
return new TimeSlotDto(
|
||||
timeSlot.getId(),
|
||||
timeSlot.getOrderNumber(),
|
||||
scope == null ? null : scope.getId(),
|
||||
scope == null ? null : scope.getName(),
|
||||
scope == null ? null : scope.getApplyMode(),
|
||||
scope == null ? null : scope.getDayOfWeek(),
|
||||
timeSlot.getStartTime(),
|
||||
timeSlot.getEndTime(),
|
||||
timeSlot.getDurationMinutes()
|
||||
);
|
||||
}
|
||||
|
||||
private TimeSlotScopeDto toScopeDto(TimeSlotScope scope) {
|
||||
return new TimeSlotScopeDto(
|
||||
scope.getId(),
|
||||
scope.getCode(),
|
||||
scope.getName(),
|
||||
scope.getApplyMode(),
|
||||
scope.getDayOfWeek(),
|
||||
scope.getSystemScope(),
|
||||
scope.getDisplayOrder()
|
||||
);
|
||||
}
|
||||
|
||||
private TimeSlotDateAssignmentDto toAssignmentDto(TimeSlotDateAssignment assignment) {
|
||||
TimeSlotScope scope = assignment.getTimeSlotScope();
|
||||
return new TimeSlotDateAssignmentDto(
|
||||
assignment.getId(),
|
||||
assignment.getDate(),
|
||||
scope.getId(),
|
||||
scope.getName()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,61 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateUserRequest;
|
||||
import com.magistr.app.dto.DepartmentTransferRequest;
|
||||
import com.magistr.app.dto.TeacherDepartmentAssignmentDto;
|
||||
import com.magistr.app.dto.UserResponse;
|
||||
import com.magistr.app.model.Department;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.TeacherDepartmentAssignment;
|
||||
import com.magistr.app.model.User;
|
||||
import com.magistr.app.repository.DepartmentRepository;
|
||||
import com.magistr.app.repository.TeacherDepartmentAssignmentRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class UserController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
|
||||
private final UserRepository userRepository;
|
||||
private final DepartmentRepository departmentRepository;
|
||||
private final TeacherDepartmentAssignmentRepository teacherDepartmentAssignmentRepository;
|
||||
private final BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
public UserController(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder, DepartmentRepository departmentRepository) {
|
||||
public UserController(UserRepository userRepository,
|
||||
BCryptPasswordEncoder passwordEncoder,
|
||||
DepartmentRepository departmentRepository,
|
||||
TeacherDepartmentAssignmentRepository teacherDepartmentAssignmentRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.departmentRepository = departmentRepository;
|
||||
this.teacherDepartmentAssignmentRepository = teacherDepartmentAssignmentRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<UserResponse> getAllUsers() {
|
||||
public List<UserResponse> getAllUsers(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
logger.info("Получен запрос на получение всех пользователей");
|
||||
try {
|
||||
List<User> users = userRepository.findAll();
|
||||
List<User> users = includeArchived
|
||||
? userRepository.findAll()
|
||||
: userRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
|
||||
List<UserResponse> response = users.stream()
|
||||
.map(u -> {
|
||||
@@ -45,13 +63,15 @@ public class UserController {
|
||||
.map(Department::getDepartmentName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
return new UserResponse(
|
||||
UserResponse userResponse = new UserResponse(
|
||||
u.getId(),
|
||||
u.getUsername(),
|
||||
u.getRole().name(),
|
||||
u.getFullName(),
|
||||
u.getJobTitle(),
|
||||
departmentName);
|
||||
userResponse.setStatus(u.getStatus());
|
||||
return userResponse;
|
||||
})
|
||||
.toList();
|
||||
logger.info("Получено {} пользователей", response.size());
|
||||
@@ -64,11 +84,12 @@ public class UserController {
|
||||
}
|
||||
|
||||
@GetMapping("/teachers")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public List<UserResponse> getTeachers() {
|
||||
logger.info("Запрос на получение пользователей с ролью 'Преподаватель'");
|
||||
|
||||
try {
|
||||
List<User> users = userRepository.findByRole(Role.TEACHER);
|
||||
List<User> users = userRepository.findByRoleAndStatusNot(Role.TEACHER, LifecycleEntity.STATUS_ARCHIVED);
|
||||
|
||||
List<UserResponse> response = users.stream()
|
||||
.map(u -> {
|
||||
@@ -76,13 +97,15 @@ public class UserController {
|
||||
.map(Department::getDepartmentName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
return new UserResponse(
|
||||
UserResponse userResponse = new UserResponse(
|
||||
u.getId(),
|
||||
u.getUsername(),
|
||||
u.getRole().name(),
|
||||
u.getFullName(),
|
||||
u.getJobTitle(),
|
||||
departmentName);
|
||||
userResponse.setStatus(u.getStatus());
|
||||
return userResponse;
|
||||
})
|
||||
.toList();
|
||||
logger.info("Получено {} преподавателей", response.size());
|
||||
@@ -94,10 +117,15 @@ public class UserController {
|
||||
}
|
||||
|
||||
@GetMapping("/teachers/{departmentId}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public ResponseEntity<?> getTeachersByDepartmentId(@PathVariable Long departmentId){
|
||||
logger.info("Получен запрос на получение преподавателей для кафедры с ID - {}", departmentId);
|
||||
try {
|
||||
List<User> users = userRepository.findByRoleAndDepartmentId(Role.TEACHER, departmentId);
|
||||
List<User> users = userRepository.findByRoleAndDepartmentIdAndStatusNot(
|
||||
Role.TEACHER,
|
||||
departmentId,
|
||||
LifecycleEntity.STATUS_ARCHIVED
|
||||
);
|
||||
|
||||
if (users.isEmpty()) {
|
||||
logger.info("Преподаватели для кафедры с ID - {} не найдены", departmentId);
|
||||
@@ -178,6 +206,19 @@ public class UserController {
|
||||
user.setJobTitle(request.getJobTitle());
|
||||
user.setDepartmentId(request.getDepartmentId());
|
||||
userRepository.save(user);
|
||||
if (role == Role.TEACHER) {
|
||||
Department department = departmentRepository.findById(user.getDepartmentId()).orElse(null);
|
||||
if (department != null) {
|
||||
TeacherDepartmentAssignment assignment = new TeacherDepartmentAssignment();
|
||||
assignment.setTeacher(user);
|
||||
assignment.setDepartment(department);
|
||||
assignment.setValidFrom(LocalDate.now());
|
||||
assignment.setPrimaryAssignment(true);
|
||||
assignment.setComment("Начальная кафедра при создании пользователя");
|
||||
assignment.setCreatedBy(AuthContext.currentUserId());
|
||||
teacherDepartmentAssignmentRepository.save(assignment);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Пользователь успешно создан с ID: {}", user.getId());
|
||||
|
||||
@@ -192,12 +233,111 @@ public class UserController {
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление пользователя с ID: {}", id);
|
||||
if (!userRepository.existsById(id)) {
|
||||
User user = userRepository.findById(id).orElse(null);
|
||||
if (user == null) {
|
||||
logger.info("Пользователь с ID - {} не найден", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
userRepository.deleteById(id);
|
||||
logger.info("Пользователь с ID - {} успешно удалён", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Пользователь удалён"));
|
||||
user.archive("Пользователь архивирован");
|
||||
userRepository.save(user);
|
||||
logger.info("Пользователь с ID - {} успешно архивирован", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Пользователь архивирован"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
public ResponseEntity<?> restoreUser(@PathVariable Long id) {
|
||||
User user = userRepository.findById(id).orElse(null);
|
||||
if (user == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
user.restore();
|
||||
userRepository.save(user);
|
||||
return ResponseEntity.ok(new UserResponse(user.getId(), user.getUsername(), user.getRole().name(),
|
||||
user.getFullName(), user.getJobTitle(), user.getDepartmentId()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/department-history")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT})
|
||||
public ResponseEntity<?> getDepartmentHistory(@PathVariable Long id) {
|
||||
if (!userRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(teacherDepartmentAssignmentRepository.findHistoryByTeacherId(id).stream()
|
||||
.map(this::toAssignmentDto)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/department-transfer")
|
||||
@Transactional
|
||||
public ResponseEntity<?> transferTeacher(@PathVariable Long id, @RequestBody DepartmentTransferRequest request) {
|
||||
User teacher = userRepository.findById(id).orElse(null);
|
||||
if (teacher == null || teacher.getRole() != Role.TEACHER) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Преподаватель не найден"));
|
||||
}
|
||||
if (teacher.isArchivedRecord()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Архивного преподавателя нельзя перевести"));
|
||||
}
|
||||
if (request.departmentId() == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Кафедра обязательна"));
|
||||
}
|
||||
Department department = departmentRepository.findById(request.departmentId()).orElse(null);
|
||||
if (department == null || department.isArchivedRecord()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Активная кафедра не найдена"));
|
||||
}
|
||||
LocalDate validFrom = request.validFrom() == null ? LocalDate.now() : request.validFrom();
|
||||
|
||||
teacherDepartmentAssignmentRepository.findOpenPrimaryByTeacherId(id)
|
||||
.ifPresent(current -> {
|
||||
if (current.getValidFrom().isBefore(validFrom)) {
|
||||
current.setValidTo(validFrom.minusDays(1));
|
||||
teacherDepartmentAssignmentRepository.save(current);
|
||||
} else {
|
||||
teacherDepartmentAssignmentRepository.delete(current);
|
||||
}
|
||||
});
|
||||
|
||||
TeacherDepartmentAssignment assignment = new TeacherDepartmentAssignment();
|
||||
assignment.setTeacher(teacher);
|
||||
assignment.setDepartment(department);
|
||||
assignment.setValidFrom(validFrom);
|
||||
assignment.setPrimaryAssignment(true);
|
||||
assignment.setComment(request.comment());
|
||||
assignment.setCreatedBy(AuthContext.currentUserId());
|
||||
teacherDepartmentAssignmentRepository.save(assignment);
|
||||
|
||||
teacher.setDepartmentId(department.getId());
|
||||
userRepository.save(teacher);
|
||||
|
||||
return ResponseEntity.ok(toAssignmentDto(assignment));
|
||||
}
|
||||
|
||||
@GetMapping("/teachers/by-department/{departmentId}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT})
|
||||
public ResponseEntity<?> getTeachersByDepartmentAtDate(@PathVariable Long departmentId,
|
||||
@RequestParam(required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
|
||||
LocalDate effectiveDate = date == null ? LocalDate.now() : date;
|
||||
return ResponseEntity.ok(teacherDepartmentAssignmentRepository
|
||||
.findDepartmentTeachersAtDate(departmentId, effectiveDate)
|
||||
.stream()
|
||||
.map(TeacherDepartmentAssignment::getTeacher)
|
||||
.filter(User::isActiveRecord)
|
||||
.map(user -> new UserResponse(user.getId(), user.getUsername(), user.getRole().name(),
|
||||
user.getFullName(), user.getJobTitle(), user.getDepartmentId()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private TeacherDepartmentAssignmentDto toAssignmentDto(TeacherDepartmentAssignment assignment) {
|
||||
return new TeacherDepartmentAssignmentDto(
|
||||
assignment.getId(),
|
||||
assignment.getTeacher().getId(),
|
||||
assignment.getTeacher().getFullName(),
|
||||
assignment.getDepartment().getId(),
|
||||
assignment.getDepartment().getDepartmentName(),
|
||||
assignment.getValidFrom(),
|
||||
assignment.getValidTo(),
|
||||
assignment.getPrimaryAssignment(),
|
||||
assignment.getComment()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.ClassroomResponse;
|
||||
import com.magistr.app.dto.RenderedLessonDto;
|
||||
import com.magistr.app.dto.WorkloadSummaryDto;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.ClassroomRepository;
|
||||
import com.magistr.app.repository.DepartmentRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import com.magistr.app.service.ScheduleQueryService;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/workload")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public class WorkloadController {
|
||||
|
||||
private static final int ACADEMIC_HOURS_PER_LESSON = 2;
|
||||
|
||||
private final ScheduleQueryService scheduleQueryService;
|
||||
private final ClassroomRepository classroomRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final DepartmentRepository departmentRepository;
|
||||
|
||||
public WorkloadController(ScheduleQueryService scheduleQueryService,
|
||||
ClassroomRepository classroomRepository,
|
||||
UserRepository userRepository,
|
||||
DepartmentRepository departmentRepository) {
|
||||
this.scheduleQueryService = scheduleQueryService;
|
||||
this.classroomRepository = classroomRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.departmentRepository = departmentRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/teachers")
|
||||
public List<WorkloadSummaryDto> teacherWorkload(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
|
||||
@RequestParam(required = false) Long departmentId
|
||||
) {
|
||||
List<RenderedLessonDto> lessons = scheduleQueryService.search(null, null, null, departmentId,
|
||||
null, null, null, null, startDate, endDate);
|
||||
Map<Long, User> teachersById = userRepository.findAllById(lessons.stream()
|
||||
.map(RenderedLessonDto::teacherId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet()))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(User::getId, Function.identity()));
|
||||
return summarize(lessons, RenderedLessonDto::teacherId, RenderedLessonDto::teacherName,
|
||||
teacherId -> {
|
||||
User teacher = teachersById.get(teacherId);
|
||||
return teacher == null ? null : teacher.getDepartmentId();
|
||||
});
|
||||
}
|
||||
|
||||
@GetMapping("/classrooms")
|
||||
public List<WorkloadSummaryDto> classroomWorkload(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
|
||||
@RequestParam(required = false) Long departmentId
|
||||
) {
|
||||
List<RenderedLessonDto> lessons = scheduleQueryService.search(null, null, null, departmentId,
|
||||
null, null, null, null, startDate, endDate);
|
||||
return summarize(lessons, RenderedLessonDto::classroomId, RenderedLessonDto::classroomName, ignored -> null);
|
||||
}
|
||||
|
||||
@GetMapping("/departments")
|
||||
public List<WorkloadSummaryDto> departmentWorkload(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
|
||||
@RequestParam(required = false) Long departmentId
|
||||
) {
|
||||
List<RenderedLessonDto> lessons = scheduleQueryService.search(null, null, null, departmentId,
|
||||
null, null, null, null, startDate, endDate);
|
||||
Map<Long, String> departments = departmentRepository.findAll().stream()
|
||||
.collect(Collectors.toMap(Department::getId, Department::getDepartmentName));
|
||||
Map<Long, User> teachersById = userRepository.findAllById(lessons.stream()
|
||||
.map(RenderedLessonDto::teacherId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet()))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(User::getId, Function.identity()));
|
||||
|
||||
Map<Long, List<RenderedLessonDto>> byDepartment = lessons.stream()
|
||||
.collect(Collectors.groupingBy(lesson -> {
|
||||
User teacher = teachersById.get(lesson.teacherId());
|
||||
return teacher == null ? 0L : teacher.getDepartmentId();
|
||||
}));
|
||||
|
||||
return byDepartment.entrySet().stream()
|
||||
.map(entry -> summary(entry.getKey(), departments.getOrDefault(entry.getKey(), "Кафедра не указана"), entry.getKey(), entry.getValue()))
|
||||
.sorted(Comparator.comparing(WorkloadSummaryDto::name, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping("/time-slots")
|
||||
public List<WorkloadSummaryDto> timeSlotWorkload(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
|
||||
@RequestParam(required = false) Long departmentId
|
||||
) {
|
||||
List<RenderedLessonDto> lessons = scheduleQueryService.search(null, null, null, departmentId,
|
||||
null, null, null, null, startDate, endDate);
|
||||
return summarize(lessons, RenderedLessonDto::timeSlotId,
|
||||
lesson -> lesson.timeSlotOrder() == null
|
||||
? "Пара"
|
||||
: lesson.timeSlotOrder() + " пара (" + lesson.startTime() + " - " + lesson.endTime() + ")",
|
||||
ignored -> null);
|
||||
}
|
||||
|
||||
@GetMapping("/free-classrooms")
|
||||
public List<ClassroomResponse> freeClassrooms(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
|
||||
@RequestParam Long timeSlotId
|
||||
) {
|
||||
Set<Long> busyClassroomIds = scheduleQueryService.search(null, null, null, null,
|
||||
null, null, timeSlotId, null, date, date)
|
||||
.stream()
|
||||
.map(RenderedLessonDto::classroomId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
return classroomRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED).stream()
|
||||
.filter(Classroom::getIsAvailable)
|
||||
.filter(classroom -> !busyClassroomIds.contains(classroom.getId()))
|
||||
.map(this::toClassroomResponse)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<WorkloadSummaryDto> summarize(List<RenderedLessonDto> lessons,
|
||||
Function<RenderedLessonDto, Long> idFn,
|
||||
Function<RenderedLessonDto, String> nameFn,
|
||||
Function<Long, Long> departmentFn) {
|
||||
Map<Long, List<RenderedLessonDto>> grouped = lessons.stream()
|
||||
.filter(lesson -> idFn.apply(lesson) != null)
|
||||
.collect(Collectors.groupingBy(idFn, LinkedHashMap::new, Collectors.toList()));
|
||||
return grouped.entrySet().stream()
|
||||
.map(entry -> summary(entry.getKey(), nameFn.apply(entry.getValue().get(0)), departmentFn.apply(entry.getKey()), entry.getValue()))
|
||||
.sorted(Comparator.comparing(WorkloadSummaryDto::name, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private WorkloadSummaryDto summary(Long id, String name, Long departmentId, List<RenderedLessonDto> lessons) {
|
||||
return new WorkloadSummaryDto(
|
||||
id,
|
||||
name,
|
||||
departmentId,
|
||||
departmentName(departmentId),
|
||||
lessons.size(),
|
||||
lessons.size() * ACADEMIC_HOURS_PER_LESSON,
|
||||
lessons.stream()
|
||||
.map(lesson -> lesson.date() + ":" + lesson.timeSlotId())
|
||||
.distinct()
|
||||
.count()
|
||||
);
|
||||
}
|
||||
|
||||
private String departmentName(Long departmentId) {
|
||||
if (departmentId == null) {
|
||||
return null;
|
||||
}
|
||||
return departmentRepository.findById(departmentId)
|
||||
.map(Department::getDepartmentName)
|
||||
.orElse("Кафедра не найдена");
|
||||
}
|
||||
|
||||
private ClassroomResponse toClassroomResponse(Classroom classroom) {
|
||||
return new ClassroomResponse(
|
||||
classroom.getId(),
|
||||
classroom.getName(),
|
||||
classroom.getCapacity(),
|
||||
classroom.getBuilding(),
|
||||
classroom.getFloor(),
|
||||
classroom.getIsAvailable(),
|
||||
classroom.getStatus(),
|
||||
classroom.getArchivedAt(),
|
||||
classroom.getArchiveReason(),
|
||||
new ArrayList<>(classroom.getEquipments())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
public record AcademicCalendarActivityTypeDto(
|
||||
Long id,
|
||||
String code,
|
||||
String name,
|
||||
Boolean allowSchedule,
|
||||
String colorCode,
|
||||
Integer displayOrder,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record AcademicCalendarDto(
|
||||
Long id,
|
||||
String title,
|
||||
Long academicYearId,
|
||||
String academicYearTitle,
|
||||
LocalDate academicYearStartDate,
|
||||
LocalDate academicYearEndDate,
|
||||
Long specialtyId,
|
||||
String specialtyCode,
|
||||
String specialtyName,
|
||||
Long specialtyProfileId,
|
||||
String specialtyProfileName,
|
||||
Long studyFormId,
|
||||
String studyFormName,
|
||||
Integer courseCount
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record AcademicCalendarGridDayDto(
|
||||
Long id,
|
||||
Long calendarId,
|
||||
Integer courseNumber,
|
||||
LocalDate date,
|
||||
Integer weekNumber,
|
||||
Integer dayOfWeek,
|
||||
Long activityTypeId,
|
||||
String activityCode,
|
||||
String activityName,
|
||||
Boolean allowSchedule
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public record AcademicYearDto(
|
||||
Long id,
|
||||
String title,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
List<SemesterDto> semesters
|
||||
) {
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import java.util.List;
|
||||
public class ClassroomRequest {
|
||||
private String name;
|
||||
private Integer capacity;
|
||||
private String building;
|
||||
private Integer floor;
|
||||
private Boolean isAvailable;
|
||||
private List<Long> equipmentIds;
|
||||
|
||||
@@ -24,6 +26,22 @@ public class ClassroomRequest {
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
public String getBuilding() {
|
||||
return building;
|
||||
}
|
||||
|
||||
public void setBuilding(String building) {
|
||||
this.building = building;
|
||||
}
|
||||
|
||||
public Integer getFloor() {
|
||||
return floor;
|
||||
}
|
||||
|
||||
public void setFloor(Integer floor) {
|
||||
this.floor = floor;
|
||||
}
|
||||
|
||||
public Boolean getIsAvailable() {
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,41 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.Equipment;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public class ClassroomResponse {
|
||||
private Long id;
|
||||
private String name;
|
||||
private Integer capacity;
|
||||
private String building;
|
||||
private Integer floor;
|
||||
private Boolean isAvailable;
|
||||
private String status;
|
||||
private LocalDateTime archivedAt;
|
||||
private String archiveReason;
|
||||
private List<Equipment> equipments;
|
||||
|
||||
public ClassroomResponse() {
|
||||
}
|
||||
|
||||
public ClassroomResponse(Long id, String name, Integer capacity, Boolean isAvailable, List<Equipment> equipments) {
|
||||
public ClassroomResponse(Long id, String name, Integer capacity, String building, Integer floor,
|
||||
Boolean isAvailable, List<Equipment> equipments) {
|
||||
this(id, name, capacity, building, floor, isAvailable, null, null, null, equipments);
|
||||
}
|
||||
|
||||
public ClassroomResponse(Long id, String name, Integer capacity, String building, Integer floor,
|
||||
Boolean isAvailable, String status, LocalDateTime archivedAt,
|
||||
String archiveReason, List<Equipment> equipments) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.capacity = capacity;
|
||||
this.building = building;
|
||||
this.floor = floor;
|
||||
this.isAvailable = isAvailable;
|
||||
this.status = status;
|
||||
this.archivedAt = archivedAt;
|
||||
this.archiveReason = archiveReason;
|
||||
this.equipments = equipments;
|
||||
}
|
||||
|
||||
@@ -45,6 +63,22 @@ public class ClassroomResponse {
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
public String getBuilding() {
|
||||
return building;
|
||||
}
|
||||
|
||||
public void setBuilding(String building) {
|
||||
this.building = building;
|
||||
}
|
||||
|
||||
public Integer getFloor() {
|
||||
return floor;
|
||||
}
|
||||
|
||||
public void setFloor(Integer floor) {
|
||||
this.floor = floor;
|
||||
}
|
||||
|
||||
public Boolean getIsAvailable() {
|
||||
return isAvailable;
|
||||
}
|
||||
@@ -53,6 +87,30 @@ public class ClassroomResponse {
|
||||
this.isAvailable = isAvailable;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getArchivedAt() {
|
||||
return archivedAt;
|
||||
}
|
||||
|
||||
public void setArchivedAt(LocalDateTime archivedAt) {
|
||||
this.archivedAt = archivedAt;
|
||||
}
|
||||
|
||||
public String getArchiveReason() {
|
||||
return archiveReason;
|
||||
}
|
||||
|
||||
public void setArchiveReason(String archiveReason) {
|
||||
this.archiveReason = archiveReason;
|
||||
}
|
||||
|
||||
public List<Equipment> getEquipments() {
|
||||
return equipments;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ public class CreateGroupRequest {
|
||||
private Long educationFormId;
|
||||
private Long departmentId;
|
||||
private Integer yearStartStudy;
|
||||
private Long specialtyId;
|
||||
private Long specialtyProfileId;
|
||||
private Long specialityCode;
|
||||
|
||||
public String getName() {
|
||||
@@ -56,4 +58,24 @@ public class CreateGroupRequest {
|
||||
public void setSpecialityCode(Long specialityCode) {
|
||||
this.specialityCode = specialityCode;
|
||||
}
|
||||
|
||||
public Long getSpecialtyId() {
|
||||
return specialtyId;
|
||||
}
|
||||
|
||||
public void setSpecialtyId(Long specialtyId) {
|
||||
this.specialtyId = specialtyId;
|
||||
}
|
||||
|
||||
public Long getSpecialtyProfileId() {
|
||||
return specialtyProfileId;
|
||||
}
|
||||
|
||||
public void setSpecialtyProfileId(Long specialtyProfileId) {
|
||||
this.specialtyProfileId = specialtyProfileId;
|
||||
}
|
||||
|
||||
public Long getEffectiveSpecialtyId() {
|
||||
return specialtyId != null ? specialtyId : specialityCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
public class CreateLessonRequest {
|
||||
|
||||
private Long teacherId;
|
||||
private Long groupId;
|
||||
private Long subjectId;
|
||||
private String lessonFormat;
|
||||
private String typeLesson;
|
||||
private Long classroomId;
|
||||
private String day;
|
||||
private String week;
|
||||
private String time;
|
||||
|
||||
public CreateLessonRequest() {
|
||||
}
|
||||
|
||||
public Long getTeacherId() {
|
||||
return teacherId;
|
||||
}
|
||||
|
||||
public void setTeacherId(Long teacherId) {
|
||||
this.teacherId = teacherId;
|
||||
}
|
||||
|
||||
public Long getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(Long groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public Long getSubjectId() {
|
||||
return subjectId;
|
||||
}
|
||||
|
||||
public void setSubjectId(Long subjectId) {
|
||||
this.subjectId = subjectId;
|
||||
}
|
||||
|
||||
public String getLessonFormat() {
|
||||
return lessonFormat;
|
||||
}
|
||||
|
||||
public void setLessonFormat(String lessonFormat) {
|
||||
this.lessonFormat = lessonFormat;
|
||||
}
|
||||
|
||||
public String getTypeLesson() {
|
||||
return typeLesson;
|
||||
}
|
||||
|
||||
public void setTypeLesson(String typeLesson) {
|
||||
this.typeLesson = typeLesson;
|
||||
}
|
||||
|
||||
public Long getClassroomId() {
|
||||
return classroomId;
|
||||
}
|
||||
|
||||
public void setClassroomId(Long classroomId) {
|
||||
this.classroomId = classroomId;
|
||||
}
|
||||
|
||||
public String getDay() {
|
||||
return day;
|
||||
}
|
||||
|
||||
public void setDay(String day) {
|
||||
this.day = day;
|
||||
}
|
||||
|
||||
public String getWeek() {
|
||||
return week;
|
||||
}
|
||||
|
||||
public void setWeek(String week) {
|
||||
this.week = week;
|
||||
}
|
||||
|
||||
public String getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public void setTime(String time) {
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.SemesterType;
|
||||
|
||||
public class CreateScheduleDataRequest {
|
||||
private Long id;
|
||||
private Long departmentId;
|
||||
private Long groupId;
|
||||
private Long subjectsId;
|
||||
private Long lessonTypeId;
|
||||
private Long numberOfHours;
|
||||
private Boolean division;
|
||||
private Long teacherId;
|
||||
private SemesterType semesterType;
|
||||
private String period;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getDepartmentId() {
|
||||
return departmentId;
|
||||
}
|
||||
|
||||
public void setDepartmentId(Long departmentId) {
|
||||
this.departmentId = departmentId;
|
||||
}
|
||||
|
||||
public Long getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(Long groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public Long getSubjectsId() {
|
||||
return subjectsId;
|
||||
}
|
||||
|
||||
public void setSubjectsId(Long subjectsId) {
|
||||
this.subjectsId = subjectsId;
|
||||
}
|
||||
|
||||
public Long getLessonTypeId() {
|
||||
return lessonTypeId;
|
||||
}
|
||||
|
||||
public void setLessonTypeId(Long lessonTypeId) {
|
||||
this.lessonTypeId = lessonTypeId;
|
||||
}
|
||||
|
||||
public Long getNumberOfHours() {
|
||||
return numberOfHours;
|
||||
}
|
||||
|
||||
public void setNumberOfHours(Long numberOfHours) {
|
||||
this.numberOfHours = numberOfHours;
|
||||
}
|
||||
|
||||
public Boolean getDivision() {
|
||||
return division;
|
||||
}
|
||||
|
||||
public void setDivision(Boolean division) {
|
||||
this.division = division;
|
||||
}
|
||||
|
||||
public Long getTeacherId() {
|
||||
return teacherId;
|
||||
}
|
||||
|
||||
public void setTeacherId(Long teacherId) {
|
||||
this.teacherId = teacherId;
|
||||
}
|
||||
|
||||
public SemesterType getSemesterType() {
|
||||
return semesterType;
|
||||
}
|
||||
|
||||
public void setSemesterType(SemesterType semesterType) {
|
||||
this.semesterType = semesterType;
|
||||
}
|
||||
|
||||
public String getPeriod() {
|
||||
return period;
|
||||
}
|
||||
|
||||
public void setPeriod(String period) {
|
||||
this.period = period;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record DepartmentTransferRequest(
|
||||
Long departmentId,
|
||||
LocalDate validFrom,
|
||||
String comment
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
public record GroupCalendarAssignmentDto(
|
||||
Long id,
|
||||
Long groupId,
|
||||
String groupName,
|
||||
Long academicYearId,
|
||||
String academicYearTitle,
|
||||
Long calendarId,
|
||||
String calendarTitle
|
||||
) {
|
||||
}
|
||||
@@ -14,21 +14,26 @@ public class GroupResponse {
|
||||
private Integer yearStartStudy;
|
||||
private Integer course;
|
||||
private Integer semester;
|
||||
private Long specialityCode;
|
||||
private Long specialtyId;
|
||||
private String specialtyCode;
|
||||
private String specialtyName;
|
||||
private Long specialtyProfileId;
|
||||
private String specialtyProfileName;
|
||||
|
||||
public GroupResponse(Long id, String name, Long groupSize, Long educationFormId, String educationFormName, Long departmentId, Integer course, Integer semester, Long specialityCode) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.groupSize = groupSize;
|
||||
this.educationFormId = educationFormId;
|
||||
this.educationFormName = educationFormName;
|
||||
this.departmentId = departmentId;
|
||||
this.course = course;
|
||||
this.semester = semester;
|
||||
this.specialityCode = specialityCode;
|
||||
}
|
||||
|
||||
public GroupResponse(Long id, String name, Long groupSize, Long educationFormId, String educationFormName, Long departmentId, Integer yearStartStudy, Long specialityCode) {
|
||||
public GroupResponse(Long id,
|
||||
String name,
|
||||
Long groupSize,
|
||||
Long educationFormId,
|
||||
String educationFormName,
|
||||
Long departmentId,
|
||||
Integer yearStartStudy,
|
||||
Integer course,
|
||||
Integer semester,
|
||||
Long specialtyId,
|
||||
String specialtyCode,
|
||||
String specialtyName,
|
||||
Long specialtyProfileId,
|
||||
String specialtyProfileName) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.groupSize = groupSize;
|
||||
@@ -36,7 +41,13 @@ public class GroupResponse {
|
||||
this.educationFormName = educationFormName;
|
||||
this.departmentId = departmentId;
|
||||
this.yearStartStudy = yearStartStudy;
|
||||
this.specialityCode = specialityCode;
|
||||
this.course = course;
|
||||
this.semester = semester;
|
||||
this.specialtyId = specialtyId;
|
||||
this.specialtyCode = specialtyCode;
|
||||
this.specialtyName = specialtyName;
|
||||
this.specialtyProfileId = specialtyProfileId;
|
||||
this.specialtyProfileName = specialtyProfileName;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
@@ -76,6 +87,26 @@ public class GroupResponse {
|
||||
}
|
||||
|
||||
public Long getSpecialityCode() {
|
||||
return specialityCode;
|
||||
return specialtyId;
|
||||
}
|
||||
|
||||
public Long getSpecialtyId() {
|
||||
return specialtyId;
|
||||
}
|
||||
|
||||
public String getSpecialtyCode() {
|
||||
return specialtyCode;
|
||||
}
|
||||
|
||||
public String getSpecialtyName() {
|
||||
return specialtyName;
|
||||
}
|
||||
|
||||
public Long getSpecialtyProfileId() {
|
||||
return specialtyProfileId;
|
||||
}
|
||||
|
||||
public String getSpecialtyProfileName() {
|
||||
return specialtyProfileName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,171 +0,0 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class LessonResponse {
|
||||
|
||||
private Long id;
|
||||
private Long teacherId;
|
||||
private String teacherName;
|
||||
private Long groupId;
|
||||
private String groupName;
|
||||
private String educationFormName;
|
||||
private Long subjectId;
|
||||
private String subjectName;
|
||||
private String lessonFormat;
|
||||
private String typeLesson;
|
||||
private Long classroomId;
|
||||
private String classroomName;
|
||||
private String day;
|
||||
private String week;
|
||||
private String time;
|
||||
|
||||
public LessonResponse() {
|
||||
}
|
||||
|
||||
public LessonResponse(Long id, Long teacherId, Long groupId, Long subjectId, String day, String week, String time) {
|
||||
this.id = id;
|
||||
this.teacherId = teacherId;
|
||||
this.groupId = groupId;
|
||||
this.subjectId = subjectId;
|
||||
this.day = day;
|
||||
this.week = week;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public LessonResponse(Long id, String teacherName, String groupName, String classroomName, String educationFormName, String subjectName, String typeLesson, String lessonFormat, String day, String week, String time) {
|
||||
this.id = id;
|
||||
this.teacherName = teacherName;
|
||||
this.groupName = groupName;
|
||||
this.classroomName = classroomName;
|
||||
this.educationFormName = educationFormName;
|
||||
this.subjectName = subjectName;
|
||||
this.typeLesson = typeLesson;
|
||||
this.lessonFormat = lessonFormat;
|
||||
this.day = day;
|
||||
this.week = week;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getTeacherId() {
|
||||
return teacherId;
|
||||
}
|
||||
|
||||
public void setTeacherId(Long teacherId) {
|
||||
this.teacherId = teacherId;
|
||||
}
|
||||
|
||||
public String getTeacherName() {
|
||||
return teacherName;
|
||||
}
|
||||
|
||||
public void setTeacherName(String teacherName) {
|
||||
this.teacherName = teacherName;
|
||||
}
|
||||
|
||||
public Long getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(Long groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public String getGroupName() {
|
||||
return groupName;
|
||||
}
|
||||
|
||||
public void setGroupName(String groupName) {
|
||||
this.groupName = groupName;
|
||||
}
|
||||
|
||||
public String getTypeLesson() {
|
||||
return typeLesson;
|
||||
}
|
||||
|
||||
public void setTypeLesson(String typeLesson) {
|
||||
this.typeLesson = typeLesson;
|
||||
}
|
||||
|
||||
public String getLessonFormat() {
|
||||
return lessonFormat;
|
||||
}
|
||||
|
||||
public void setLessonFormat(String lessonFormat) {
|
||||
this.lessonFormat = lessonFormat;
|
||||
}
|
||||
|
||||
public Long getClassroomId() {
|
||||
return classroomId;
|
||||
}
|
||||
|
||||
public void setClassroomId(Long classroomId) {
|
||||
this.classroomId = classroomId;
|
||||
}
|
||||
|
||||
public String getClassroomName() {
|
||||
return classroomName;
|
||||
}
|
||||
|
||||
public void setClassroomName(String classroomName) {
|
||||
this.classroomName = classroomName;
|
||||
}
|
||||
|
||||
public String getEducationFormName() {
|
||||
return educationFormName;
|
||||
}
|
||||
|
||||
public void setEducationFormName(String educationFormName) {
|
||||
this.educationFormName = educationFormName;
|
||||
}
|
||||
|
||||
public Long getSubjectId() {
|
||||
return subjectId;
|
||||
}
|
||||
|
||||
public void setSubjectId(Long subjectId) {
|
||||
this.subjectId = subjectId;
|
||||
}
|
||||
|
||||
public String getSubjectName() {
|
||||
return subjectName;
|
||||
}
|
||||
|
||||
public void setSubjectName(String subjectName) {
|
||||
this.subjectName = subjectName;
|
||||
}
|
||||
|
||||
public String getDay() {
|
||||
return day;
|
||||
}
|
||||
|
||||
public void setDay(String day) {
|
||||
this.day = day;
|
||||
}
|
||||
|
||||
public String getWeek() {
|
||||
return week;
|
||||
}
|
||||
|
||||
public void setWeek(String week) {
|
||||
this.week = week;
|
||||
}
|
||||
|
||||
public String getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public void setTime(String time) {
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
public record LessonTypeResponse(
|
||||
Long id,
|
||||
String name
|
||||
) {
|
||||
}
|
||||
@@ -8,17 +8,23 @@ public class LoginResponse {
|
||||
private String role;
|
||||
private String redirect;
|
||||
private Long departmentId;
|
||||
private Long userId;
|
||||
|
||||
public LoginResponse() {
|
||||
}
|
||||
|
||||
public LoginResponse(boolean success, String message, String token, String role, String redirect, Long departmentId) {
|
||||
this(success, message, token, role, redirect, departmentId, null);
|
||||
}
|
||||
|
||||
public LoginResponse(boolean success, String message, String token, String role, String redirect, Long departmentId, Long userId) {
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
this.token = token;
|
||||
this.role = role;
|
||||
this.redirect = redirect;
|
||||
this.departmentId = departmentId;
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
@@ -68,4 +74,12 @@ public class LoginResponse {
|
||||
public void setDepartmentId(Long departmentId) {
|
||||
this.departmentId = departmentId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.ScheduleParity;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
|
||||
public record RenderedLessonDto(
|
||||
Long scheduleRuleId,
|
||||
Long scheduleRuleSlotId,
|
||||
LocalDate date,
|
||||
Integer dayOfWeek,
|
||||
String dayName,
|
||||
Integer weekNumber,
|
||||
ScheduleParity parity,
|
||||
Long timeSlotId,
|
||||
Integer timeSlotOrder,
|
||||
LocalTime startTime,
|
||||
LocalTime endTime,
|
||||
Long subjectId,
|
||||
String subjectName,
|
||||
Long teacherId,
|
||||
String teacherName,
|
||||
Long classroomId,
|
||||
String classroomName,
|
||||
Long lessonTypeId,
|
||||
String lessonTypeName,
|
||||
String lessonFormat,
|
||||
Long subgroupId,
|
||||
String subgroupName,
|
||||
List<Long> subgroupIds,
|
||||
List<String> subgroupNames,
|
||||
List<Long> groupIds,
|
||||
List<String> groupNames,
|
||||
String activityType,
|
||||
Integer lessonTypeAcademicHours,
|
||||
Integer consumedLessonTypeAcademicHoursBeforeLesson,
|
||||
Integer remainingLessonTypeAcademicHoursAfterLesson
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record ScheduleOverrideDto(
|
||||
Long id,
|
||||
Long baseRuleSlotId,
|
||||
LocalDate lessonDate,
|
||||
String action,
|
||||
Long newTimeSlotId,
|
||||
Long newClassroomId,
|
||||
Long newTeacherId,
|
||||
String newLessonFormat,
|
||||
String comment,
|
||||
Long createdBy,
|
||||
LocalDateTime createdAt
|
||||
) {
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.magistr.app.model.SemesterType;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class ScheduleResponse {
|
||||
private Long id;
|
||||
private String specialityCode;
|
||||
private Long departmentId;
|
||||
private Long groupId;
|
||||
private String groupName;
|
||||
private Integer groupCourse;
|
||||
private Integer groupSemester;
|
||||
private Long subjectsId;
|
||||
private String subjectName;
|
||||
private Long lessonTypeId;
|
||||
private String lessonType;
|
||||
private Long numberOfHours;
|
||||
private Boolean division;
|
||||
private Long teacherId;
|
||||
private String teacherName;
|
||||
private String teacherJobTitle;
|
||||
private SemesterType semesterType;
|
||||
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) {
|
||||
this.id = id;
|
||||
this.departmentId = departmentId;
|
||||
this.groupId = groupId;
|
||||
this.subjectsId = subjectsId;
|
||||
this.lessonTypeId = lessonTypeId;
|
||||
this.numberOfHours = numberOfHours;
|
||||
this.division = division;
|
||||
this.teacherId = teacherId;
|
||||
this.semesterType = semesterType;
|
||||
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) {
|
||||
this.id = id;
|
||||
this.departmentId = departmentId;
|
||||
this.specialityCode = specialityCode;
|
||||
this.groupName = groupName;
|
||||
this.groupCourse = groupCourse;
|
||||
this.groupSemester = groupSemester;
|
||||
this.subjectName = subjectName;
|
||||
this.lessonType = lessonType;
|
||||
this.numberOfHours = numberOfHours;
|
||||
this.division = division;
|
||||
this.teacherName = teacherName;
|
||||
this.teacherJobTitle = teacherJobTitle;
|
||||
this.semesterType = semesterType;
|
||||
this.period = period;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getSpecialityCode() {
|
||||
return specialityCode;
|
||||
}
|
||||
|
||||
public Long getDepartmentId() {
|
||||
return departmentId;
|
||||
}
|
||||
|
||||
public Long getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public String getGroupName() {
|
||||
return groupName;
|
||||
}
|
||||
|
||||
public Integer getGroupCourse() {
|
||||
return groupCourse;
|
||||
}
|
||||
|
||||
public Integer getGroupSemester() {
|
||||
return groupSemester;
|
||||
}
|
||||
|
||||
public Long getSubjectsId() {
|
||||
return subjectsId;
|
||||
}
|
||||
|
||||
public String getSubjectName() {
|
||||
return subjectName;
|
||||
}
|
||||
|
||||
public Long getLessonTypeId() {
|
||||
return lessonTypeId;
|
||||
}
|
||||
|
||||
public String getLessonType() {
|
||||
return lessonType;
|
||||
}
|
||||
|
||||
public Long getNumberOfHours() {
|
||||
return numberOfHours;
|
||||
}
|
||||
|
||||
public Boolean getDivision() {
|
||||
return division;
|
||||
}
|
||||
|
||||
public Long getTeacherId() {
|
||||
return teacherId;
|
||||
}
|
||||
|
||||
public String getTeacherName() {
|
||||
return teacherName;
|
||||
}
|
||||
|
||||
public String getTeacherJobTitle() {
|
||||
return teacherJobTitle;
|
||||
}
|
||||
|
||||
public SemesterType getSemesterType() {
|
||||
return semesterType;
|
||||
}
|
||||
|
||||
public String getPeriod() {
|
||||
return period;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.SemesterType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record ScheduleRuleDto(
|
||||
Long id,
|
||||
Long subjectId,
|
||||
String subjectName,
|
||||
Long semesterId,
|
||||
String academicYearTitle,
|
||||
SemesterType semesterType,
|
||||
Integer lectureAcademicHours,
|
||||
Integer laboratoryAcademicHours,
|
||||
Integer practiceAcademicHours,
|
||||
Integer lectureStartWeek,
|
||||
Integer laboratoryStartWeek,
|
||||
Integer practiceStartWeek,
|
||||
List<Long> groupIds,
|
||||
List<String> groupNames,
|
||||
List<ScheduleRuleSlotDto> slots
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.ScheduleParity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record ScheduleRuleSlotDto(
|
||||
Long id,
|
||||
Integer dayOfWeek,
|
||||
String dayName,
|
||||
ScheduleParity parity,
|
||||
Long timeSlotId,
|
||||
Integer timeSlotOrder,
|
||||
String timeSlotLabel,
|
||||
Long subgroupId,
|
||||
String subgroupName,
|
||||
List<Long> subgroupIds,
|
||||
List<String> subgroupNames,
|
||||
Long teacherId,
|
||||
String teacherName,
|
||||
Long classroomId,
|
||||
String classroomName,
|
||||
Long lessonTypeId,
|
||||
String lessonTypeName,
|
||||
String lessonFormat
|
||||
) {
|
||||
}
|
||||
15
backend/src/main/java/com/magistr/app/dto/SemesterDto.java
Normal file
15
backend/src/main/java/com/magistr/app/dto/SemesterDto.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.SemesterType;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record SemesterDto(
|
||||
Long id,
|
||||
Long academicYearId,
|
||||
String academicYearTitle,
|
||||
SemesterType semesterType,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
public record SpecialtyProfileDto(
|
||||
Long id,
|
||||
Long specialtyId,
|
||||
String specialtyCode,
|
||||
String specialtyName,
|
||||
String name,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
10
backend/src/main/java/com/magistr/app/dto/SubgroupDto.java
Normal file
10
backend/src/main/java/com/magistr/app/dto/SubgroupDto.java
Normal file
@@ -0,0 +1,10 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
public record SubgroupDto(
|
||||
Long id,
|
||||
Long groupId,
|
||||
String groupName,
|
||||
String name,
|
||||
Integer studentCapacity
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record SubjectCommentDto(
|
||||
Long id,
|
||||
Long subjectId,
|
||||
Long authorId,
|
||||
String authorName,
|
||||
String comment,
|
||||
LocalDateTime createdAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record TeacherDepartmentAssignmentDto(
|
||||
Long id,
|
||||
Long teacherId,
|
||||
String teacherName,
|
||||
Long departmentId,
|
||||
String departmentName,
|
||||
LocalDate validFrom,
|
||||
LocalDate validTo,
|
||||
Boolean primaryAssignment,
|
||||
String comment
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record TimeSlotDateAssignmentDto(
|
||||
Long id,
|
||||
LocalDate date,
|
||||
Long scopeId,
|
||||
String scopeName
|
||||
) {
|
||||
}
|
||||
16
backend/src/main/java/com/magistr/app/dto/TimeSlotDto.java
Normal file
16
backend/src/main/java/com/magistr/app/dto/TimeSlotDto.java
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalTime;
|
||||
|
||||
public record TimeSlotDto(
|
||||
Long id,
|
||||
Integer orderNumber,
|
||||
Long scopeId,
|
||||
String scopeName,
|
||||
String scopeApplyMode,
|
||||
Integer scopeDayOfWeek,
|
||||
LocalTime startTime,
|
||||
LocalTime endTime,
|
||||
Integer durationMinutes
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
public record TimeSlotScopeDto(
|
||||
Long id,
|
||||
String code,
|
||||
String name,
|
||||
String applyMode,
|
||||
Integer dayOfWeek,
|
||||
Boolean systemScope,
|
||||
Integer displayOrder
|
||||
) {
|
||||
}
|
||||
@@ -12,6 +12,7 @@ public class UserResponse {
|
||||
private String jobTitle;
|
||||
private String departmentName;
|
||||
private Long departmentId;
|
||||
private String status;
|
||||
|
||||
public UserResponse() {
|
||||
}
|
||||
@@ -69,4 +70,12 @@ public class UserResponse {
|
||||
public Long getDepartmentId() {
|
||||
return departmentId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
public record WorkloadSummaryDto(
|
||||
Long id,
|
||||
String name,
|
||||
Long departmentId,
|
||||
String departmentName,
|
||||
long lessonCount,
|
||||
int academicHours,
|
||||
long occupiedSlotCount
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "academic_calendars")
|
||||
public class AcademicCalendar {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "academic_year_id", nullable = false)
|
||||
private AcademicYear academicYear;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "specialty_id", nullable = false)
|
||||
private Speciality speciality;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "specialty_profile_id", nullable = false)
|
||||
private SpecialtyProfile specialtyProfile;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "study_form_id", nullable = false)
|
||||
private EducationForm studyForm;
|
||||
|
||||
@Column(name = "course_count", nullable = false)
|
||||
private Integer courseCount;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public AcademicYear getAcademicYear() {
|
||||
return academicYear;
|
||||
}
|
||||
|
||||
public void setAcademicYear(AcademicYear academicYear) {
|
||||
this.academicYear = academicYear;
|
||||
}
|
||||
|
||||
public Speciality getSpeciality() {
|
||||
return speciality;
|
||||
}
|
||||
|
||||
public void setSpeciality(Speciality speciality) {
|
||||
this.speciality = speciality;
|
||||
}
|
||||
|
||||
public SpecialtyProfile getSpecialtyProfile() {
|
||||
return specialtyProfile;
|
||||
}
|
||||
|
||||
public void setSpecialtyProfile(SpecialtyProfile specialtyProfile) {
|
||||
this.specialtyProfile = specialtyProfile;
|
||||
}
|
||||
|
||||
public EducationForm getStudyForm() {
|
||||
return studyForm;
|
||||
}
|
||||
|
||||
public void setStudyForm(EducationForm studyForm) {
|
||||
this.studyForm = studyForm;
|
||||
}
|
||||
|
||||
public Integer getCourseCount() {
|
||||
return courseCount;
|
||||
}
|
||||
|
||||
public void setCourseCount(Integer courseCount) {
|
||||
this.courseCount = courseCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "academic_calendar_activity_types")
|
||||
public class AcademicCalendarActivityType {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 10)
|
||||
private String code;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "allow_schedule", nullable = false)
|
||||
private Boolean allowSchedule;
|
||||
|
||||
@Column(name = "color_code", nullable = false, length = 7)
|
||||
private String colorCode;
|
||||
|
||||
@Column(name = "display_order", nullable = false)
|
||||
private Integer displayOrder;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Boolean getAllowSchedule() {
|
||||
return allowSchedule;
|
||||
}
|
||||
|
||||
public void setAllowSchedule(Boolean allowSchedule) {
|
||||
this.allowSchedule = allowSchedule;
|
||||
}
|
||||
|
||||
public String getColorCode() {
|
||||
return colorCode;
|
||||
}
|
||||
|
||||
public void setColorCode(String colorCode) {
|
||||
this.colorCode = colorCode;
|
||||
}
|
||||
|
||||
public Integer getDisplayOrder() {
|
||||
return displayOrder;
|
||||
}
|
||||
|
||||
public void setDisplayOrder(Integer displayOrder) {
|
||||
this.displayOrder = displayOrder;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "academic_calendar_days")
|
||||
public class AcademicCalendarDay {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "calendar_id", nullable = false)
|
||||
private AcademicCalendar academicCalendar;
|
||||
|
||||
@Column(name = "course_number", nullable = false)
|
||||
private Integer courseNumber;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDate date;
|
||||
|
||||
@Column(name = "week_number", nullable = false)
|
||||
private Integer weekNumber;
|
||||
|
||||
@Column(name = "day_of_week", nullable = false)
|
||||
private Integer dayOfWeek;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "activity_type_id", nullable = false)
|
||||
private AcademicCalendarActivityType activityType;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public AcademicCalendar getAcademicCalendar() {
|
||||
return academicCalendar;
|
||||
}
|
||||
|
||||
public void setAcademicCalendar(AcademicCalendar academicCalendar) {
|
||||
this.academicCalendar = academicCalendar;
|
||||
}
|
||||
|
||||
public Integer getCourseNumber() {
|
||||
return courseNumber;
|
||||
}
|
||||
|
||||
public void setCourseNumber(Integer courseNumber) {
|
||||
this.courseNumber = courseNumber;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public Integer getWeekNumber() {
|
||||
return weekNumber;
|
||||
}
|
||||
|
||||
public void setWeekNumber(Integer weekNumber) {
|
||||
this.weekNumber = weekNumber;
|
||||
}
|
||||
|
||||
public Integer getDayOfWeek() {
|
||||
return dayOfWeek;
|
||||
}
|
||||
|
||||
public void setDayOfWeek(Integer dayOfWeek) {
|
||||
this.dayOfWeek = dayOfWeek;
|
||||
}
|
||||
|
||||
public AcademicCalendarActivityType getActivityType() {
|
||||
return activityType;
|
||||
}
|
||||
|
||||
public void setActivityType(AcademicCalendarActivityType activityType) {
|
||||
this.activityType = activityType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "academic_years")
|
||||
public class AcademicYear {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 20)
|
||||
private String title;
|
||||
|
||||
@Column(name = "start_date", nullable = false)
|
||||
private LocalDate startDate;
|
||||
|
||||
@Column(name = "end_date", nullable = false)
|
||||
private LocalDate endDate;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public LocalDate getStartDate() {
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setStartDate(LocalDate startDate) {
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public LocalDate getEndDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setEndDate(LocalDate endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "classrooms")
|
||||
public class Classroom {
|
||||
public class Classroom extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@@ -18,6 +18,11 @@ public class Classroom {
|
||||
@Column(nullable = false)
|
||||
private Integer capacity;
|
||||
|
||||
@Column(length = 50)
|
||||
private String building;
|
||||
|
||||
private Integer floor;
|
||||
|
||||
@Column(name = "is_available", nullable = false)
|
||||
private Boolean isAvailable = true;
|
||||
|
||||
@@ -52,6 +57,22 @@ public class Classroom {
|
||||
this.capacity = capacity;
|
||||
}
|
||||
|
||||
public String getBuilding() {
|
||||
return building;
|
||||
}
|
||||
|
||||
public void setBuilding(String building) {
|
||||
this.building = building;
|
||||
}
|
||||
|
||||
public Integer getFloor() {
|
||||
return floor;
|
||||
}
|
||||
|
||||
public void setFloor(Integer floor) {
|
||||
this.floor = floor;
|
||||
}
|
||||
|
||||
public Boolean getIsAvailable() {
|
||||
return isAvailable;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name="departments")
|
||||
public class Department {
|
||||
public class Department extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "education_forms")
|
||||
public class EducationForm {
|
||||
public class EducationForm extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "equipments")
|
||||
public class Equipment {
|
||||
public class Equipment extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "lessons")
|
||||
public class Lesson {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "teacher_id", nullable = false)
|
||||
private Long teacherId;
|
||||
|
||||
@Column(name = "group_id", nullable = false)
|
||||
private Long groupId;
|
||||
|
||||
@Column(name = "subject_id", nullable = false)
|
||||
private Long subjectId;
|
||||
|
||||
@Column(name = "lesson_format", nullable = false, length = 255)
|
||||
private String lessonFormat;
|
||||
|
||||
@Column(name = "type_lesson", nullable = false, length = 255)
|
||||
private String typeLesson;
|
||||
|
||||
@Column(name = "classroom_id", nullable = false)
|
||||
private Long classroomId;
|
||||
|
||||
@Column(name = "day", nullable = false, length = 255)
|
||||
private String day;
|
||||
|
||||
@Column(name = "week", nullable = false, length = 255)
|
||||
private String week;
|
||||
|
||||
@Column(name = "time", nullable = false, length = 255)
|
||||
private String time;
|
||||
|
||||
public Lesson() {
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getTeacherId() {
|
||||
return teacherId;
|
||||
}
|
||||
|
||||
public void setTeacherId(Long teacherId) {
|
||||
this.teacherId = teacherId;
|
||||
}
|
||||
|
||||
public Long getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(Long groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public Long getSubjectId() {
|
||||
return subjectId;
|
||||
}
|
||||
|
||||
public void setSubjectId(Long subjectId) {
|
||||
this.subjectId = subjectId;
|
||||
}
|
||||
|
||||
public String getLessonFormat() {
|
||||
return lessonFormat;
|
||||
}
|
||||
|
||||
public void setLessonFormat(String lessonFormat) {
|
||||
this.lessonFormat = lessonFormat;
|
||||
}
|
||||
|
||||
public String getTypeLesson() {
|
||||
return typeLesson;
|
||||
}
|
||||
|
||||
public void setTypeLesson(String typeLesson) {
|
||||
this.typeLesson = typeLesson;
|
||||
}
|
||||
|
||||
public Long getClassroomId() {
|
||||
return classroomId;
|
||||
}
|
||||
|
||||
public void setClassroomId(Long classroomId) {
|
||||
this.classroomId = classroomId;
|
||||
}
|
||||
|
||||
public String getDay() {
|
||||
return day;
|
||||
}
|
||||
|
||||
public void setDay(String day) {
|
||||
this.day = day;
|
||||
}
|
||||
|
||||
public String getWeek() {
|
||||
return week;
|
||||
}
|
||||
|
||||
public void setWeek(String week) {
|
||||
this.week = week;
|
||||
}
|
||||
|
||||
public String getTime() {
|
||||
return time;
|
||||
}
|
||||
|
||||
public void setTime(String time) {
|
||||
this.time = time;
|
||||
}
|
||||
}
|
||||
104
backend/src/main/java/com/magistr/app/model/LifecycleEntity.java
Normal file
104
backend/src/main/java/com/magistr/app/model/LifecycleEntity.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@MappedSuperclass
|
||||
public abstract class LifecycleEntity {
|
||||
|
||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||
public static final String STATUS_ARCHIVED = "ARCHIVED";
|
||||
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status = STATUS_ACTIVE;
|
||||
|
||||
@Column(name = "active_from")
|
||||
private LocalDate activeFrom;
|
||||
|
||||
@Column(name = "active_to")
|
||||
private LocalDate activeTo;
|
||||
|
||||
@Column(name = "archived_at")
|
||||
private LocalDateTime archivedAt;
|
||||
|
||||
@Column(name = "archive_reason")
|
||||
private String archiveReason;
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status == null || status.isBlank() ? STATUS_ACTIVE : status;
|
||||
}
|
||||
|
||||
public LocalDate getActiveFrom() {
|
||||
return activeFrom;
|
||||
}
|
||||
|
||||
public void setActiveFrom(LocalDate activeFrom) {
|
||||
this.activeFrom = activeFrom;
|
||||
}
|
||||
|
||||
public LocalDate getActiveTo() {
|
||||
return activeTo;
|
||||
}
|
||||
|
||||
public void setActiveTo(LocalDate activeTo) {
|
||||
this.activeTo = activeTo;
|
||||
}
|
||||
|
||||
public LocalDateTime getArchivedAt() {
|
||||
return archivedAt;
|
||||
}
|
||||
|
||||
public void setArchivedAt(LocalDateTime archivedAt) {
|
||||
this.archivedAt = archivedAt;
|
||||
}
|
||||
|
||||
public String getArchiveReason() {
|
||||
return archiveReason;
|
||||
}
|
||||
|
||||
public void setArchiveReason(String archiveReason) {
|
||||
this.archiveReason = archiveReason;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isActiveRecord() {
|
||||
return !STATUS_ARCHIVED.equals(status);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isArchivedRecord() {
|
||||
return STATUS_ARCHIVED.equals(status);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isActiveOn(LocalDate date) {
|
||||
if (date == null) {
|
||||
return isActiveRecord();
|
||||
}
|
||||
boolean afterStart = activeFrom == null || !date.isBefore(activeFrom);
|
||||
boolean beforeEnd = activeTo == null || !date.isAfter(activeTo);
|
||||
return afterStart && beforeEnd;
|
||||
}
|
||||
|
||||
public void archive(String reason) {
|
||||
status = STATUS_ARCHIVED;
|
||||
archivedAt = LocalDateTime.now();
|
||||
activeTo = LocalDate.now();
|
||||
archiveReason = reason == null || reason.isBlank() ? "Архивировано пользователем" : reason.trim();
|
||||
}
|
||||
|
||||
public void restore() {
|
||||
status = STATUS_ACTIVE;
|
||||
archivedAt = null;
|
||||
archiveReason = null;
|
||||
activeTo = null;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package com.magistr.app.model;
|
||||
|
||||
public enum Role {
|
||||
ADMIN,
|
||||
EDUCATION_OFFICE,
|
||||
DEPARTMENT,
|
||||
SCHEDULE_VIEWER,
|
||||
TEACHER,
|
||||
STUDENT
|
||||
}
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name="schedule_data")
|
||||
public class ScheduleData {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name="department_id", nullable = false)
|
||||
private Long departmentId;
|
||||
|
||||
@Column(name="group_id", nullable = false)
|
||||
private Long groupId;
|
||||
|
||||
@Column(name="subjects_id", nullable = false)
|
||||
private Long subjectsId;
|
||||
|
||||
@Column(name="lesson_type_id", nullable = false)
|
||||
private Long lessonTypeId;
|
||||
|
||||
@Column(name="number_of_hours", nullable = false)
|
||||
private Long numberOfHours;
|
||||
|
||||
@Column(name="is_division", nullable = false)
|
||||
private Boolean division;
|
||||
|
||||
@Column(name="teacher_id", nullable = false)
|
||||
private Long teacherId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name="semester_type", nullable = false)
|
||||
private SemesterType semesterType;
|
||||
|
||||
@Column(name="period", nullable = false)
|
||||
private String period;
|
||||
|
||||
public ScheduleData() {}
|
||||
|
||||
public ScheduleData(Long id, Long departmentId, Long groupId, Long subjectsId, Long lessonTypeId, Long numberOfHours, Boolean division, Long teacherId, SemesterType semesterType, String period) {
|
||||
this.id = id;
|
||||
this.departmentId = departmentId;
|
||||
this.groupId = groupId;
|
||||
this.subjectsId = subjectsId;
|
||||
this.lessonTypeId = lessonTypeId;
|
||||
this.numberOfHours = numberOfHours;
|
||||
this.division = division;
|
||||
this.teacherId = teacherId;
|
||||
this.semesterType = semesterType;
|
||||
this.period = period;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Long getDepartmentId() {
|
||||
return departmentId;
|
||||
}
|
||||
|
||||
public void setDepartmentId(Long departmentId) {
|
||||
this.departmentId = departmentId;
|
||||
}
|
||||
|
||||
public Long getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public void setGroupId(Long groupId) {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public Long getSubjectsId() {
|
||||
return subjectsId;
|
||||
}
|
||||
|
||||
public void setSubjectsId(Long subjectsId) {
|
||||
this.subjectsId = subjectsId;
|
||||
}
|
||||
|
||||
public Long getLessonTypeId() {
|
||||
return lessonTypeId;
|
||||
}
|
||||
|
||||
public void setLessonTypeId(Long lessonTypeId) {
|
||||
this.lessonTypeId = lessonTypeId;
|
||||
}
|
||||
|
||||
public Long getNumberOfHours() {
|
||||
return numberOfHours;
|
||||
}
|
||||
|
||||
public void setNumberOfHours(Long numberOfHours) {
|
||||
this.numberOfHours = numberOfHours;
|
||||
}
|
||||
|
||||
public Boolean getDivision() {
|
||||
return division;
|
||||
}
|
||||
|
||||
public void setDivision(Boolean division) {
|
||||
this.division = division;
|
||||
}
|
||||
|
||||
public Long getTeacherId() {
|
||||
return teacherId;
|
||||
}
|
||||
|
||||
public void setTeacherId(Long teacherId) {
|
||||
this.teacherId = teacherId;
|
||||
}
|
||||
|
||||
public SemesterType getSemesterType() {
|
||||
return semesterType;
|
||||
}
|
||||
|
||||
public void setSemesterType(SemesterType semesterType) {
|
||||
this.semesterType = semesterType;
|
||||
}
|
||||
|
||||
public String getPeriod() {
|
||||
return period;
|
||||
}
|
||||
|
||||
public void setPeriod(String period) {
|
||||
this.period = period;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public enum ScheduleLessonCategory {
|
||||
LECTURE("Лекции"),
|
||||
LABORATORY("Лабораторные"),
|
||||
PRACTICE("Практики");
|
||||
|
||||
private final String displayName;
|
||||
|
||||
ScheduleLessonCategory(String displayName) {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public static ScheduleLessonCategory fromLessonType(LessonType lessonType) {
|
||||
if (lessonType == null || lessonType.getLessonType() == null) {
|
||||
throw new IllegalArgumentException("Тип занятия должен быть лекцией, практикой или лабораторной работой");
|
||||
}
|
||||
String name = lessonType.getLessonType().toLowerCase(Locale.forLanguageTag("ru-RU"));
|
||||
if (name.contains("лекц")) {
|
||||
return LECTURE;
|
||||
}
|
||||
if (name.contains("лаб")) {
|
||||
return LABORATORY;
|
||||
}
|
||||
if (name.contains("практ")) {
|
||||
return PRACTICE;
|
||||
}
|
||||
throw new IllegalArgumentException("Тип занятия должен быть лекцией, практикой или лабораторной работой");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "schedule_overrides")
|
||||
public class ScheduleOverride {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "base_rule_slot_id", nullable = false)
|
||||
private ScheduleRuleSlot baseRuleSlot;
|
||||
|
||||
@Column(name = "lesson_date", nullable = false)
|
||||
private LocalDate lessonDate;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String action;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "new_time_slot_id")
|
||||
private TimeSlot newTimeSlot;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "new_classroom_id")
|
||||
private Classroom newClassroom;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "new_teacher_id")
|
||||
private User newTeacher;
|
||||
|
||||
@Column(name = "new_lesson_format", length = 30)
|
||||
private String newLessonFormat;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String comment;
|
||||
|
||||
@Column(name = "created_by")
|
||||
private Long createdBy;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ScheduleRuleSlot getBaseRuleSlot() {
|
||||
return baseRuleSlot;
|
||||
}
|
||||
|
||||
public void setBaseRuleSlot(ScheduleRuleSlot baseRuleSlot) {
|
||||
this.baseRuleSlot = baseRuleSlot;
|
||||
}
|
||||
|
||||
public LocalDate getLessonDate() {
|
||||
return lessonDate;
|
||||
}
|
||||
|
||||
public void setLessonDate(LocalDate lessonDate) {
|
||||
this.lessonDate = lessonDate;
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public void setAction(String action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public TimeSlot getNewTimeSlot() {
|
||||
return newTimeSlot;
|
||||
}
|
||||
|
||||
public void setNewTimeSlot(TimeSlot newTimeSlot) {
|
||||
this.newTimeSlot = newTimeSlot;
|
||||
}
|
||||
|
||||
public Classroom getNewClassroom() {
|
||||
return newClassroom;
|
||||
}
|
||||
|
||||
public void setNewClassroom(Classroom newClassroom) {
|
||||
this.newClassroom = newClassroom;
|
||||
}
|
||||
|
||||
public User getNewTeacher() {
|
||||
return newTeacher;
|
||||
}
|
||||
|
||||
public void setNewTeacher(User newTeacher) {
|
||||
this.newTeacher = newTeacher;
|
||||
}
|
||||
|
||||
public String getNewLessonFormat() {
|
||||
return newLessonFormat;
|
||||
}
|
||||
|
||||
public void setNewLessonFormat(String newLessonFormat) {
|
||||
this.newLessonFormat = newLessonFormat;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
public void setComment(String comment) {
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public Long getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(Long createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
public enum ScheduleParity {
|
||||
BOTH,
|
||||
EVEN,
|
||||
ODD
|
||||
}
|
||||
198
backend/src/main/java/com/magistr/app/model/ScheduleRule.java
Normal file
198
backend/src/main/java/com/magistr/app/model/ScheduleRule.java
Normal file
@@ -0,0 +1,198 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "schedule_rules")
|
||||
public class ScheduleRule {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "subject_id", nullable = false)
|
||||
private Subject subject;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "semester_id", nullable = false)
|
||||
private Semester semester;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String status = LifecycleEntity.STATUS_ACTIVE;
|
||||
|
||||
@Column(name = "valid_from")
|
||||
private LocalDate validFrom;
|
||||
|
||||
@Column(name = "valid_to")
|
||||
private LocalDate validTo;
|
||||
|
||||
@Column(name = "lecture_academic_hours", nullable = false)
|
||||
private Integer lectureAcademicHours = 0;
|
||||
|
||||
@Column(name = "laboratory_academic_hours", nullable = false)
|
||||
private Integer laboratoryAcademicHours = 0;
|
||||
|
||||
@Column(name = "practice_academic_hours", nullable = false)
|
||||
private Integer practiceAcademicHours = 0;
|
||||
|
||||
@Column(name = "lecture_start_week", nullable = false)
|
||||
private Integer lectureStartWeek = 1;
|
||||
|
||||
@Column(name = "laboratory_start_week", nullable = false)
|
||||
private Integer laboratoryStartWeek = 1;
|
||||
|
||||
@Column(name = "practice_start_week", nullable = false)
|
||||
private Integer practiceStartWeek = 1;
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "schedule_rule_groups",
|
||||
joinColumns = @JoinColumn(name = "schedule_rule_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "group_id")
|
||||
)
|
||||
private Set<StudentGroup> groups = new HashSet<>();
|
||||
|
||||
@OneToMany(mappedBy = "scheduleRule", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private Set<ScheduleRuleSlot> slots = new HashSet<>();
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Subject getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public void setSubject(Subject subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public Semester getSemester() {
|
||||
return semester;
|
||||
}
|
||||
|
||||
public void setSemester(Semester semester) {
|
||||
this.semester = semester;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDate getValidFrom() {
|
||||
return validFrom;
|
||||
}
|
||||
|
||||
public void setValidFrom(LocalDate validFrom) {
|
||||
this.validFrom = validFrom;
|
||||
}
|
||||
|
||||
public LocalDate getValidTo() {
|
||||
return validTo;
|
||||
}
|
||||
|
||||
public void setValidTo(LocalDate validTo) {
|
||||
this.validTo = validTo;
|
||||
}
|
||||
|
||||
public Integer getLectureAcademicHours() {
|
||||
return lectureAcademicHours;
|
||||
}
|
||||
|
||||
public void setLectureAcademicHours(Integer lectureAcademicHours) {
|
||||
this.lectureAcademicHours = lectureAcademicHours;
|
||||
}
|
||||
|
||||
public Integer getLaboratoryAcademicHours() {
|
||||
return laboratoryAcademicHours;
|
||||
}
|
||||
|
||||
public void setLaboratoryAcademicHours(Integer laboratoryAcademicHours) {
|
||||
this.laboratoryAcademicHours = laboratoryAcademicHours;
|
||||
}
|
||||
|
||||
public Integer getPracticeAcademicHours() {
|
||||
return practiceAcademicHours;
|
||||
}
|
||||
|
||||
public void setPracticeAcademicHours(Integer practiceAcademicHours) {
|
||||
this.practiceAcademicHours = practiceAcademicHours;
|
||||
}
|
||||
|
||||
public Integer getLectureStartWeek() {
|
||||
return lectureStartWeek;
|
||||
}
|
||||
|
||||
public void setLectureStartWeek(Integer lectureStartWeek) {
|
||||
this.lectureStartWeek = lectureStartWeek;
|
||||
}
|
||||
|
||||
public Integer getLaboratoryStartWeek() {
|
||||
return laboratoryStartWeek;
|
||||
}
|
||||
|
||||
public void setLaboratoryStartWeek(Integer laboratoryStartWeek) {
|
||||
this.laboratoryStartWeek = laboratoryStartWeek;
|
||||
}
|
||||
|
||||
public Integer getPracticeStartWeek() {
|
||||
return practiceStartWeek;
|
||||
}
|
||||
|
||||
public void setPracticeStartWeek(Integer practiceStartWeek) {
|
||||
this.practiceStartWeek = practiceStartWeek;
|
||||
}
|
||||
|
||||
public int academicHoursFor(ScheduleLessonCategory category) {
|
||||
return switch (category) {
|
||||
case LECTURE -> valueOrZero(lectureAcademicHours);
|
||||
case LABORATORY -> valueOrZero(laboratoryAcademicHours);
|
||||
case PRACTICE -> valueOrZero(practiceAcademicHours);
|
||||
};
|
||||
}
|
||||
|
||||
public int startWeekFor(ScheduleLessonCategory category) {
|
||||
return switch (category) {
|
||||
case LECTURE -> valueOrDefault(lectureStartWeek, 1);
|
||||
case LABORATORY -> valueOrDefault(laboratoryStartWeek, 1);
|
||||
case PRACTICE -> valueOrDefault(practiceStartWeek, 1);
|
||||
};
|
||||
}
|
||||
|
||||
private int valueOrZero(Integer value) {
|
||||
return valueOrDefault(value, 0);
|
||||
}
|
||||
|
||||
private int valueOrDefault(Integer value, int defaultValue) {
|
||||
return value == null ? defaultValue : value;
|
||||
}
|
||||
|
||||
public Set<StudentGroup> getGroups() {
|
||||
return groups;
|
||||
}
|
||||
|
||||
public void setGroups(Set<StudentGroup> groups) {
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
public Set<ScheduleRuleSlot> getSlots() {
|
||||
return slots;
|
||||
}
|
||||
|
||||
public void setSlots(Set<ScheduleRuleSlot> slots) {
|
||||
this.slots = slots;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "schedule_rule_slots")
|
||||
public class ScheduleRuleSlot {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "schedule_rule_id", nullable = false)
|
||||
private ScheduleRule scheduleRule;
|
||||
|
||||
@Column(name = "day_of_week", nullable = false)
|
||||
private Integer dayOfWeek;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 10)
|
||||
private ScheduleParity parity;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "time_slot_id", nullable = false)
|
||||
private TimeSlot timeSlot;
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "schedule_rule_slot_subgroups",
|
||||
joinColumns = @JoinColumn(name = "schedule_rule_slot_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "subgroup_id")
|
||||
)
|
||||
private Set<Subgroup> subgroups = new LinkedHashSet<>();
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "teacher_id", nullable = false)
|
||||
private User teacher;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "classroom_id", nullable = false)
|
||||
private Classroom classroom;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "lesson_type_id", nullable = false)
|
||||
private LessonType lessonType;
|
||||
|
||||
@Column(name = "lesson_format", nullable = false, length = 30)
|
||||
private String lessonFormat;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public ScheduleRule getScheduleRule() {
|
||||
return scheduleRule;
|
||||
}
|
||||
|
||||
public void setScheduleRule(ScheduleRule scheduleRule) {
|
||||
this.scheduleRule = scheduleRule;
|
||||
}
|
||||
|
||||
public Integer getDayOfWeek() {
|
||||
return dayOfWeek;
|
||||
}
|
||||
|
||||
public void setDayOfWeek(Integer dayOfWeek) {
|
||||
this.dayOfWeek = dayOfWeek;
|
||||
}
|
||||
|
||||
public ScheduleParity getParity() {
|
||||
return parity;
|
||||
}
|
||||
|
||||
public void setParity(ScheduleParity parity) {
|
||||
this.parity = parity;
|
||||
}
|
||||
|
||||
public TimeSlot getTimeSlot() {
|
||||
return timeSlot;
|
||||
}
|
||||
|
||||
public void setTimeSlot(TimeSlot timeSlot) {
|
||||
this.timeSlot = timeSlot;
|
||||
}
|
||||
|
||||
public Long getSubgroupId() {
|
||||
return subgroups.stream().findFirst().map(Subgroup::getId).orElse(null);
|
||||
}
|
||||
|
||||
public Subgroup getSubgroup() {
|
||||
return subgroups.stream().findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public void setSubgroup(Subgroup subgroup) {
|
||||
subgroups.clear();
|
||||
if (subgroup != null) {
|
||||
subgroups.add(subgroup);
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Subgroup> getSubgroups() {
|
||||
return subgroups;
|
||||
}
|
||||
|
||||
public void setSubgroups(Set<Subgroup> subgroups) {
|
||||
this.subgroups.clear();
|
||||
if (subgroups != null) {
|
||||
this.subgroups.addAll(subgroups);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Long> getSubgroupIds() {
|
||||
return subgroups.stream()
|
||||
.map(Subgroup::getId)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<String> getSubgroupNames() {
|
||||
return subgroups.stream()
|
||||
.map(Subgroup::getName)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public User getTeacher() {
|
||||
return teacher;
|
||||
}
|
||||
|
||||
public void setTeacher(User teacher) {
|
||||
this.teacher = teacher;
|
||||
}
|
||||
|
||||
public Classroom getClassroom() {
|
||||
return classroom;
|
||||
}
|
||||
|
||||
public void setClassroom(Classroom classroom) {
|
||||
this.classroom = classroom;
|
||||
}
|
||||
|
||||
public LessonType getLessonType() {
|
||||
return lessonType;
|
||||
}
|
||||
|
||||
public void setLessonType(LessonType lessonType) {
|
||||
this.lessonType = lessonType;
|
||||
}
|
||||
|
||||
public String getLessonFormat() {
|
||||
return lessonFormat;
|
||||
}
|
||||
|
||||
public void setLessonFormat(String lessonFormat) {
|
||||
this.lessonFormat = lessonFormat;
|
||||
}
|
||||
}
|
||||
68
backend/src/main/java/com/magistr/app/model/Semester.java
Normal file
68
backend/src/main/java/com/magistr/app/model/Semester.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "semesters")
|
||||
public class Semester {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "academic_year_id", nullable = false)
|
||||
private AcademicYear academicYear;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "semester_type", nullable = false, length = 20)
|
||||
private SemesterType semesterType;
|
||||
|
||||
@Column(name = "start_date", nullable = false)
|
||||
private LocalDate startDate;
|
||||
|
||||
@Column(name = "end_date", nullable = false)
|
||||
private LocalDate endDate;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public AcademicYear getAcademicYear() {
|
||||
return academicYear;
|
||||
}
|
||||
|
||||
public void setAcademicYear(AcademicYear academicYear) {
|
||||
this.academicYear = academicYear;
|
||||
}
|
||||
|
||||
public SemesterType getSemesterType() {
|
||||
return semesterType;
|
||||
}
|
||||
|
||||
public void setSemesterType(SemesterType semesterType) {
|
||||
this.semesterType = semesterType;
|
||||
}
|
||||
|
||||
public LocalDate getStartDate() {
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setStartDate(LocalDate startDate) {
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public LocalDate getEndDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setEndDate(LocalDate endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name="specialties")
|
||||
public class Speciality {
|
||||
public class Speciality extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "specialty_profiles")
|
||||
public class SpecialtyProfile extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "specialty_id", nullable = false)
|
||||
private Speciality speciality;
|
||||
|
||||
@Column(nullable = false, length = 500)
|
||||
private String name;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Speciality getSpeciality() {
|
||||
return speciality;
|
||||
}
|
||||
|
||||
public void setSpeciality(Speciality speciality) {
|
||||
this.speciality = speciality;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,13 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "student_groups")
|
||||
public class StudentGroup {
|
||||
public class StudentGroup extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(unique = true, nullable = false, length = 100)
|
||||
@Column(nullable = false, length = 100)
|
||||
private String name;
|
||||
|
||||
@Column(name = "group_size", nullable = false)
|
||||
@@ -23,8 +23,13 @@ public class StudentGroup {
|
||||
@Column(name = "department_id", nullable = false)
|
||||
private Long departmentId;
|
||||
|
||||
@Column(name="specialty_code", nullable = false)
|
||||
private Long specialityCode;
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "specialty_id", nullable = false)
|
||||
private Speciality speciality;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "specialty_profile_id", nullable = false)
|
||||
private SpecialtyProfile specialtyProfile;
|
||||
|
||||
@Column(name="year_start_study", nullable = false)
|
||||
private Integer yearStartStudy;
|
||||
@@ -72,12 +77,20 @@ public class StudentGroup {
|
||||
this.departmentId = departmentId;
|
||||
}
|
||||
|
||||
public Long getSpecialityCode() {
|
||||
return specialityCode;
|
||||
public Speciality getSpeciality() {
|
||||
return speciality;
|
||||
}
|
||||
|
||||
public void setSpecialityCode(Long specialityCode) {
|
||||
this.specialityCode = specialityCode;
|
||||
public void setSpeciality(Speciality speciality) {
|
||||
this.speciality = speciality;
|
||||
}
|
||||
|
||||
public SpecialtyProfile getSpecialtyProfile() {
|
||||
return specialtyProfile;
|
||||
}
|
||||
|
||||
public void setSpecialtyProfile(SpecialtyProfile specialtyProfile) {
|
||||
this.specialtyProfile = specialtyProfile;
|
||||
}
|
||||
|
||||
public Integer getYearStartStudy() {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "student_group_calendar_assignments")
|
||||
public class StudentGroupCalendarAssignment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "group_id", nullable = false)
|
||||
private StudentGroup studentGroup;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "academic_year_id", nullable = false)
|
||||
private AcademicYear academicYear;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "calendar_id", nullable = false)
|
||||
private AcademicCalendar academicCalendar;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public StudentGroup getStudentGroup() {
|
||||
return studentGroup;
|
||||
}
|
||||
|
||||
public void setStudentGroup(StudentGroup studentGroup) {
|
||||
this.studentGroup = studentGroup;
|
||||
}
|
||||
|
||||
public AcademicYear getAcademicYear() {
|
||||
return academicYear;
|
||||
}
|
||||
|
||||
public void setAcademicYear(AcademicYear academicYear) {
|
||||
this.academicYear = academicYear;
|
||||
}
|
||||
|
||||
public AcademicCalendar getAcademicCalendar() {
|
||||
return academicCalendar;
|
||||
}
|
||||
|
||||
public void setAcademicCalendar(AcademicCalendar academicCalendar) {
|
||||
this.academicCalendar = academicCalendar;
|
||||
}
|
||||
}
|
||||
54
backend/src/main/java/com/magistr/app/model/Subgroup.java
Normal file
54
backend/src/main/java/com/magistr/app/model/Subgroup.java
Normal file
@@ -0,0 +1,54 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "subgroups")
|
||||
public class Subgroup extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "group_id", nullable = false)
|
||||
private StudentGroup studentGroup;
|
||||
|
||||
@Column(nullable = false, length = 100)
|
||||
private String name;
|
||||
|
||||
@Column(name = "student_capacity")
|
||||
private Integer studentCapacity;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public StudentGroup getStudentGroup() {
|
||||
return studentGroup;
|
||||
}
|
||||
|
||||
public void setStudentGroup(StudentGroup studentGroup) {
|
||||
this.studentGroup = studentGroup;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Integer getStudentCapacity() {
|
||||
return studentCapacity;
|
||||
}
|
||||
|
||||
public void setStudentCapacity(Integer studentCapacity) {
|
||||
this.studentCapacity = studentCapacity;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "subjects")
|
||||
public class Subject {
|
||||
public class Subject extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "subject_comments")
|
||||
public class SubjectComment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "subject_id", nullable = false)
|
||||
private Subject subject;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "author_id")
|
||||
private User author;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String comment;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Subject getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public void setSubject(Subject subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public User getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(User author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
public void setComment(String comment) {
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "teacher_department_assignments")
|
||||
public class TeacherDepartmentAssignment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "teacher_id", nullable = false)
|
||||
private User teacher;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "department_id", nullable = false)
|
||||
private Department department;
|
||||
|
||||
@Column(name = "valid_from", nullable = false)
|
||||
private LocalDate validFrom;
|
||||
|
||||
@Column(name = "valid_to")
|
||||
private LocalDate validTo;
|
||||
|
||||
@Column(name = "is_primary", nullable = false)
|
||||
private Boolean primaryAssignment = true;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String comment;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
|
||||
@Column(name = "created_by")
|
||||
private Long createdBy;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public User getTeacher() {
|
||||
return teacher;
|
||||
}
|
||||
|
||||
public void setTeacher(User teacher) {
|
||||
this.teacher = teacher;
|
||||
}
|
||||
|
||||
public Department getDepartment() {
|
||||
return department;
|
||||
}
|
||||
|
||||
public void setDepartment(Department department) {
|
||||
this.department = department;
|
||||
}
|
||||
|
||||
public LocalDate getValidFrom() {
|
||||
return validFrom;
|
||||
}
|
||||
|
||||
public void setValidFrom(LocalDate validFrom) {
|
||||
this.validFrom = validFrom;
|
||||
}
|
||||
|
||||
public LocalDate getValidTo() {
|
||||
return validTo;
|
||||
}
|
||||
|
||||
public void setValidTo(LocalDate validTo) {
|
||||
this.validTo = validTo;
|
||||
}
|
||||
|
||||
public Boolean getPrimaryAssignment() {
|
||||
return primaryAssignment;
|
||||
}
|
||||
|
||||
public void setPrimaryAssignment(Boolean primaryAssignment) {
|
||||
this.primaryAssignment = primaryAssignment;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
public void setComment(String comment) {
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(Long createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
}
|
||||
78
backend/src/main/java/com/magistr/app/model/TimeSlot.java
Normal file
78
backend/src/main/java/com/magistr/app/model/TimeSlot.java
Normal file
@@ -0,0 +1,78 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "time_slots")
|
||||
public class TimeSlot {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "order_number", nullable = false)
|
||||
private Integer orderNumber;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "time_slot_scope_id", nullable = false)
|
||||
private TimeSlotScope timeSlotScope;
|
||||
|
||||
@Column(name = "start_time", nullable = false)
|
||||
private LocalTime startTime;
|
||||
|
||||
@Column(name = "end_time", nullable = false)
|
||||
private LocalTime endTime;
|
||||
|
||||
@Column(name = "duration_minutes", nullable = false)
|
||||
private Integer durationMinutes;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Integer getOrderNumber() {
|
||||
return orderNumber;
|
||||
}
|
||||
|
||||
public void setOrderNumber(Integer orderNumber) {
|
||||
this.orderNumber = orderNumber;
|
||||
}
|
||||
|
||||
public TimeSlotScope getTimeSlotScope() {
|
||||
return timeSlotScope;
|
||||
}
|
||||
|
||||
public void setTimeSlotScope(TimeSlotScope timeSlotScope) {
|
||||
this.timeSlotScope = timeSlotScope;
|
||||
}
|
||||
|
||||
public LocalTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalTime getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(LocalTime endTime) {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public Integer getDurationMinutes() {
|
||||
return durationMinutes;
|
||||
}
|
||||
|
||||
public void setDurationMinutes(Integer durationMinutes) {
|
||||
this.durationMinutes = durationMinutes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "time_slot_date_assignments")
|
||||
public class TimeSlotDateAssignment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "assignment_date", nullable = false, unique = true)
|
||||
private LocalDate date;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "time_slot_scope_id", nullable = false)
|
||||
private TimeSlotScope timeSlotScope;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public TimeSlotScope getTimeSlotScope() {
|
||||
return timeSlotScope;
|
||||
}
|
||||
|
||||
public void setTimeSlotScope(TimeSlotScope timeSlotScope) {
|
||||
this.timeSlotScope = timeSlotScope;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "time_slot_scopes")
|
||||
public class TimeSlotScope {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 50)
|
||||
private String code;
|
||||
|
||||
@Column(nullable = false, length = 120)
|
||||
private String name;
|
||||
|
||||
@Column(name = "apply_mode", nullable = false, length = 20)
|
||||
private String applyMode;
|
||||
|
||||
@Column(name = "day_of_week")
|
||||
private Integer dayOfWeek;
|
||||
|
||||
@Column(name = "system_scope", nullable = false)
|
||||
private Boolean systemScope = false;
|
||||
|
||||
@Column(name = "display_order", nullable = false)
|
||||
private Integer displayOrder = 100;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getApplyMode() {
|
||||
return applyMode;
|
||||
}
|
||||
|
||||
public void setApplyMode(String applyMode) {
|
||||
this.applyMode = applyMode;
|
||||
}
|
||||
|
||||
public Integer getDayOfWeek() {
|
||||
return dayOfWeek;
|
||||
}
|
||||
|
||||
public void setDayOfWeek(Integer dayOfWeek) {
|
||||
this.dayOfWeek = dayOfWeek;
|
||||
}
|
||||
|
||||
public Boolean getSystemScope() {
|
||||
return systemScope;
|
||||
}
|
||||
|
||||
public void setSystemScope(Boolean systemScope) {
|
||||
this.systemScope = systemScope;
|
||||
}
|
||||
|
||||
public Integer getDisplayOrder() {
|
||||
return displayOrder;
|
||||
}
|
||||
|
||||
public void setDisplayOrder(Integer displayOrder) {
|
||||
this.displayOrder = displayOrder;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
public class User extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.AcademicCalendarActivityType;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AcademicCalendarActivityTypeRepository extends JpaRepository<AcademicCalendarActivityType, Long> {
|
||||
|
||||
List<AcademicCalendarActivityType> findAllByOrderByDisplayOrderAscCodeAsc();
|
||||
|
||||
Optional<AcademicCalendarActivityType> findByCode(String code);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.AcademicCalendarDay;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Modifying;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AcademicCalendarDayRepository extends JpaRepository<AcademicCalendarDay, Long> {
|
||||
|
||||
@Query("""
|
||||
select day
|
||||
from AcademicCalendarDay day
|
||||
join fetch day.activityType
|
||||
where day.academicCalendar.id = :calendarId
|
||||
order by day.courseNumber asc, day.date asc
|
||||
""")
|
||||
List<AcademicCalendarDay> findByCalendarIdWithActivity(@Param("calendarId") Long calendarId);
|
||||
|
||||
@Query("""
|
||||
select day
|
||||
from AcademicCalendarDay day
|
||||
join fetch day.activityType
|
||||
where day.academicCalendar.id = :calendarId
|
||||
and day.courseNumber = :courseNumber
|
||||
and day.date = :date
|
||||
""")
|
||||
Optional<AcademicCalendarDay> findByCalendarIdAndCourseNumberAndDate(
|
||||
@Param("calendarId") Long calendarId,
|
||||
@Param("courseNumber") Integer courseNumber,
|
||||
@Param("date") LocalDate date
|
||||
);
|
||||
|
||||
boolean existsByActivityTypeId(Long activityTypeId);
|
||||
|
||||
@Modifying
|
||||
@Query("delete from AcademicCalendarDay day where day.academicCalendar.id = :calendarId")
|
||||
void deleteByCalendarId(@Param("calendarId") Long calendarId);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.AcademicCalendar;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AcademicCalendarRepository extends JpaRepository<AcademicCalendar, Long> {
|
||||
|
||||
long countByStudyFormId(Long studyFormId);
|
||||
|
||||
@Query("""
|
||||
select calendar
|
||||
from AcademicCalendar calendar
|
||||
join fetch calendar.academicYear
|
||||
join fetch calendar.speciality
|
||||
join fetch calendar.specialtyProfile
|
||||
join fetch calendar.studyForm
|
||||
where (:academicYearId is null or calendar.academicYear.id = :academicYearId)
|
||||
and (:specialtyId is null or calendar.speciality.id = :specialtyId)
|
||||
and (:profileId is null or calendar.specialtyProfile.id = :profileId)
|
||||
order by calendar.academicYear.startDate desc, calendar.title asc
|
||||
""")
|
||||
List<AcademicCalendar> findAllWithDetails(
|
||||
@Param("academicYearId") Long academicYearId,
|
||||
@Param("specialtyId") Long specialtyId,
|
||||
@Param("profileId") Long profileId
|
||||
);
|
||||
|
||||
@Query("""
|
||||
select calendar
|
||||
from AcademicCalendar calendar
|
||||
join fetch calendar.academicYear
|
||||
join fetch calendar.speciality
|
||||
join fetch calendar.specialtyProfile
|
||||
join fetch calendar.studyForm
|
||||
where calendar.id = :id
|
||||
""")
|
||||
Optional<AcademicCalendar> findByIdWithDetails(@Param("id") Long id);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.AcademicYear;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AcademicYearRepository extends JpaRepository<AcademicYear, Long> {
|
||||
|
||||
Optional<AcademicYear> findByTitle(String title);
|
||||
}
|
||||
@@ -4,7 +4,10 @@ import com.magistr.app.model.Classroom;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.List;
|
||||
|
||||
public interface ClassroomRepository extends JpaRepository<Classroom, Long> {
|
||||
Optional<Classroom> findByName(String name);
|
||||
|
||||
List<Classroom> findByStatusNot(String status);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user