From 96e9d8155fbd2ddee30a68a929c086c7ede4d4e5 Mon Sep 17 00:00:00 2001 From: Zuev Date: Wed, 29 Apr 2026 23:35:16 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9C=D0=B8=D0=BB=D0=BB=D0=B8=D0=BE=D0=BD=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9,=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=BA?= =?UTF-8?q?=D0=BB=D0=B0=D0=B4=D0=BA=D0=B8=20"=D0=A0=D0=B0=D1=81=D0=BF?= =?UTF-8?q?=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=B0=D0=BD=D1=8F?= =?UTF-8?q?=D1=82=D0=B8=D0=B9"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DYNAMIC_SCHEDULE_IMPLEMENTATION.md | 26 +- .../app/controller/DepartmentController.java | 57 +- .../app/controller/LessonTypeController.java | 30 + .../app/controller/LessonsController.java | 509 ------ .../controller/ScheduleDataController.java | 283 ---- .../app/controller/SpecialityController.java | 60 +- .../magistr/app/dto/CreateLessonRequest.java | 89 - .../app/dto/CreateScheduleDataRequest.java | 96 -- .../com/magistr/app/dto/LessonResponse.java | 171 -- .../magistr/app/dto/LessonTypeResponse.java | 7 + .../com/magistr/app/dto/ScheduleResponse.java | 128 -- .../java/com/magistr/app/model/Lesson.java | 122 -- .../com/magistr/app/model/ScheduleData.java | 135 -- .../app/repository/LessonRepository.java | 14 - .../repository/ScheduleDataRepository.java | 24 - .../app/utils/DayAndWeekValidator.java | 30 - .../app/utils/SemesterTypeValidator.java | 26 - .../utils/TypeAndFormatLessonValidator.java | 30 - .../main/resources/db/migration/V1__init.sql | 51 - docs/API.md | 112 +- docs/BUSINESS_LOGIC.md | 46 +- docs/DATABASE.md | 73 - docs/DEVELOPMENT.md | 12 +- docs/FRONTEND.md | 19 +- docs/LOGGING.md | 2 +- frontend/admin/css/auditorium-workload.css | 2 +- frontend/admin/css/components.css | 100 +- frontend/admin/css/department.css | 344 ---- frontend/admin/css/modals.css | 357 ---- frontend/admin/index.html | 14 +- frontend/admin/js/main.js | 2 - .../admin/js/views/auditorium-workload.js | 2 +- frontend/admin/js/views/department.js | 456 ----- frontend/admin/js/views/departments-data.js | 133 +- frontend/admin/js/views/groups.js | 95 +- frontend/admin/js/views/schedule.js | 1482 ++++++++++------- frontend/admin/js/views/users.js | 431 +---- frontend/admin/views/department.html | 180 -- frontend/admin/views/departments-data.html | 50 +- frontend/admin/views/groups.html | 22 +- frontend/admin/views/schedule.html | 424 +++-- frontend/admin/views/users.html | 133 +- 42 files changed, 1718 insertions(+), 4661 deletions(-) create mode 100644 backend/src/main/java/com/magistr/app/controller/LessonTypeController.java delete mode 100755 backend/src/main/java/com/magistr/app/controller/LessonsController.java delete mode 100644 backend/src/main/java/com/magistr/app/controller/ScheduleDataController.java delete mode 100755 backend/src/main/java/com/magistr/app/dto/CreateLessonRequest.java delete mode 100644 backend/src/main/java/com/magistr/app/dto/CreateScheduleDataRequest.java delete mode 100755 backend/src/main/java/com/magistr/app/dto/LessonResponse.java create mode 100644 backend/src/main/java/com/magistr/app/dto/LessonTypeResponse.java delete mode 100644 backend/src/main/java/com/magistr/app/dto/ScheduleResponse.java delete mode 100755 backend/src/main/java/com/magistr/app/model/Lesson.java delete mode 100644 backend/src/main/java/com/magistr/app/model/ScheduleData.java delete mode 100755 backend/src/main/java/com/magistr/app/repository/LessonRepository.java delete mode 100644 backend/src/main/java/com/magistr/app/repository/ScheduleDataRepository.java delete mode 100755 backend/src/main/java/com/magistr/app/utils/DayAndWeekValidator.java delete mode 100644 backend/src/main/java/com/magistr/app/utils/SemesterTypeValidator.java delete mode 100755 backend/src/main/java/com/magistr/app/utils/TypeAndFormatLessonValidator.java delete mode 100644 frontend/admin/css/department.css delete mode 100644 frontend/admin/js/views/department.js mode change 100755 => 100644 frontend/admin/js/views/schedule.js mode change 100755 => 100644 frontend/admin/js/views/users.js delete mode 100644 frontend/admin/views/department.html mode change 100755 => 100644 frontend/admin/views/schedule.html diff --git a/DYNAMIC_SCHEDULE_IMPLEMENTATION.md b/DYNAMIC_SCHEDULE_IMPLEMENTATION.md index 10dde97..d7f7256 100644 --- a/DYNAMIC_SCHEDULE_IMPLEMENTATION.md +++ b/DYNAMIC_SCHEDULE_IMPLEMENTATION.md @@ -6,7 +6,7 @@ Реализован первый рабочий срез динамического расписания. Новая модель теперь является базовой схемой проекта и создаётся сразу в `V1__init.sql`. -Старые таблицы `lessons` и `schedule_data` пока физически остаются в `V1__init.sql`, потому что связанный старый код будет удаляться отдельным шагом. Они больше не заполняются тестовыми данными и не являются источником для новой модели расписания. +Старые таблицы `lessons` и `schedule_data`, старые контроллеры, DTO, модели, репозитории и админские вызовы старых API удалены. Новая модель расписания является единственным базовым способом хранения и генерации расписания. ## Что добавлено @@ -78,7 +78,7 @@ | `AcademicDateService` | Календарная математика: семестр по дате, номер недели, чётность, праздник, курс группы, тип активности | | `ScheduleGeneratorService` | Генерация расписания группы или преподавателя на диапазон дат | -Старый `LessonsController` пока физически присутствует в коде и помечен как `@Deprecated`. Это технический хвост до удаления старых экранов и таблиц, а не целевой слой совместимости. +Старый ручной слой занятий удалён: в backend больше нет `LessonsController`, `ScheduleDataController`, сущностей `Lesson`/`ScheduleData`, их DTO, репозиториев и валидаторов старых строк расписания. ### API @@ -134,6 +134,8 @@ GET /api/schedule?teacherId={userId}&startDate=...&endDate=... Заглушки кабинетов заменены на базовые экраны просмотра расписания. +Админский экран `schedule` показывает текущие `schedule_rules` и сетку `time_slots` через новые admin API. + #### Кабинет студента Файл: `frontend/student/index.html`. @@ -268,8 +270,8 @@ docker run --rm -v /mnt/HDD/magistr/magistr/backend:/app -w /app maven:3.9-eclip | `schedule_rule_slots` | 4 | | `time_slots` | 7 | | `academic_calendar_matrix` | 132 | -| `lessons` | 0 | -| `schedule_data` | 0 | +| `lessons` | таблица отсутствует | +| `schedule_data` | таблица отсутствует | Также выполнен: @@ -285,27 +287,19 @@ git diff --check Ограничения: -- полноценный UI конструктора правил в админке ещё не реализован; -- визуальный редактор матрицы учебного графика в админке ещё не реализован; -- UI управления праздниками и учебными годами пока доступен только через API; - строгая проверка конфликтов преподавателей, аудиторий и групп при сохранении правил ещё не реализована; -- студент пока выбирает группу вручную, потому что в модели пользователя нет связи `student -> group`; -- старые таблицы и контроллеры `lessons` / `schedule_data` ещё физически есть, но больше не используются для seed-расписания. +- студент пока выбирает группу вручную, потому что в модели пользователя нет связи `student -> group`. ## Следующий этап Рекомендуемый следующий этап: -1. Добавить в админку вкладку настройки временных слотов. -2. Добавить UI учебных годов, семестров и праздников. -3. Добавить визуальную матрицу учебного графика. -4. Добавить конструктор правил расписания. -5. Реализовать backend-валидацию конфликтов: +1. Реализовать backend-валидацию конфликтов: - преподаватель не может вести разные занятия одновременно; - аудитория не может быть занята двумя разными занятиями одновременно; - группа не может быть на двух занятиях одновременно, кроме корректного деления по подгруппам. -6. Удалить старые экраны, контроллеры, DTO, модели и репозитории `lessons` / `schedule_data`. -7. Удалить старые таблицы `lessons` и `schedule_data` из `V1__init.sql`. +2. Добавить модель связи студента с учебной группой, чтобы кабинет студента не требовал ручного выбора группы. +3. Добавить отдельное управление подгруппами и подключить выбор `subgroupId` в конструкторе правил. ## Связанные документы diff --git a/backend/src/main/java/com/magistr/app/controller/DepartmentController.java b/backend/src/main/java/com/magistr/app/controller/DepartmentController.java index 8adfc3b..3569f96 100644 --- a/backend/src/main/java/com/magistr/app/controller/DepartmentController.java +++ b/backend/src/main/java/com/magistr/app/controller/DepartmentController.java @@ -92,7 +92,62 @@ public class DepartmentController { } } - @DeleteMapping("/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(); + } + + 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); if (!departmentRepository.existsById(id)) { diff --git a/backend/src/main/java/com/magistr/app/controller/LessonTypeController.java b/backend/src/main/java/com/magistr/app/controller/LessonTypeController.java new file mode 100644 index 0000000..af69050 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/controller/LessonTypeController.java @@ -0,0 +1,30 @@ +package com.magistr.app.controller; + +import com.magistr.app.dto.LessonTypeResponse; +import com.magistr.app.model.LessonType; +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") +public class LessonTypeController { + + private final LessonTypesRepository lessonTypesRepository; + + public LessonTypeController(LessonTypesRepository lessonTypesRepository) { + this.lessonTypesRepository = lessonTypesRepository; + } + + @GetMapping + public List getAllLessonTypes() { + return lessonTypesRepository.findAll().stream() + .sorted(Comparator.comparing(LessonType::getLessonType)) + .map(lessonType -> new LessonTypeResponse(lessonType.getId(), lessonType.getLessonType())) + .toList(); + } +} diff --git a/backend/src/main/java/com/magistr/app/controller/LessonsController.java b/backend/src/main/java/com/magistr/app/controller/LessonsController.java deleted file mode 100755 index d0b4119..0000000 --- a/backend/src/main/java/com/magistr/app/controller/LessonsController.java +++ /dev/null @@ -1,509 +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 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())); - } - } - - //Запрос для получения всего списка занятий - @Deprecated - @GetMapping - public List getAllLessons() { - logger.info("Запрос на получение всех занятий"); - - try { - List lessons = lessonRepository.findAll(); - - List 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; - } - } - - //Запрос на получение всех занятий для конкретного преподавателя - @Deprecated - @GetMapping("/{teacherId}") - public ResponseEntity getLessonsById(@PathVariable Long teacherId) { - logger.info("Запрос на получение занятий для преподавателя с ID: {}", teacherId); - try { - List lessons = lessonRepository.findByTeacherId(teacherId); - - if(lessons.isEmpty()) { - logger.info("У преподавателя с ID {} нет занятий", teacherId); - return ResponseEntity.ok(Map.of( - "message", "У преподавателя с ID " + teacherId +" нет занятий.", - "lessons", Collections.emptyList() - )); - } - - List 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 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 response = buildResponse(existingLesson); - response.put("message", "Изменений не обнаружено"); - return ResponseEntity.ok(response); - } - - Lesson updatedLesson = lessonRepository.save(existingLesson); - - Map 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 buildResponse(Lesson lesson) { - Map 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; - } -} diff --git a/backend/src/main/java/com/magistr/app/controller/ScheduleDataController.java b/backend/src/main/java/com/magistr/app/controller/ScheduleDataController.java deleted file mode 100644 index 65f2994..0000000 --- a/backend/src/main/java/com/magistr/app/controller/ScheduleDataController.java +++ /dev/null @@ -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 getAllScheduleDataList() { - logger.info("Получен запрос на получение списка данных расписаний"); - try { - List scheduleData = scheduleDataRepository.findAll(); - List 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 = scheduleDataRepository.findByDepartmentIdAndSemesterTypeAndPeriod(departmentId, semesterType, period ); - - if(scheduleData.isEmpty()){ - logger.info("По параметрам: departmentId = {}, semester = {}, period = {} не найдено записей", departmentId, semesterType, period); - return ResponseEntity.ok(Map.of( - "message", "Записей не найдено" - )); - } - - List 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 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", "Запись удалена")); - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/magistr/app/controller/SpecialityController.java b/backend/src/main/java/com/magistr/app/controller/SpecialityController.java index 92db85a..ab05f3e 100644 --- a/backend/src/main/java/com/magistr/app/controller/SpecialityController.java +++ b/backend/src/main/java/com/magistr/app/controller/SpecialityController.java @@ -92,7 +92,63 @@ public class SpecialityController { } } - @DeleteMapping("/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(); + } + + 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); if (!specialtiesRepository.existsById(id)) { @@ -101,6 +157,6 @@ public class SpecialityController { } specialtiesRepository.deleteById(id); logger.info("Специальность с ID - {} успешно удалена", id); - return ResponseEntity.ok(Map.of("message", "Специальнсть удалена")); + return ResponseEntity.ok(Map.of("message", "Специальность удалена")); } } diff --git a/backend/src/main/java/com/magistr/app/dto/CreateLessonRequest.java b/backend/src/main/java/com/magistr/app/dto/CreateLessonRequest.java deleted file mode 100755 index 8276857..0000000 --- a/backend/src/main/java/com/magistr/app/dto/CreateLessonRequest.java +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/magistr/app/dto/CreateScheduleDataRequest.java b/backend/src/main/java/com/magistr/app/dto/CreateScheduleDataRequest.java deleted file mode 100644 index 7160986..0000000 --- a/backend/src/main/java/com/magistr/app/dto/CreateScheduleDataRequest.java +++ /dev/null @@ -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; - } -} diff --git a/backend/src/main/java/com/magistr/app/dto/LessonResponse.java b/backend/src/main/java/com/magistr/app/dto/LessonResponse.java deleted file mode 100755 index c1ba061..0000000 --- a/backend/src/main/java/com/magistr/app/dto/LessonResponse.java +++ /dev/null @@ -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; - } -} diff --git a/backend/src/main/java/com/magistr/app/dto/LessonTypeResponse.java b/backend/src/main/java/com/magistr/app/dto/LessonTypeResponse.java new file mode 100644 index 0000000..8e44247 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/dto/LessonTypeResponse.java @@ -0,0 +1,7 @@ +package com.magistr.app.dto; + +public record LessonTypeResponse( + Long id, + String name +) { +} diff --git a/backend/src/main/java/com/magistr/app/dto/ScheduleResponse.java b/backend/src/main/java/com/magistr/app/dto/ScheduleResponse.java deleted file mode 100644 index 36b8f28..0000000 --- a/backend/src/main/java/com/magistr/app/dto/ScheduleResponse.java +++ /dev/null @@ -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; - } -} diff --git a/backend/src/main/java/com/magistr/app/model/Lesson.java b/backend/src/main/java/com/magistr/app/model/Lesson.java deleted file mode 100755 index b28d27b..0000000 --- a/backend/src/main/java/com/magistr/app/model/Lesson.java +++ /dev/null @@ -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; - } -} diff --git a/backend/src/main/java/com/magistr/app/model/ScheduleData.java b/backend/src/main/java/com/magistr/app/model/ScheduleData.java deleted file mode 100644 index bdd485f..0000000 --- a/backend/src/main/java/com/magistr/app/model/ScheduleData.java +++ /dev/null @@ -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; - } -} diff --git a/backend/src/main/java/com/magistr/app/repository/LessonRepository.java b/backend/src/main/java/com/magistr/app/repository/LessonRepository.java deleted file mode 100755 index f02e9b5..0000000 --- a/backend/src/main/java/com/magistr/app/repository/LessonRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.magistr.app.repository; - -import com.magistr.app.model.Lesson; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; -import java.util.Optional; - -public interface LessonRepository extends JpaRepository { - - Optional findBySubjectId(Long subjectId); - - List findByTeacherId(Long teacherId); -} diff --git a/backend/src/main/java/com/magistr/app/repository/ScheduleDataRepository.java b/backend/src/main/java/com/magistr/app/repository/ScheduleDataRepository.java deleted file mode 100644 index e9c0318..0000000 --- a/backend/src/main/java/com/magistr/app/repository/ScheduleDataRepository.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.magistr.app.repository; - -import com.magistr.app.model.ScheduleData; -import com.magistr.app.model.SemesterType; -import org.springframework.data.jpa.repository.JpaRepository; - -import java.util.List; - -public interface ScheduleDataRepository extends JpaRepository { - - List findByDepartmentIdAndSemesterTypeAndPeriod(Long departmentId, SemesterType semesterType, String period); - - boolean existsByDepartmentIdAndGroupIdAndSubjectsIdAndLessonTypeIdAndNumberOfHoursAndDivisionAndTeacherIdAndSemesterTypeAndPeriod( - Long departmentId, - Long groupId, - Long subjectsId, - Long lessonTypeId, - Long numberOfHours, - Boolean division, - Long teacherId, - SemesterType semesterType, - String period - ); -} diff --git a/backend/src/main/java/com/magistr/app/utils/DayAndWeekValidator.java b/backend/src/main/java/com/magistr/app/utils/DayAndWeekValidator.java deleted file mode 100755 index b4461f9..0000000 --- a/backend/src/main/java/com/magistr/app/utils/DayAndWeekValidator.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.magistr.app.utils; - -import java.util.Set; - -public class DayAndWeekValidator { - - private static final Set VALID_DAYS = Set.of( - "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота" - ); - - private static final Set VALID_WEEKS = Set.of( - "Верхняя", "Нижняя", "Обе" - ); - - public static boolean isValidDay(String day) { - return day != null && VALID_DAYS.contains(day); - } - - public static boolean isValidWeek(String week) { - return week != null && VALID_WEEKS.contains(week); - } - - public static String getValidDaysMessage() { - return "Допустимые дни: " + String.join(", ", VALID_DAYS); - } - - public static String getValidWeekMessage() { - return "Допустимы для выбора: " + String.join(", ", VALID_WEEKS); - } -} diff --git a/backend/src/main/java/com/magistr/app/utils/SemesterTypeValidator.java b/backend/src/main/java/com/magistr/app/utils/SemesterTypeValidator.java deleted file mode 100644 index f4b53d7..0000000 --- a/backend/src/main/java/com/magistr/app/utils/SemesterTypeValidator.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.magistr.app.utils; - -import com.magistr.app.model.SemesterType; - -import java.util.Arrays; - -public class SemesterTypeValidator { - - public static boolean isValidTypeSemester(String semesterType) { - if (semesterType == null) { - return false; - } - try { - SemesterType.valueOf(semesterType); - return true; - } catch (IllegalArgumentException e) { - return false; - } - } - - public static String getValidTypes() { - return String.join(", ", Arrays.stream(SemesterType.values()) - .map(Enum::name) - .toArray(String[]::new)); - } -} diff --git a/backend/src/main/java/com/magistr/app/utils/TypeAndFormatLessonValidator.java b/backend/src/main/java/com/magistr/app/utils/TypeAndFormatLessonValidator.java deleted file mode 100755 index 21e3146..0000000 --- a/backend/src/main/java/com/magistr/app/utils/TypeAndFormatLessonValidator.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.magistr.app.utils; - -import java.util.Set; - -public class TypeAndFormatLessonValidator { - - private static final Set VALID_TYPES = Set.of( - "Лекция", "Лабораторная работа", "Практическая работа" - ); - - private static final Set VALID_FORMATS = Set.of( - "Онлайн", "Очно" - ); - - public static boolean isValidType(String type) { - return type != null && VALID_TYPES.contains(type); - } - - public static boolean isValidFormat(String format) { - return format != null && VALID_FORMATS.contains(format); - } - - public static String getValidTypesMessage() { - return "Допустимые типы: " + String.join(", ", VALID_TYPES); - } - - public static String getValidFormatsMessage() { - return "Допустимые форматы: " + String.join(", ", VALID_FORMATS); - } -} diff --git a/backend/src/main/resources/db/migration/V1__init.sql b/backend/src/main/resources/db/migration/V1__init.sql index 2eb31e9..de034a4 100755 --- a/backend/src/main/resources/db/migration/V1__init.sql +++ b/backend/src/main/resources/db/migration/V1__init.sql @@ -429,35 +429,6 @@ JOIN users teacher ON teacher.username = data.teacher_username JOIN classrooms classroom ON classroom.name = data.classroom_name JOIN lesson_types lt ON lt.name = data.lesson_type_name; --- ========================================== --- Временные таблицы старой модели. Будут удалены после очистки старого кода. --- ========================================== -CREATE TABLE IF NOT EXISTS lessons ( - id BIGSERIAL PRIMARY KEY, - teacher_id BIGINT NOT NULL REFERENCES users(id), - group_id BIGINT NOT NULL REFERENCES student_groups(id), - subject_id BIGINT NOT NULL REFERENCES subjects(id), - lesson_format VARCHAR(255) NOT NULL, - type_lesson VARCHAR(255) NOT NULL, - classroom_id BIGINT NOT NULL REFERENCES classrooms(id), - day VARCHAR(255) NOT NULL, - week VARCHAR(255) NOT NULL, - time VARCHAR(255) NOT NULL -); - -CREATE TABLE IF NOT EXISTS schedule_data ( - id BIGSERIAL PRIMARY KEY, - department_id BIGINT NOT NULL REFERENCES departments(id), - group_id BIGINT NOT NULL REFERENCES student_groups(id), - subjects_id BIGINT NOT NULL REFERENCES subjects(id), - lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id), - number_of_hours INT NOT NULL, - is_division BOOLEAN NOT NULL DEFAULT FALSE, - teacher_id BIGINT NOT NULL REFERENCES users(id), - semester_type VARCHAR(255) NOT NULL, - period VARCHAR(255) NOT NULL -); - -- ========================================== -- Функция обновления timestamp -- ========================================== @@ -478,10 +449,8 @@ CREATE TRIGGER update_users_updated_at -- Комментарии к таблицам и полям (для документации) -- ========================================== COMMENT ON TABLE users IS 'Пользователи системы (студенты, преподаватели, администраторы)'; -COMMENT ON TABLE lessons IS 'Временная таблица старой модели расписания до удаления старого кода'; COMMENT ON TABLE departments IS 'Кафедры'; COMMENT ON TABLE specialties IS 'Специальности'; -COMMENT ON TABLE schedule_data IS 'Временная таблица старой модели плановой нагрузки до удаления старого кода'; COMMENT ON TABLE time_slots IS 'Настраиваемая сетка временных слотов занятий'; COMMENT ON TABLE academic_years IS 'Учебные годы'; COMMENT ON TABLE semesters IS 'Семестры учебного года'; @@ -490,15 +459,6 @@ COMMENT ON TABLE academic_calendar_matrix IS 'Матрица учебного г COMMENT ON TABLE schedule_rules IS 'Правила динамической генерации расписания'; COMMENT ON TABLE schedule_rule_groups IS 'Привязка правил расписания к учебным группам'; COMMENT ON TABLE schedule_rule_slots IS 'Слоты проведения занятий внутри правила расписания'; -COMMENT ON COLUMN schedule_data.department_id IS 'Идентификатор кафедры'; -COMMENT ON COLUMN schedule_data.group_id IS 'Идентификатор группы'; -COMMENT ON COLUMN schedule_data.subjects_id IS 'Идентификатор предмета'; -COMMENT ON COLUMN schedule_data.lesson_type_id IS 'Идентификатор типа занятия'; -COMMENT ON COLUMN schedule_data.number_of_hours IS 'Количество часов'; -COMMENT ON COLUMN schedule_data.is_division IS 'Является ли занятие разделенным'; -COMMENT ON COLUMN schedule_data.teacher_id IS 'Идентификатор преподавателя'; -COMMENT ON COLUMN schedule_data.semester_type IS 'Тип семестра (Весенний, Осенний)'; -COMMENT ON COLUMN schedule_data.period IS 'Период занятий (год/год)'; COMMENT ON TABLE education_forms IS 'Формы обучения'; COMMENT ON TABLE subgroups IS 'Подгруппы'; @@ -574,17 +534,6 @@ COMMENT ON COLUMN teacher_subjects.subject_id IS 'ID предмета'; COMMENT ON COLUMN teacher_subjects.qualification_level IS 'Уровень квалификации преподавателя'; COMMENT ON COLUMN teacher_subjects.experience_years IS 'Опыт преподавания'; -COMMENT ON COLUMN lessons.id IS 'ID урока'; -COMMENT ON COLUMN lessons.teacher_id IS 'Идентификатор преподавателя, который проводит урок'; -COMMENT ON COLUMN lessons.group_id IS 'ID группы, в которой проходит урок'; -COMMENT ON COLUMN lessons.subject_id IS 'ID предмета, который преподается'; -COMMENT ON COLUMN lessons.lesson_format IS 'Формат урока'; -COMMENT ON COLUMN lessons.type_lesson IS 'Тип урока'; -COMMENT ON COLUMN lessons.classroom_id IS 'ID аудитории, в которой проходит урок'; -COMMENT ON COLUMN lessons.day IS 'День недели, в который проходит урок'; -COMMENT ON COLUMN lessons.week IS 'Номер недели, в которой проходит урок'; -COMMENT ON COLUMN lessons.time IS 'Время урока'; - COMMENT ON COLUMN departments.id IS 'ID кафедры'; COMMENT ON COLUMN departments.name IS 'Название кафедры'; COMMENT ON COLUMN departments.code IS 'Код кафедры'; diff --git a/docs/API.md b/docs/API.md index 8ec374f..a7f633b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -233,110 +233,56 @@ CRUD доступен по: | `PUT` | `/api/admin/schedule-rules/{id}` | Обновить правило | | `DELETE` | `/api/admin/schedule-rules/{id}` | Удалить правило | ---- +### `GET /api/lesson-types` -## Расписание (Lessons, deprecated) +Справочник типов занятий для конструктора правил расписания. -Старые эндпоинты ещё физически есть в коде до удаления старых контроллеров и таблиц. Новый просмотр расписания использует `GET /api/schedule`, а новые seed-данные создаются сразу в динамической модели. - -### `GET /api/users/lessons` - -Список всех занятий с разрешёнными именами (преподаватель, группа, дисциплина, аудитория). - -**Ответ:** +**Ответ:** ```json [ - { - "id": 1, - "teacherName": "Тестовый преподаватель", - "groupName": "ИВТ-21-1", - "classroomName": "101 Ленинская", - "educationFormName": "Бакалавриат", - "subjectName": "Высшая математика", - "typeLesson": "Лекция", - "lessonFormat": "Очно", - "day": "Понедельник", - "week": "Верхняя", - "time": "11:40 - 13:10" - } + { "id": 1, "name": "Лекция" }, + { "id": 2, "name": "Практика" } ] ``` -### `GET /api/users/lessons/{teacherId}` +--- -Занятия конкретного преподавателя. +## Кафедры и специальности -### `POST /api/users/lessons/create` +### Кафедры -Создание занятия. +| Метод | URL | Назначение | +|-------|-----|------------| +| `GET` | `/api/departments` | Список кафедр | +| `POST` | `/api/departments` | Создать кафедру | +| `PUT` | `/api/departments/{id}` | Обновить кафедру | +| `DELETE` | `/api/departments/{id}` | Удалить кафедру | -**Тело запроса:** +**Тело создания/обновления:** ```json { - "teacherId": 2, - "groupId": 1, - "subjectId": 1, - "lessonFormat": "Очно", - "typeLesson": "Лекция", - "classroomId": 1, - "day": "Понедельник", - "week": "Верхняя", - "time": "11:40 - 13:10" + "departmentName": "Кафедра ИБ", + "departmentCode": 1 } ``` -**Валидация:** +### Специальности -| Поле | Правило | -|------|---------| -| `teacherId` | Обязателен, ≠ 0 | -| `groupId` | Обязателен, ≠ 0 | -| `subjectId` | Обязателен, ≠ 0 | -| `lessonFormat` | `Очно` или `Онлайн` | -| `typeLesson` | `Лекция`, `Практическая работа`, `Лабораторная работа` | -| `classroomId` | Обязателен, ≠ 0 | -| `day` | Пн–Сб (на русском) | -| `week` | `Верхняя`, `Нижняя`, `Обе` | -| `time` | Обязателен | +| Метод | URL | Назначение | +|-------|-----|------------| +| `GET` | `/api/specialties` | Список специальностей | +| `POST` | `/api/specialties` | Создать специальность | +| `PUT` | `/api/specialties/{id}` | Обновить специальность | +| `DELETE` | `/api/specialties/{id}` | Удалить специальность | - -### `PUT /api/users/lessons/update/{lessonId}` - -Обновление занятия. Поддерживает partial update — передаются только изменённые поля. - -**Тело ответа:** +**Тело создания/обновления:** ```json { - "id": 5, - "teacherId": 1, - "groupId": 1, - "subjectId": 2, - "LessonFormat": "Онлайн", - "typeLesson": "Практическая работа", - "classroomId": 3, - "day": "Понедельник", - "week": "Верхняя", - "time": "9:40 - 11:10", - "updatedFields": { - "teacherId": 1, - "subjectId": 2, - "lessonFormat": "Онлайн", - "classroomId": 3, - "day": "Понедельник", - "time": "9:40 - 11:10" - }, - "message": "Занятие успешно обновлено" + "specialityName": "Программная инженерия", + "specialityCode": "09.03.04" } ``` -### `DELETE /api/users/lessons/delete/{lessonId}` - -Удаление занятия. - -### `GET /api/users/lessons/ping` - -Проверка доступности контроллера. Возвращает строку `pong`. - --- ## Группы @@ -375,11 +321,13 @@ CRUD доступен по: "groupSize": 12, "educationFormId": 1, "departmentId": 1, - "course": 2, + "yearStartStudy": 2026, "specialityCode": 1 } ``` +`specialityCode` исторически содержит ID записи из `/api/specialties`; текущий курс вычисляется из `yearStartStudy`. + ### `DELETE /api/groups/{id}` Удаление группы. diff --git a/docs/BUSINESS_LOGIC.md b/docs/BUSINESS_LOGIC.md index a87d978..da1e6ea 100644 --- a/docs/BUSINESS_LOGIC.md +++ b/docs/BUSINESS_LOGIC.md @@ -91,24 +91,6 @@ Праздник считается пропуском: занятие не переносится и не списывает академические часы. -### Временная старая сущность «Занятие» (Lesson) - -`lessons` пока физически остаётся в схеме до удаления старого кода, но базовая миграция больше не заполняет её тестовыми данными. Новые экраны просмотра используют `GET /api/schedule`. - -Каждая запись в расписании содержит: - -| Поле | Описание | Пример | -|------|----------|--------| -| `teacher_id` | Преподаватель | 2 | -| `group_id` | Учебная группа | 1 | -| `subject_id` | Дисциплина | 3 | -| `lesson_format` | Формат проведения | `Очно`, `Онлайн` | -| `type_lesson` | Тип занятия | `Лекция`, `Практическая работа`, `Лабораторная работа` | -| `classroom_id` | Аудитория | 1 | -| `day` | День недели | `Понедельник` ... `Суббота` | -| `week` | Чётность недели | `Верхняя`, `Нижняя`, `Обе` | -| `time` | Временной слот | `8:00 - 9:30` | - ### Временны́е слоты Сетка пар хранится в `time_slots` и настраивается для каждого тенанта. При миграции создаются базовые слоты: @@ -123,30 +105,12 @@ | 6 | 16:40 – 18:10 | | 7 | 18:30 – 20:00 | -### Валидация при создании/обновлении +### Валидация правил расписания -- **Дни:** только `Понедельник` – `Суббота` (`DayAndWeekValidator`) -- **Недели:** только `Верхняя`, `Нижняя`, `Обе` -- **Формат:** только `Очно`, `Онлайн` (`TypeAndFormatLessonValidator`) -- **Тип:** только `Лекция`, `Практическая работа`, `Лабораторная работа` -- Все ID (преподаватель, группа, дисциплина, аудитория) обязательны и не могут быть 0 - -### Временные старые данные к составлению расписания (Schedule Data) - -Таблица `schedule_data` пока физически остаётся в схеме до удаления старого кода. Новая базовая миграция создаёт `schedule_rules` и `schedule_rule_groups` напрямую, без ETL из `schedule_data`. - -| Поле | Описание | -|------|----------| -| `department_id` | Кафедра | -| `semester` | Номер семестра | -| `group_id` | Учебная группа | -| `subjects_id` | Дисциплина | -| `lesson_type_id` | Тип занятия | -| `number_of_hours` | Количество часов | -| `is_division` | Деление на подгруппы | -| `teacher_id` | Преподаватель | -| `semester_type` | Тип семестра (Весенний / Осенний) | -| `period` | Учебный год (напр. `2024/2025`) | +- **Правило:** обязательны дисциплина, семестр, дата начала, положительный лимит академических часов и хотя бы одна группа. +- **Слот:** день недели должен быть от 1 до 7, чётность недели обязательна. +- **Связанные сущности:** временной слот, преподаватель, аудитория и тип занятия должны существовать в БД. +- **Формат:** `lessonFormat` обязателен и хранится в слоте правила. --- diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 8e5d67e..5dd0cd8 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -117,33 +117,6 @@ erDiagram BIGINT lesson_type_id FK,PK } - lessons { - BIGSERIAL id PK - BIGINT teacher_id FK - BIGINT group_id FK - BIGINT subject_id FK - VARCHAR lesson_format - VARCHAR type_lesson - BIGINT classroom_id FK - VARCHAR day - VARCHAR week - VARCHAR time - } - - schedule_data { - BIGSERIAL id PK - BIGINT department_id FK - INT semester - BIGINT group_id FK - BIGINT subjects_id FK - BIGINT lesson_type_id FK - INT number_of_hours - BOOLEAN is_division - BIGINT teacher_id FK - VARCHAR semester_type - VARCHAR period - } - time_slots { BIGSERIAL id PK INT order_number UK @@ -212,26 +185,17 @@ erDiagram departments ||--o{ users : "department_id" departments ||--o{ student_groups : "department_id" departments ||--o{ subjects : "department_id" - departments ||--o{ schedule_data : "department_id" education_forms ||--o{ student_groups : "education_form_id" specialties ||--o{ student_groups : "specialty_code" student_groups ||--o{ subgroups : "group_id" - student_groups ||--o{ lessons : "group_id" - student_groups ||--o{ schedule_data : "group_id" student_groups ||--o{ schedule_rule_groups : "group_id" - users ||--o{ lessons : "teacher_id" users ||--o{ teacher_subjects : "user_id" users ||--o{ teacher_lesson_types : "user_id" - users ||--o{ schedule_data : "teacher_id" - subjects ||--o{ lessons : "subject_id" subjects ||--o{ teacher_subjects : "subject_id" subjects ||--o{ teacher_lesson_types : "subject_id" - subjects ||--o{ schedule_data : "subjects_id" subjects ||--o{ schedule_rules : "subject_id" lesson_types ||--o{ teacher_lesson_types : "lesson_type_id" - lesson_types ||--o{ schedule_data : "lesson_type_id" lesson_types ||--o{ schedule_rule_slots : "lesson_type_id" - classrooms ||--o{ lessons : "classroom_id" classrooms ||--o{ schedule_rule_slots : "classroom_id" classrooms ||--o{ classroom_equipments : "classroom_id" equipments ||--o{ classroom_equipments : "equipment_id" @@ -352,23 +316,6 @@ erDiagram ### Расписание -#### `lessons` — Временная таблица старой модели - -Таблица пока остаётся в схеме до удаления старого кода, но новая базовая миграция больше не заполняет её тестовыми данными. Целевой источник расписания — `schedule_rules` и `schedule_rule_slots`. - -| Колонка | Тип | Описание | -|---------|-----|----------| -| `id` | BIGSERIAL PK | ID | -| `teacher_id` | BIGINT FK → users | Преподаватель | -| `group_id` | BIGINT FK → student_groups | Группа | -| `subject_id` | BIGINT FK → subjects | Дисциплина | -| `lesson_format` | VARCHAR(255) | `Очно` / `Онлайн` | -| `type_lesson` | VARCHAR(255) | `Лекция` / `Практическая работа` / `Лабораторная работа` | -| `classroom_id` | BIGINT FK → classrooms | Аудитория | -| `day` | VARCHAR(255) | День недели | -| `week` | VARCHAR(255) | `Верхняя` / `Нижняя` / `Обе` | -| `time` | VARCHAR(255) | Временной слот | - #### `lesson_types` — Типы занятий (справочник) | Колонка | Тип | Описание | |---------|-----|----------| @@ -394,26 +341,6 @@ erDiagram | `subject_id` | BIGINT PK, FK → subjects (CASCADE) | Дисциплина | | `lesson_type_id` | BIGINT PK, FK → lesson_types (CASCADE) | Тип занятия | -#### `schedule_data` — Временная таблица старой модели нагрузки - -Таблица пока остаётся в схеме до удаления старого кода, но новая базовая миграция больше не использует её как источник seed-данных. - -| Колонка | Тип | Описание | -|---------|-----|----------| -| `id` | BIGSERIAL PK | ID | -| `department_id` | BIGINT FK → departments | Кафедра | -| `semester` | INT | Номер семестра | -| `group_id` | BIGINT FK → student_groups | Группа | -| `subjects_id` | BIGINT FK → subjects | Дисциплина | -| `lesson_type_id` | BIGINT FK → lesson_types | Тип занятия | -| `number_of_hours` | INT | Количество часов | -| `is_division` | BOOLEAN | Деление на подгруппы | -| `teacher_id` | BIGINT FK → users | Преподаватель | -| `semester_type` | VARCHAR(255) | Весенний / Осенний | -| `period` | VARCHAR(255) | Учебный год | - -### Динамическое расписание - #### `time_slots` — Временные слоты занятий | Колонка | Тип | Описание | |---------|-----|----------| diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index a0ba3a2..f7727e9 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -50,8 +50,8 @@ docker compose up -d # Пересоздаёт с нуля | Категория | Стиль | Пример | |-----------|-------|--------| -| Классы | PascalCase | `LessonsController`, `LessonResponse` | -| Методы и переменные | camelCase | `getAllLessons()`, `teacherId` | +| Классы | PascalCase | `ScheduleController`, `RenderedLessonDto` | +| Методы и переменные | camelCase | `buildScheduleForTeacher()`, `teacherId` | | Константы | UPPER_SNAKE_CASE | `ROLE_REDIRECTS` | | Пакеты | lowercase | `com.magistr.app.controller` | @@ -79,7 +79,7 @@ logger.error("Ошибка при сохранении: {}", e.getMessage(), e); #### Валидация -- Для сложных правил — отдельные классы-валидаторы (`DayAndWeekValidator`, `TypeAndFormatLessonValidator`) +- Для сложных правил — отдельные сервисы или приватные методы в контроллере с понятным сообщением об ошибке - Для простых — inline-проверки в контроллере с `ResponseEntity.badRequest()` #### Импорты @@ -257,7 +257,7 @@ com.magistr.app/ │ └── ConfigMapUpdater.java # Обновление K8s ConfigMap ├── controller/ # REST-контроллеры │ ├── AuthController.java -│ ├── LessonsController.java +│ ├── ScheduleController.java │ ├── ClassroomController.java │ ├── DatabaseController.java │ ├── UserController.java @@ -269,7 +269,5 @@ com.magistr.app/ ├── dto/ # Data Transfer Objects ├── model/ # JPA-сущности ├── repository/ # Spring Data JPA -└── utils/ # Валидаторы - ├── DayAndWeekValidator.java - └── TypeAndFormatLessonValidator.java +└── utils/ # Общие утилиты ``` diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md index 391558d..c71916b 100644 --- a/docs/FRONTEND.md +++ b/docs/FRONTEND.md @@ -29,7 +29,6 @@ frontend/ │ │ ├── layout.css # Раскладка (sidebar, topbar, content) │ │ ├── components.css # Кнопки, таблицы, карточки, формы │ │ ├── modals.css # Модальные окна -│ │ ├── department.css # Стили кафедры │ │ └── departments-data.css # Стили создания кафедры/специальности │ ├── js/ │ │ ├── main.js # Инициализация, маршрутизация, навигация @@ -43,9 +42,8 @@ frontend/ │ │ ├── subjects.js # Управление дисциплинами │ │ ├── equipments.js # Управление оборудованием │ │ ├── edu-forms.js # Формы обучения -│ │ ├── schedule.js # Расписание занятий +│ │ ├── schedule.js # Конструктор правил, сетки пар и учебного графика │ │ ├── database.js # Управление тенантами -│ │ ├── department.js # Кафедры │ │ └── departments-data.js # Создание кафедры/специальности │ ├── views/ # HTML-шаблоны представлений │ │ ├── users.html @@ -56,7 +54,6 @@ frontend/ │ │ ├── edu-forms.html │ │ ├── schedule.html │ │ ├── database.html -│ │ ├── department.html │ │ └── departments-data.html │ │ │ └── settings/ # ⚙️ Страница настроек (отдельный SPA) @@ -106,10 +103,15 @@ frontend/ | `equipments` | Оборудование | `/api/equipments` | | `classrooms` | Аудитории | `/api/classrooms` | | `subjects` | Дисциплины | `/api/subjects` | -| `schedule` | Старый админский экран расписания до удаления | `/api/users/lessons`; новые правила — `/api/admin/schedule-rules` | +| `schedule` | Конструктор правил динамического расписания, сетка пар, учебные годы, семестры, праздники и матрица графика | `/api/admin/schedule-rules`, `/api/admin/time-slots`, `/api/admin/calendar`, `/api/lesson-types` | | `database` | Тенанты | `/api/database` | -| `department` | Кафедры | `/api/departments` | -| `departments-data` | Создание кафедры/специальности | `/api/departments` | +| `departments-data` | Создание, редактирование и удаление кафедр/специальностей | `/api/departments`, `/api/specialties` | + +### Особенности админских вкладок + +- Вкладка `groups` загружает кафедры и специальности из справочников и при создании группы отправляет `yearStartStudy`; поле `specialityCode` содержит ID выбранной специальности. +- Вкладка `schedule` не обращается к старым `lessons` API. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы и набор слотов. +- Вкладка `departments-data` использует модальные формы редактирования и маршруты `PUT/DELETE /api/departments/{id}` и `PUT/DELETE /api/specialties/{id}`. ### Страница настроек (`/admin/settings/`) @@ -226,8 +228,7 @@ export function isAuthenticatedAsAdmin() { 2. **`layout.css`** — Sidebar, topbar, content area, dropdown настроек, responsive 3. **`components.css`** — Кнопки, таблицы, карточки, badge, формы, theme-toggle 4. **`modals.css`** — Модальные окна -5. **`department.css`** — Стили страницы кафедр -6. **`departments-data.css`** — Стили создания кафедры/специальности +5. **`departments-data.css`** — Стили создания кафедры/специальности ### Темизация diff --git a/docs/LOGGING.md b/docs/LOGGING.md index 41ad688..f26455a 100644 --- a/docs/LOGGING.md +++ b/docs/LOGGING.md @@ -94,7 +94,7 @@ MDC.remove("tenant.id"); | `TenantConfigWatcher` | INFO, ERROR, WARN | Изменения ConfigMap, Flyway миграции | | `ConfigMapUpdater` | INFO, WARN, ERROR | Обновление ConfigMap в K8s | | `DataInitializer` | INFO | Инициализация БД при старте | -| `LessonsController` | INFO, DEBUG, ERROR | CRUD-операции с занятиями, валидация | +| `ScheduleController` | INFO, ERROR | Запросы динамического расписания и ошибки генерации | ### Паттерн использования diff --git a/frontend/admin/css/auditorium-workload.css b/frontend/admin/css/auditorium-workload.css index c43f42d..8ea95ae 100644 --- a/frontend/admin/css/auditorium-workload.css +++ b/frontend/admin/css/auditorium-workload.css @@ -95,7 +95,7 @@ color: var(--text-secondary); } -/* Lesson Cards inside grid cells */ +/* Карточки занятий внутри ячеек сетки */ .lesson-card { background: var(--bg-card); border: 1px solid var(--bg-card-border); diff --git a/frontend/admin/css/components.css b/frontend/admin/css/components.css index 3dbb57e..c0cb7b0 100755 --- a/frontend/admin/css/components.css +++ b/frontend/admin/css/components.css @@ -104,6 +104,104 @@ opacity: 0.5; } +.schedule-section-label { + display: block; + font-size: 0.78rem; + font-weight: 500; + color: var(--text-secondary); + margin: 1rem 0 0.5rem; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.schedule-checkbox-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.35rem; + padding: 0.5rem; + background: var(--bg-input); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); +} + +.schedule-slots-panel { + margin-top: 1rem; +} + +.schedule-slots-list { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-top: 0.75rem; +} + +.schedule-slot-row { + display: grid; + grid-template-columns: repeat(7, minmax(130px, 1fr)) auto; + gap: 0.75rem; + align-items: end; + padding: 0.75rem; + background: var(--bg-input); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); +} + +.schedule-slot-summary, +.schedule-semester-line { + margin-bottom: 0.35rem; +} + +.schedule-semester-line { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.schedule-inline-table { + margin-top: 1rem; +} + +.matrix-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 0.75rem; + margin-top: 1rem; +} + +.matrix-week { + display: grid; + gap: 0.45rem; + padding: 0.75rem; + background: var(--bg-input); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); +} + +.matrix-week > span { + color: var(--text-secondary); + font-size: 0.78rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.muted { + color: var(--text-secondary); +} + +@media (max-width: 1180px) { + .schedule-slot-row { + grid-template-columns: repeat(2, minmax(180px, 1fr)); + } +} + +@media (max-width: 640px) { + .schedule-slot-row { + grid-template-columns: 1fr; + } +} + /* Hide Number Arrows */ input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button { @@ -1032,4 +1130,4 @@ tbody tr:hover { position: fixed; top: 1.25rem; right: 1.25rem; -} \ No newline at end of file +} diff --git a/frontend/admin/css/department.css b/frontend/admin/css/department.css deleted file mode 100644 index 88b83c9..0000000 --- a/frontend/admin/css/department.css +++ /dev/null @@ -1,344 +0,0 @@ -/* ===== Оверлей для модалок создания записей (к/ф) ===== */ -.cs-overlay { - display: none; - position: fixed; - inset: 0; - z-index: 1000; - background: rgba(0, 0, 0, 0.55); - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); -} - -.cs-overlay.open { - display: block; -} - -.cs-overlay-scroll { - width: 100%; - height: 100%; - overflow-y: auto; - padding: 2rem 1rem; - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; -} - -/* Общие стили для обеих модалок */ -.cs-modal { - width: 100%; - max-width: 1100px; - position: relative; - animation: csModalAppear 0.25s ease-out; -} - -/* Модалка 1 (форма) всегда поверх модалки 2 (таблицы), - чтобы выпадающие списки не уходили под таблицу */ -.cs-modal-form { - z-index: 2; -} - -.cs-modal-table { - z-index: 1; -} - -@keyframes csModalAppear { - from { opacity: 0; transform: translateY(-12px); } - to { opacity: 1; transform: translateY(0); } -} - -.cs-modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; -} - -.cs-modal-header h2 { - margin: 0; -} - -/* Кнопка закрытия */ -.btn-close-panel { - background: none; - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-sm); - font-size: 1.3rem; - line-height: 1; - padding: 0.25rem 0.6rem; - color: var(--text-secondary); - cursor: pointer; - transition: color var(--transition), background var(--transition), border-color var(--transition); -} - -.btn-close-panel:hover { - color: var(--error); - background: rgba(239, 68, 68, 0.1); - border-color: var(--error); -} - -.wrap{ - max-width: 900px; - margin: 0 auto; - background: var(--bg-card); - border: 1px solid var(--bg-card-border); - border-radius: 12px; - overflow: hidden; - box-shadow: 0 6px 20px rgba(0,0,0,.06); -} - -.header{ - padding: 14px 16px; - border-bottom: 1px solid var(--bg-card-border); - font-weight: 700; - color: var(--text-primary); -} - -details.table-item{ - border-top: 1px solid var(--bg-card-border); -} -details.table-item:first-of-type{ border-top:none; } - -summary{ - list-style: none; - cursor: pointer; - user-select: none; - padding: 12px 16px; - display: flex; - align-items: center; - gap: 10px; -} -summary::-webkit-details-marker{ display:none; } - -.chev{ - width: 28px; - height: 28px; - border: 1px solid var(--bg-card-border); - border-radius: 10px; - display: grid; - place-items: center; - flex: 0 0 auto; - - color: var(--text-secondary); - background: var(--bg-input); - - transition: transform .18s ease, color .18s ease, border-color .18s ease, background .18s ease; -} - -.chev-icon{ - width: 16px; - height: 16px; - display: block; -} - -summary:hover .chev{ - background: var(--bg-hover); - border-color: color-mix(in srgb, var(--accent) 22%, var(--bg-card-border)); - color: var(--text-primary); -} - -details[open] .chev{ - transform: rotate(180deg); - color: var(--accent); - border-color: color-mix(in srgb, var(--accent) 35%, var(--bg-card-border)); - background: color-mix(in srgb, var(--accent) 10%, var(--bg-input)); -} - -.meta{ color: var(--text-secondary); font-size: 12px; } - -.content{ padding: 0 16px 16px 16px; } - -.wrap table{ - width: 100%; - border-collapse: collapse; - border: 1px solid var(--bg-card-border); - border-radius: 10px; - overflow: hidden; - background: var(--bg-card); -} - -.wrap thead th{ - text-align: left; - font-size: 13px; - color: var(--text-secondary); - background: var(--bg-input); - border-bottom: 1px solid var(--bg-card-border); - padding: 10px 12px; -} - -.wrap tbody td{ - padding: 10px 12px; - border-bottom: 1px solid var(--bg-card-border); - font-size: 14px; - color: var(--text-primary); -} - -.wrap tbody tr:hover{ background: var(--bg-hover); } - -.title-multiline{ - display: flex; - flex-direction: column; - gap: 2px; - line-height: 1.2; -} - -.title-multiline .title-main{ - font-weight: 700; - color: var(--text-primary); -} - -.title-multiline .title-sub{ - font-weight: 500; - font-size: 12px; - color: var(--text-secondary); -} - -.title-multiline b{ - font-weight: 700; - color: var(--text-primary); -} - -/* summary = 3 колонки: [chev] [title] [meta] */ -details.table-item > summary{ - display: grid; - grid-template-columns: 28px 1fr auto; - gap: 12px; - align-items: start; /* важно: всё прижимаем к верху */ - padding: 12px 16px; -} - -/* чтобы текст нормально переносился и не растягивал мету */ -details.table-item > summary .title{ - min-width: 0; /* важно для grid, иначе может распирать */ -} - -/* "2 записи" всегда справа и сверху, аккуратно */ -details.table-item > summary .meta{ - justify-self: end; - align-self: start; - white-space: nowrap; - padding-top: 4px; /* чуть опустить относительно первой строки */ - font-size: 12px; - color: var(--text-secondary); -} - -/* стрелка тоже сверху */ -details.table-item > summary .chev{ - align-self: start; - margin-top: 2px; -} - -.records-search{ - width: min(360px, 60vw); - padding: 0.45rem 0.7rem; - background: var(--bg-input); - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-size: 0.9rem; - outline: none; - transition: border-color .2s ease, box-shadow .2s ease, background .2s ease; -} - -.records-search::placeholder{ color: var(--text-placeholder); } - -.records-search:focus{ - background: var(--bg-input-focus); - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-glow); -} -/* Таблица внутри раскрывающегося блока */ -details.table-item .content table{ - width: 100%; - border-collapse: separate; /* нужно для красивых линий */ - border-spacing: 0; - border: 1px solid var(--bg-card-border); - border-radius: 12px; - overflow: hidden; - background: var(--bg-card); -} - -/* Шапка */ -details.table-item .content thead th{ - position: sticky; /* опционально: шапка прилипает при скролле */ - top: 0; - z-index: 1; - - background: var(--bg-input); - color: var(--text-secondary); - border-bottom: 1px solid var(--bg-card-border); -} - -/* Ячейки: одинаковые отступы */ -details.table-item .content th, -details.table-item .content td{ - padding: 0.75rem 0.85rem; - vertical-align: top; -} - -/* Вертикальные разделители между колонками */ -details.table-item .content th:not(:last-child), -details.table-item .content td:not(:last-child){ - border-right: 1px solid var(--bg-card-border); -} - -/* Горизонтальные разделители между строками */ -details.table-item .content tbody td{ - border-bottom: 1px solid var(--bg-card-border); - color: var(--text-primary); -} - -/* У последней строки нет нижней линии */ -details.table-item .content tbody tr:last-child td{ - border-bottom: none; -} - -/* "Зебра" для читабельности */ -details.table-item .content tbody tr:nth-child(even){ - background: color-mix(in srgb, var(--bg-card) 70%, var(--bg-hover)); -} - -/* Ховер по строке */ -details.table-item .content tbody tr:hover{ - background: var(--bg-hover); -} - -/* (Опционально) Чтобы длинный текст не ломал ширину */ -details.table-item .content td{ - word-break: break-word; -} - -/* (Опционально) если таблица широкая — пусть скроллится горизонтально */ -details.table-item .content{ - overflow-x: auto; -} - -/* ===== Контейнер занятий преподавателя в модалках ===== */ -.cs-modal-table .lessons-container { - max-height: 50vh; - overflow-y: auto; - padding-right: 0.5rem; - scrollbar-width: thin; - scrollbar-color: rgba(99, 102, 241, 0.55) rgba(255, 255, 255, 0.06); -} - -.cs-modal-table .lessons-container::-webkit-scrollbar { - width: 10px; -} - -.cs-modal-table .lessons-container::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.06); - border-radius: 10px; -} - -.cs-modal-table .lessons-container::-webkit-scrollbar-thumb { - background: rgba(99, 102, 241, 0.55); - border-radius: 10px; - border: 2px solid rgba(0, 0, 0, 0); - background-clip: padding-box; -} - -.cs-modal-table .lessons-container::-webkit-scrollbar-thumb:hover { - background: rgba(99, 102, 241, 0.75); -} - \ No newline at end of file diff --git a/frontend/admin/css/modals.css b/frontend/admin/css/modals.css index e4be11b..6235575 100644 --- a/frontend/admin/css/modals.css +++ b/frontend/admin/css/modals.css @@ -59,360 +59,3 @@ .modal-close:hover { color: var(--error); } - -/* ===== Кнопки ===== */ -.btn-add-lesson { - padding: 0.35rem 0.7rem; - background: rgba(16, 185, 129, 0.1); - border: 1px solid rgba(16, 185, 129, 0.2); - border-radius: var(--radius-sm); - color: var(--success); - font-family: inherit; - font-size: 0.8rem; - cursor: pointer; - transition: background var(--transition), transform var(--transition); - position: relative; - overflow: hidden; -} - -.btn-add-lesson:hover { - background: rgba(16, 185, 129, 0.2); - transform: scale(1.05); -} - -.btn-view-lessons { - padding: 0.35rem 0.7rem; - background: rgba(99, 102, 241, 0.1); - border: 1px solid rgba(99, 102, 241, 0.2); - border-radius: var(--radius-sm); - color: var(--accent); - font-family: inherit; - font-size: 0.8rem; - cursor: pointer; - transition: all var(--transition); - white-space: nowrap; -} - -.btn-view-lessons:hover { - background: rgba(99, 102, 241, 0.2); - transform: translateY(-1px); -} - -/* ===== Кнопки-переключатели (неделя) ===== */ -.btn-checkbox { - display: inline-block; - cursor: pointer; -} - -.btn-checkbox input { - position: absolute; - opacity: 0; - width: 0; - height: 0; -} - -.checkbox-btn { - display: inline-block; - padding: 0.5rem 1rem; - background: var(--bg-secondary); - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-sm); - color: var(--text-primary); - transition: all var(--transition); - user-select: none; -} - -.btn-checkbox input:checked + .checkbox-btn { - background: var(--success, #10b981); - border-color: var(--success, #10b981); - color: #fff; -} - -/* =========================================================== - ===== 2-е модальное окно (View Lessons) — ОСНОВНЫЕ ПРАВКИ ===== - Требования: - - слева - - ~30% ширины - - сверху начинается СРАЗУ под 1-й модалкой - - высота = весь остаток до низа экрана - - визуально "ниже" 1-й модалки (и по z-index тоже ниже) - =========================================================== */ - -#modal-view-lessons.modal-overlay { - background: transparent !important; - backdrop-filter: none !important; - pointer-events: none; - z-index: 999; /* ниже чем 1-е (1000) */ -} - -/* В открытом состоянии: прижать влево и опустить вниз на высоту "шапки" */ -#modal-view-lessons.modal-overlay.open { - justify-content: flex-start; - align-items: flex-start; - - padding-left: 1rem; - padding-right: 1rem; - - /* ключевое: высота 1-й модалки приходит из JS через --add-lesson-height */ - padding-top: var(--add-lesson-height, 0px); -} - -/* Панель 2-й модалки */ -#modal-view-lessons .view-lessons-modal { - width: 30vw !important; - max-width: 30vw !important; - min-width: 320px; - - pointer-events: auto; - - background: var(--bg-primary); - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-md); - padding: 2rem; - - position: relative; - box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); - - margin: 0; - - /* отключаем "пружинку" от .modal-content */ - transform: none; - - /* ключевое: занимает остаток по высоте */ - height: calc(100vh - var(--add-lesson-height, 0px)); - max-height: calc(100vh - var(--add-lesson-height, 0px)); - - /* чтобы скролл был внутри, а не у всей модалки */ - display: flex; - flex-direction: column; - overflow: hidden; -} - -/* Header во 2-й модалке */ -#modal-veiw-lessons .modal-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 1rem; - padding-right: 2rem; - flex: 0 0 auto; -} - -#modal-view-lessons .modal-header h2 { - margin: 0; - font-size: 1.3rem; - color: var(--text-primary); -} - -/* Контейнер занятий: растягивается и скроллится */ -#modal-view-lessons .lessons-container { - flex: 1 1 auto; - overflow-y: auto; - - /* перебиваем старое ограничение */ - max-height: none; - - padding-right: 0.5rem; -} - -/* ===== Карточки занятий ===== */ -.lesson-card { - background: var(--bg-card); - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-sm); - padding: 1.2rem; - margin-bottom: 1rem; - transition: all 0.2s ease; -} - -.lesson-card:hover { - transform: translateY(-2px); - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15); - border-color: var(--accent); -} - -.lesson-card-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.8rem; - padding-bottom: 0.5rem; - border-bottom: 1px dashed var(--bg-card-border); -} - -.lesson-group { - font-weight: 700; - color: var(--accent); - font-size: 1rem; - background: rgba(99, 102, 241, 0.1); - padding: 0.3rem 0.8rem; - border-radius: 20px; -} - -.lesson-time { - color: var(--text-secondary); - font-size: 0.9rem; - display: flex; - align-items: center; - gap: 0.3rem; -} - -.lesson-time::before { - content: "🕒"; - font-size: 0.9rem; - opacity: 0.7; -} - -.lesson-card-body { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.lesson-subject { - font-weight: 600; - color: var(--text-primary); - font-size: 1.1rem; - display: flex; - align-items: center; - gap: 0.5rem; -} - -.lesson-subject::before { - content: "📚"; - font-size: 1rem; - opacity: 0.7; -} - -.lesson-details { - display: flex; - flex-wrap: wrap; - gap: 0.8rem; - margin-top: 0.5rem; -} - -.lesson-detail-item { - background: var(--bg-input); - padding: 0.3rem 0.8rem; - border-radius: 15px; - font-size: 0.85rem; - color: var(--text-secondary); - border: 1px solid var(--bg-card-border); -} - -/* День недели как разделитель */ -.lesson-day-divider { - margin: 1.5rem 0 1rem 0; - font-weight: 700; - color: var(--accent); - font-size: 1.1rem; - text-transform: uppercase; - letter-spacing: 0.05em; - border-bottom: 2px solid var(--accent-glow); - padding-bottom: 0.3rem; -} - -.lesson-day-divider:first-of-type { - margin-top: 0; -} - -/* Загрузка/пусто */ -.loading-lessons, -.no-lessons { - text-align: center; - color: var(--text-secondary); - padding: 3rem; - font-size: 1rem; - background: var(--bg-card); - border-radius: var(--radius-sm); -} - -/* Светлая тема */ -[data-theme="light"] .lesson-card { - background: #fff; - border-color: rgba(0, 0, 0, 0.1); -} - -[data-theme="light"] .lesson-group { - background: rgba(99, 102, 241, 0.05); -} - -/* ===== Адаптивность ===== */ -@media (max-width: 1200px) { - #modal-view-lessons .view-lessons-modal { - width: 40vw !important; - max-width: 40vw !important; - } -} - -@media (max-width: 768px) { - /* На мобилке делаем поведение более "обычным" */ - #modal-view-lessons.modal-overlay.open { - padding-top: 1rem; - justify-content: center; - align-items: flex-start; - } - - #modal-view-lessons .view-lessons-modal { - width: 90vw !important; - max-width: 90vw !important; - min-width: 0; - - /* чтобы занимало почти весь экран */ - height: calc(100vh - 2rem); - max-height: calc(100vh - 2rem); - } - - .lesson-card-header { - flex-direction: column; - align-items: flex-start; - gap: 0.5rem; - } -} - -/* ===== Скролл во 2-й модалке ===== */ -#modal-view-lessons .lessons-container { - scrollbar-width: thin; /* Firefox */ - scrollbar-color: rgba(99, 102, 241, 0.55) rgba(255, 255, 255, 0.06); /* thumb track */ -} - -/* WebKit (Chrome/Edge/Safari) */ -#modal-view-lessons .lessons-container::-webkit-scrollbar { - width: 10px; -} - -#modal-view-lessons .lessons-container::-webkit-scrollbar-track { - background: rgba(255, 255, 255, 0.06); - border-radius: 10px; -} - -#modal-view-lessons .lessons-container::-webkit-scrollbar-thumb { - background: rgba(99, 102, 241, 0.55); /* под accent */ - border-radius: 10px; - border: 2px solid rgba(0, 0, 0, 0); /* чтобы выглядел “тоньше” */ - background-clip: padding-box; -} - -#modal-view-lessons .lessons-container::-webkit-scrollbar-thumb:hover { - background: rgba(99, 102, 241, 0.75); -} - -/* Общий блюр/затемнение за модалками */ -#modal-backdrop{ - position: fixed; - inset: 0; - background: rgba(0,0,0,0.55); - backdrop-filter: blur(6px); - -webkit-backdrop-filter: blur(6px); - - opacity: 0; - pointer-events: none; - transition: opacity var(--transition); - z-index: 998; /* ниже модалок: 999 и 1000 */ -} - -#modal-backdrop.open{ - opacity: 1; - pointer-events: auto; -} - diff --git a/frontend/admin/index.html b/frontend/admin/index.html index d499948..3881f85 100755 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -14,7 +14,6 @@ - @@ -54,17 +53,6 @@ Пользователи - - - - - - - - - Кафедра - @@ -204,4 +192,4 @@ - \ No newline at end of file + diff --git a/frontend/admin/js/main.js b/frontend/admin/js/main.js index 7fa36c4..596d309 100755 --- a/frontend/admin/js/main.js +++ b/frontend/admin/js/main.js @@ -26,7 +26,6 @@ import { initClassrooms } from './views/classrooms.js'; import { initSubjects } from './views/subjects.js'; import {initSchedule} from "./views/schedule.js"; import {initDatabase} from "./views/database.js"; -import {initDepartment} from "./views/department.js"; import {initDepartmentsData} from "./views/departments-data.js"; import {initAuditoriumWorkload} from "./views/auditorium-workload.js"; @@ -41,7 +40,6 @@ const ROUTES = { schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule }, 'auditorium-workload': { title: 'Загруженность аудиторий', file: 'views/auditorium-workload.html', init: initAuditoriumWorkload }, database: { title: 'База данных', file: 'views/database.html', init: initDatabase }, - department: { title: 'Кафедры', file: 'views/department.html', init: initDepartment }, 'departments-data': { title: 'Создание кафедры/специальности', file: 'views/departments-data.html', init: initDepartmentsData }, }; diff --git a/frontend/admin/js/views/auditorium-workload.js b/frontend/admin/js/views/auditorium-workload.js index f7c7629..bd3dc44 100644 --- a/frontend/admin/js/views/auditorium-workload.js +++ b/frontend/admin/js/views/auditorium-workload.js @@ -90,7 +90,7 @@ function renderMockGrid() { ]; // Mock schedule data mapped by room and time - // Key: "roomId_timeSlotId", Value: Lesson object + // Ключ: "roomId_timeSlotId", значение: объект занятия const mockSchedule = { "201_8:00-9:30": { subject: "Физика", group: "ИБ-41м", teacher: "Атлетов А.Р." }, "201_9:40-11:10": { subject: "Физика", group: "ИВТ-21-1", teacher: "Атлетов А.Р." }, diff --git a/frontend/admin/js/views/department.js b/frontend/admin/js/views/department.js deleted file mode 100644 index f1011c7..0000000 --- a/frontend/admin/js/views/department.js +++ /dev/null @@ -1,456 +0,0 @@ -import { api } from '../api.js'; -import { escapeHtml, showAlert, hideAlert } from '../utils.js'; - -// Ключ для хранения данных в sessionStorage -const STORAGE_KEY = 'department_schedule_blocks'; - -export async function initDepartment() { - const form = document.getElementById('department-schedule-form'); - const departmentSelect = document.getElementById('filter-department'); - const container = document.getElementById('schedule-blocks-container'); - - let departments = []; - - // Загрузка кафедр - try { - departments = await api.get('/api/departments'); - departmentSelect.innerHTML = '' + - departments.map(d => ``).join(''); - } catch (e) { - departmentSelect.innerHTML = ''; - } - - // ===== Восстанавливаем ранее загруженные таблицы из sessionStorage ===== - restoreScheduleBlocks(); - - form.addEventListener('submit', async (e) => { - e.preventDefault(); - hideAlert('schedule-form-alert'); - - const departmentId = departmentSelect.value; - const period = document.getElementById('filter-period').value; - const semesterType = document.querySelector('input[name="semesterType"]:checked')?.value; - - if (!departmentId || !period || !semesterType) { - showAlert('schedule-form-alert', 'Заполните все поля', 'error'); - return; - } - - const deptName = departmentSelect.options[departmentSelect.selectedIndex].text; - - try { - const params = new URLSearchParams({ departmentId, semesterType, period }); - const data = await api.get(`/api/department/schedule?${params.toString()}`); - - const semesterName = semesterType === 'spring' ? 'весенний' : (semesterType === 'autumn' ? 'осенний' : semesterType); - const periodName = period.replace('-', '/'); - - renderScheduleBlock(deptName, semesterName, periodName, data, departmentId, semesterType, period); - - // НЕ сбрасываем форму — фильтры остаются заполненными (fix #3) - - } catch (err) { - showAlert('schedule-form-alert', err.message || 'Ошибка загрузки данных', 'error'); - } - }); - - // ===== Уникальный ключ для таблицы по параметрам ===== - function blockKey(departmentId, semesterType, period) { - return `${departmentId}_${semesterType}_${period}`; - } - - // ===== Рендер блока таблицы (с дедупликацией — fix #6) ===== - function renderScheduleBlock(deptName, semester, period, schedule, departmentId, semesterType, rawPeriod) { - const key = blockKey(departmentId, semesterType, rawPeriod); - - // Удаляем ранее загруженный блок с тем же ключом - const existing = container.querySelector(`[data-block-key="${key}"]`); - if (existing) existing.remove(); - - const details = document.createElement('details'); - details.className = 'table-item'; - details.open = true; - details.setAttribute('data-block-key', key); - details.innerHTML = ` - - -
- Данные к составлению расписания - Кафедра: ${escapeHtml(deptName)} - Семестр: ${escapeHtml(semester)} - Уч. год: ${escapeHtml(period)} -
-
${Array.isArray(schedule) ? schedule.length : 0} записей
- -
- - - - - - - - - - - - - - - ${renderRows(schedule)} - -
СпециальностьКурс/семестрГруппаДисциплинаВид занятийЧасов в неделюДеление на подгруппыПреподаватель
-
- `; - - container.prepend(details); - - // Сохраняем в sessionStorage - saveScheduleBlock(key, { deptName, semester, period, schedule, departmentId, semesterType, rawPeriod }); - } - - function renderRows(schedule) { - if (!Array.isArray(schedule) || schedule.length === 0) { - return 'Нет данных'; - } - return schedule.map(r => ` - - ${escapeHtml(r.specialityCode || '-')} - ${(() => { - const course = r.groupCourse || '-'; - const semester = r.semester || '-'; - if (course === '-' && semester === '-') return '-'; - return `${course} | ${semester}`; - })()} - ${escapeHtml(r.groupName || '-')} - ${escapeHtml(r.subjectName || '-')} - ${escapeHtml(r.lessonType || '-')} - ${escapeHtml(r.numberOfHours || '-')} - ${r.division === true ? '✓' : ''} - ${(() => { - const jobTitle = r.teacherJobTitle || '-'; - const teacherName = r.teacherName || '-'; - if (jobTitle === '-' && teacherName === '-') return '-'; - return `${jobTitle}, ${teacherName}`; - })()} - - `).join(''); - } - - // ===== Persistence: sessionStorage (fix #4) ===== - function saveScheduleBlock(key, blockData) { - try { - const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}'); - stored[key] = blockData; - sessionStorage.setItem(STORAGE_KEY, JSON.stringify(stored)); - } catch (e) { - console.warn('Ошибка сохранения в sessionStorage:', e); - } - } - - function restoreScheduleBlocks() { - try { - const stored = JSON.parse(sessionStorage.getItem(STORAGE_KEY) || '{}'); - const keys = Object.keys(stored); - if (keys.length === 0) return; - - keys.forEach(key => { - const b = stored[key]; - renderScheduleBlock(b.deptName, b.semester, b.period, b.schedule, b.departmentId, b.semesterType, b.rawPeriod); - }); - } catch (e) { - console.warn('Ошибка восстановления из sessionStorage:', e); - } - } - - // ========================================================= - // ЛОГИКА ДЛЯ ФУНКЦИОНАЛА "СОЗДАТЬ ЗАПИСЬ (К/Ф)" - // Два модальных окна поверх всего контента в одном оверлее - // ========================================================= - const btnCreateSchedule = document.getElementById('btn-create-schedule'); - const csOverlay = document.getElementById('cs-overlay'); - - const modalCreateSchedule = document.getElementById('modal-create-schedule'); - const modalCreateScheduleClose = document.getElementById('modal-create-schedule-close'); - const formCreateSchedule = document.getElementById('create-schedule-form'); - - const modalViewSchedules = document.getElementById('modal-view-schedules'); - const btnSaveSchedules = document.getElementById('btn-save-schedules'); - const preparedSchedulesTbody = document.getElementById('prepared-schedules-tbody'); - - const csGroupSelect = document.getElementById('cs-group'); - const csSubjectSelect = document.getElementById('cs-subject'); - const csTeacherSelect = document.getElementById('cs-teacher'); - const csDepartmentIdInput = document.getElementById('cs-department-id'); - - let preparedSchedules = []; - let csGroups = []; - let csSubjects = []; - let csTeachers = []; - - const SEMESTER_LABELS = { autumn: 'Осенний', spring: 'Весенний' }; - const LESSON_TYPE_LABELS = { 1: 'Лекция', 2: 'Практическая работа', 3: 'Лабораторная работа' }; - - const localDepartmentId = localStorage.getItem('departmentId'); - - // ===== Загрузка справочников ===== - async function loadDictionariesForSchedule() { - try { - csGroups = await api.get('/api/groups'); - csGroupSelect.innerHTML = '' + - csGroups.map(g => ``).join(''); - - csSubjects = await api.get('/api/subjects'); - csSubjectSelect.innerHTML = '' + - csSubjects.map(s => ``).join(''); - - // Загрузка преподавателей: сначала по кафедре, при ошибке — все преподаватели - csTeachers = []; - if (localDepartmentId) { - try { - csTeachers = await api.get(`/api/users/teachers/${localDepartmentId}`); - } catch (e) { - console.warn('Не удалось загрузить преподавателей для кафедры, загружаем всех:', e); - } - } - // Фолбэк: загружаем всех преподавателей - if (!Array.isArray(csTeachers) || csTeachers.length === 0) { - try { - csTeachers = await api.get('/api/users/teachers'); - } catch (e2) { - console.error('Ошибка загрузки всех преподавателей:', e2); - } - } - if (Array.isArray(csTeachers) && csTeachers.length > 0) { - csTeacherSelect.innerHTML = '' + - csTeachers.map(t => ``).join(''); - } else { - csTeacherSelect.innerHTML = ''; - } - } catch (e) { - console.error('Ошибка загрузки справочников:', e); - } - } - - loadDictionariesForSchedule(); - - // ===== Открытие / Закрытие оверлея ===== - function openOverlay() { - csOverlay.classList.add('open'); - document.body.style.overflow = 'hidden'; - } - - function closeOverlay() { - csOverlay.classList.remove('open'); - document.body.style.overflow = ''; - hideAlert('create-schedule-alert'); - hideAlert('save-schedules-alert'); - } - - function updateTableVisibility() { - modalViewSchedules.style.display = preparedSchedules.length > 0 ? '' : 'none'; - } - - // ===== Кнопка «Создать запись» ===== - btnCreateSchedule.addEventListener('click', () => { - if (localDepartmentId) { - csDepartmentIdInput.value = localDepartmentId; - } else { - showAlert('schedule-form-alert', 'Требуется перезайти (отсутствует ID кафедры)', 'error'); - return; - } - openOverlay(); - }); - - // ===== Закрытие ===== - modalCreateScheduleClose.addEventListener('click', closeOverlay); - - csOverlay.addEventListener('click', (e) => { - if (e.target === csOverlay || e.target.classList.contains('cs-overlay-scroll')) { - closeOverlay(); - } - }); - - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && csOverlay.classList.contains('open')) { - closeOverlay(); - } - }); - - // ===== Рендер таблицы подготовленных записей ===== - function renderPreparedSchedules() { - if (preparedSchedules.length === 0) { - preparedSchedulesTbody.innerHTML = 'Нет записей'; - return; - } - preparedSchedulesTbody.innerHTML = preparedSchedules.map((s, index) => { - const groupName = csGroups.find(g => g.id == s.groupId)?.name || s.groupId; - const subjectName = csSubjects.find(sub => sub.id == s.subjectsId)?.name || s.subjectsId; - const teacherName = csTeachers.find(t => t.id == s.teacherId)?.fullName - || csTeachers.find(t => t.id == s.teacherId)?.username || s.teacherId; - const lessonTypeName = LESSON_TYPE_LABELS[s.lessonTypeId] || 'Неизвестно'; - const semLabel = SEMESTER_LABELS[s.semesterType] || s.semesterType; - const periodDisplay = s.period.replace('-', '/'); - const divText = s.isDivision ? '✓' : ''; - const hasError = !!s._errorMsg; - const rowStyle = hasError ? ' style="background: rgba(239, 68, 68, 0.08);"' : ''; - let row = ` - - ${escapeHtml(periodDisplay)} - ${escapeHtml(semLabel)} - ${escapeHtml(String(groupName))} - ${escapeHtml(String(subjectName))} - ${escapeHtml(lessonTypeName)} - ${s.numberOfHours} - ${divText} - ${escapeHtml(String(teacherName))} - - `; - if (hasError) { - row += ` - - ⚠ ${escapeHtml(s._errorMsg)} - - `; - } - return row; - }).join(''); - } - - // ===== Удаление строки из таблицы ===== - preparedSchedulesTbody.addEventListener('click', (e) => { - if (e.target.classList.contains('btn-delete')) { - const idx = parseInt(e.target.getAttribute('data-index'), 10); - preparedSchedules.splice(idx, 1); - renderPreparedSchedules(); - updateTableVisibility(); - } - }); - - // ===== Очистка полей формы (частичная) ===== - function clearFormFields() { - document.getElementById('cs-hours').value = ''; - document.getElementById('cs-division').checked = false; - } - - // ===== Добавление записи в список ===== - formCreateSchedule.addEventListener('submit', (e) => { - e.preventDefault(); - hideAlert('create-schedule-alert'); - - const depId = csDepartmentIdInput.value; - const period = document.getElementById('cs-period').value; - const semesterType = document.querySelector('input[name="csSemesterType"]:checked')?.value; - const groupId = csGroupSelect.value; - const subjectId = csSubjectSelect.value; - const lessonTypeId = document.getElementById('cs-lesson-type').value; - const hours = document.getElementById('cs-hours').value; - const isDivision = document.getElementById('cs-division').checked; - const teacherId = csTeacherSelect.value; - - if (!period || !semesterType || !groupId || !subjectId || !lessonTypeId || !hours || !teacherId) { - showAlert('create-schedule-alert', 'Заполните все обязательные поля', 'error'); - return; - } - - const newRecord = { - departmentId: Number(depId), - groupId: Number(groupId), - subjectsId: Number(subjectId), - lessonTypeId: Number(lessonTypeId), - numberOfHours: Number(hours), - isDivision: isDivision, - teacherId: Number(teacherId), - semesterType: semesterType, - period: period - }; - - // Проверка на дубликат - const isDuplicate = preparedSchedules.some(s => - s.period === newRecord.period && - s.semesterType === newRecord.semesterType && - s.groupId === newRecord.groupId && - s.subjectsId === newRecord.subjectsId && - s.lessonTypeId === newRecord.lessonTypeId && - s.numberOfHours === newRecord.numberOfHours && - s.isDivision === newRecord.isDivision && - s.teacherId === newRecord.teacherId - ); - - if (isDuplicate) { - showAlert('create-schedule-alert', 'Такая запись уже есть в списке', 'error'); - return; - } - - preparedSchedules.push(newRecord); - - clearFormFields(); - - showAlert('create-schedule-alert', 'Запись добавлена ✓', 'success'); - setTimeout(() => hideAlert('create-schedule-alert'), 4000); // fix #1: 4 секунды - - renderPreparedSchedules(); - updateTableVisibility(); - }); - - // ===== Сохранение в БД ===== - btnSaveSchedules.addEventListener('click', async () => { - if (preparedSchedules.length === 0) { - showAlert('save-schedules-alert', 'Нет записей для сохранения', 'error'); - return; - } - - btnSaveSchedules.disabled = true; - btnSaveSchedules.textContent = 'Сохранение...'; - hideAlert('save-schedules-alert'); - - let errors = 0; - let saved = 0; - const failedRecords = []; - - for (const record of preparedSchedules) { - try { - await api.post('/api/department/schedule/create', record); - saved++; - } catch (err) { - console.error('Ошибка сохранения записи:', err); - errors++; - const isDuplicate = err.status === 409 || - (err.message && err.message.toLowerCase().includes('уже существует')); - failedRecords.push({ - ...record, - _errorMsg: isDuplicate - ? 'Такая запись уже есть в базе данных' - : (err.message || 'Ошибка сохранения') - }); - } - } - - btnSaveSchedules.disabled = false; - btnSaveSchedules.textContent = 'Сохранить в БД'; - - if (errors === 0) { - showAlert('save-schedules-alert', `Все записи (${saved}) успешно сохранены!`, 'success'); - preparedSchedules = []; - renderPreparedSchedules(); - updateTableVisibility(); - setTimeout(closeOverlay, 2000); - } else { - preparedSchedules = failedRecords; - renderPreparedSchedules(); - if (saved > 0) { - showAlert('save-schedules-alert', - `Сохранено: ${saved}. Ошибок: ${errors}. Проблемные записи отмечены в таблице.`, 'error'); - } else { - showAlert('save-schedules-alert', - `Не удалось сохранить. Ошибок: ${errors}. Проблемные записи отмечены в таблице.`, 'error'); - } - } - }); - -} \ No newline at end of file diff --git a/frontend/admin/js/views/departments-data.js b/frontend/admin/js/views/departments-data.js index 489a188..2e2e64f 100644 --- a/frontend/admin/js/views/departments-data.js +++ b/frontend/admin/js/views/departments-data.js @@ -7,6 +7,12 @@ export async function initDepartmentsData() { const createDeptForm = document.getElementById('create-department-form'); const createSpecForm = document.getElementById('create-specialty-form'); + const editDeptForm = document.getElementById('edit-department-form'); + const editSpecForm = document.getElementById('edit-specialty-form'); + const editDeptModal = document.getElementById('modal-edit-department'); + const editSpecModal = document.getElementById('modal-edit-specialty'); + const editDeptClose = document.getElementById('modal-edit-department-close'); + const editSpecClose = document.getElementById('modal-edit-specialty-close'); let departments = []; let specialties = []; @@ -17,7 +23,7 @@ export async function initDepartmentsData() { departments = await api.get('/api/departments'); renderDepartments(); } catch (e) { - deptTbody.innerHTML = '-'; + deptTbody.innerHTML = '-'; } // Load Specialties @@ -25,13 +31,13 @@ export async function initDepartmentsData() { specialties = await api.get('/api/specialties'); renderSpecialties(); } catch (e) { - specTbody.innerHTML = '-'; + specTbody.innerHTML = '-'; } } function renderDepartments() { if (!departments || !departments.length) { - deptTbody.innerHTML = '-'; + deptTbody.innerHTML = '-'; return; } deptTbody.innerHTML = departments.map(d => ` @@ -39,13 +45,17 @@ export async function initDepartmentsData() { ${d.id} ${escapeHtml(d.departmentName || d.name)} ${escapeHtml(String(d.departmentCode || d.code))} + + + + `).join(''); } function renderSpecialties() { if (!specialties || !specialties.length) { - specTbody.innerHTML = '-'; + specTbody.innerHTML = '-'; return; } specTbody.innerHTML = specialties.map(s => ` @@ -53,6 +63,10 @@ export async function initDepartmentsData() { ${s.id} ${escapeHtml(s.specialityName || s.name)} ${escapeHtml(s.specialityCode || s.specialtyCode || s.specialty_code)} + + + + `).join(''); } @@ -78,6 +92,34 @@ export async function initDepartmentsData() { } }); + deptTbody.addEventListener('click', async (e) => { + const editBtn = e.target.closest('.btn-edit-department'); + const deleteBtn = e.target.closest('.btn-delete-department'); + + if (editBtn) { + const department = departments.find(item => item.id == editBtn.dataset.id); + if (!department) return; + + document.getElementById('edit-dept-id').value = department.id; + document.getElementById('edit-dept-name').value = department.departmentName || department.name || ''; + document.getElementById('edit-dept-code').value = department.departmentCode || department.code || ''; + hideAlert('edit-dept-alert'); + editDeptModal.classList.add('open'); + return; + } + + if (deleteBtn) { + if (!confirm('Удалить кафедру?')) return; + try { + await api.delete('/api/departments/' + deleteBtn.dataset.id); + showAlert('create-dept-alert', 'Кафедра удалена', 'success'); + await loadData(); + } catch (error) { + showAlert('create-dept-alert', error.message || 'Ошибка удаления кафедры', 'error'); + } + } + }); + createSpecForm.addEventListener('submit', async (e) => { e.preventDefault(); hideAlert('create-spec-alert'); @@ -99,5 +141,86 @@ export async function initDepartmentsData() { } }); - loadData(); + specTbody.addEventListener('click', async (e) => { + const editBtn = e.target.closest('.btn-edit-specialty'); + const deleteBtn = e.target.closest('.btn-delete-specialty'); + + if (editBtn) { + const speciality = specialties.find(item => item.id == editBtn.dataset.id); + if (!speciality) return; + + document.getElementById('edit-spec-id').value = speciality.id; + document.getElementById('edit-spec-name').value = speciality.specialityName || speciality.name || ''; + document.getElementById('edit-spec-code').value = speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code || ''; + hideAlert('edit-spec-alert'); + editSpecModal.classList.add('open'); + return; + } + + if (deleteBtn) { + if (!confirm('Удалить специальность?')) return; + try { + await api.delete('/api/specialties/' + deleteBtn.dataset.id); + showAlert('create-spec-alert', 'Специальность удалена', 'success'); + await loadData(); + } catch (error) { + showAlert('create-spec-alert', error.message || 'Ошибка удаления специальности', 'error'); + } + } + }); + + editDeptForm.addEventListener('submit', async (e) => { + e.preventDefault(); + hideAlert('edit-dept-alert'); + const id = document.getElementById('edit-dept-id').value; + const name = document.getElementById('edit-dept-name').value.trim(); + const code = document.getElementById('edit-dept-code').value.trim(); + + if (!name || !code) { + showAlert('edit-dept-alert', 'Заполните все поля', 'error'); + return; + } + + try { + await api.put('/api/departments/' + id, { departmentName: name, departmentCode: Number(code) }); + editDeptModal.classList.remove('open'); + showAlert('create-dept-alert', `Кафедра "${name}" обновлена`, 'success'); + await loadData(); + } catch (error) { + showAlert('edit-dept-alert', error.message || 'Ошибка обновления кафедры', 'error'); + } + }); + + editSpecForm.addEventListener('submit', async (e) => { + e.preventDefault(); + hideAlert('edit-spec-alert'); + const id = document.getElementById('edit-spec-id').value; + const name = document.getElementById('edit-spec-name').value.trim(); + const code = document.getElementById('edit-spec-code').value.trim(); + + if (!name || !code) { + showAlert('edit-spec-alert', 'Заполните все поля', 'error'); + return; + } + + try { + await api.put('/api/specialties/' + id, { specialityName: name, specialityCode: code }); + editSpecModal.classList.remove('open'); + showAlert('create-spec-alert', `Специальность "${name}" обновлена`, 'success'); + await loadData(); + } catch (error) { + showAlert('edit-spec-alert', error.message || 'Ошибка обновления специальности', 'error'); + } + }); + + editDeptClose.addEventListener('click', () => editDeptModal.classList.remove('open')); + editSpecClose.addEventListener('click', () => editSpecModal.classList.remove('open')); + editDeptModal.addEventListener('click', (e) => { + if (e.target === editDeptModal) editDeptModal.classList.remove('open'); + }); + editSpecModal.addEventListener('click', (e) => { + if (e.target === editSpecModal) editSpecModal.classList.remove('open'); + }); + + await loadData(); } diff --git a/frontend/admin/js/views/groups.js b/frontend/admin/js/views/groups.js index ec57a24..a8a65d8 100755 --- a/frontend/admin/js/views/groups.js +++ b/frontend/admin/js/views/groups.js @@ -6,15 +6,25 @@ export async function initGroups() { const groupsTbody = document.getElementById('groups-tbody'); const createGroupForm = document.getElementById('create-group-form'); const newGroupEfSelect = document.getElementById('new-group-ef'); + const newGroupDepartmentSelect = document.getElementById('new-group-department'); + const newGroupSpecialitySelect = document.getElementById('new-group-speciality-code'); const filterEfSelect = document.getElementById('filter-ef'); let allGroups = []; let educationForms = []; + let departments = []; + let specialties = []; async function loadInitialData() { try { - educationForms = await fetchEducationForms(); + [educationForms, departments, specialties] = await Promise.all([ + fetchEducationForms(), + api.get('/api/departments'), + api.get('/api/specialties') + ]); populateEfSelects(educationForms); + populateDepartmentSelect(departments); + populateSpecialitySelect(specialties); await loadGroups(); } catch (e) { groupsTbody.innerHTML = 'Ошибка загрузки данных'; @@ -49,6 +59,7 @@ export async function initGroups() { if (currentVal && forms.find(f => f.id == currentVal)) { newGroupEfSelect.value = currentVal; } + syncSelects(newGroupEfSelect); // Filter select const currentFilter = filterEfSelect.value; @@ -57,6 +68,59 @@ export async function initGroups() { `` ).join(''); if (currentFilter) filterEfSelect.value = currentFilter; + syncSelects(filterEfSelect); + } + + function populateDepartmentSelect(items) { + const currentVal = newGroupDepartmentSelect.value; + if (!items || !items.length) { + newGroupDepartmentSelect.innerHTML = ''; + return; + } + + newGroupDepartmentSelect.innerHTML = '' + + items.map(department => { + const name = department.departmentName || department.name || 'Без названия'; + const code = department.departmentCode || department.code || department.id; + return ``; + }).join(''); + if (currentVal && items.find(department => department.id == currentVal)) { + newGroupDepartmentSelect.value = currentVal; + } + syncSelects(newGroupDepartmentSelect); + } + + function populateSpecialitySelect(items) { + const currentVal = newGroupSpecialitySelect.value; + if (!items || !items.length) { + newGroupSpecialitySelect.innerHTML = ''; + return; + } + + newGroupSpecialitySelect.innerHTML = '' + + items.map(speciality => { + const name = speciality.specialityName || speciality.name || 'Без названия'; + const code = speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code || speciality.id; + return ``; + }).join(''); + if (currentVal && items.find(speciality => speciality.id == currentVal)) { + newGroupSpecialitySelect.value = currentVal; + } + syncSelects(newGroupSpecialitySelect); + } + + function departmentLabel(departmentId) { + const department = departments.find(item => item.id == departmentId); + if (!department) return departmentId || '-'; + return department.departmentName || department.name || departmentId; + } + + function specialityLabel(specialityId) { + const speciality = specialties.find(item => item.id == specialityId); + if (!speciality) return specialityId || '-'; + const code = speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code || speciality.id; + const name = speciality.specialityName || speciality.name || ''; + return name ? `${code} — ${name}` : code; } function renderGroups(groups) { @@ -70,9 +134,9 @@ export async function initGroups() { ${escapeHtml(g.name)} ${escapeHtml(g.groupSize)} ${escapeHtml(g.educationFormName)} - ${g.departmentId || '-'} + ${escapeHtml(departmentLabel(g.departmentId))} ${g.course || '-'} - ${escapeHtml(g.specialityCode || '-')} + ${escapeHtml(specialityLabel(g.specialityCode))} `).join(''); } @@ -83,16 +147,16 @@ export async function initGroups() { const name = document.getElementById('new-group-name').value.trim(); const groupSize = document.getElementById('new-group-size').value; const educationFormId = newGroupEfSelect.value; - const departmentId = document.getElementById('new-group-department').value; - const course = document.getElementById('new-group-course').value; - const specialityCode = document.getElementById('new-group-speciality-code').value.trim(); + const departmentId = newGroupDepartmentSelect.value; + const yearStartStudy = document.getElementById('new-group-yearStartStudy').value; + const specialityCode = newGroupSpecialitySelect.value; if (!name) { showAlert('create-group-alert', 'Введите название группы', 'error'); return; } if (!groupSize) { showAlert('create-group-alert', 'Введите размер группы', 'error'); return; } if (!educationFormId) { showAlert('create-group-alert', 'Выберите форму обучения', 'error'); return; } - if (!departmentId) { showAlert('create-group-alert', 'Введите ID кафедры', 'error'); return; } - if (!course) { showAlert('create-group-alert', 'Введите курс', 'error'); return; } - if (!specialityCode) { showAlert('create-group-alert', 'Введите код специальности', 'error'); return; } + if (!departmentId) { showAlert('create-group-alert', 'Выберите кафедру', 'error'); return; } + if (!yearStartStudy) { showAlert('create-group-alert', 'Введите год начала обучения', 'error'); return; } + if (!specialityCode) { showAlert('create-group-alert', 'Выберите специальность', 'error'); return; } try { const data = await api.post('/api/groups', { @@ -100,11 +164,12 @@ export async function initGroups() { groupSize: Number(groupSize), educationFormId: Number(educationFormId), departmentId: Number(departmentId), - course: Number(course), - specialityCode: specialityCode + yearStartStudy: Number(yearStartStudy), + specialityCode: Number(specialityCode) }); showAlert('create-group-alert', `Группа "${escapeHtml(data.name || name)}" создана`, 'success'); createGroupForm.reset(); + syncSelects(newGroupEfSelect, newGroupDepartmentSelect, newGroupSpecialitySelect); loadGroups(); } catch (e) { showAlert('create-group-alert', e.message || 'Ошибка создания', 'error'); @@ -123,5 +188,11 @@ export async function initGroups() { } }); - loadInitialData(); + await loadInitialData(); + + function syncSelects(...selects) { + selects.filter(Boolean).forEach(select => { + select.dispatchEvent(new Event('change', { bubbles: true })); + }); + } } diff --git a/frontend/admin/js/views/schedule.js b/frontend/admin/js/views/schedule.js old mode 100755 new mode 100644 index 57ca394..3c867ab --- a/frontend/admin/js/views/schedule.js +++ b/frontend/admin/js/views/schedule.js @@ -1,684 +1,904 @@ import { api } from '../api.js'; import { escapeHtml, showAlert, hideAlert } from '../utils.js'; +const DAY_OPTIONS = [ + { value: 1, label: 'Понедельник' }, + { value: 2, label: 'Вторник' }, + { value: 3, label: 'Среда' }, + { value: 4, label: 'Четверг' }, + { value: 5, label: 'Пятница' }, + { value: 6, label: 'Суббота' }, + { value: 7, label: 'Воскресенье' } +]; + +const PARITY_LABELS = { + BOTH: 'каждую неделю', + ODD: 'нечётная', + EVEN: 'чётная' +}; + +const SEMESTER_LABELS = { + autumn: 'осенний', + spring: 'весенний' +}; + +const ACTIVITY_LABELS = { + THEORY: 'Теория', + EXAM: 'Экзамены', + VACATION: 'Каникулы', + PRACTICE: 'Практика' +}; + export async function initSchedule() { - const tbody = document.getElementById('schedule-tbody'); - const table = document.getElementById('schedule-table'); + const ruleForm = document.getElementById('schedule-rule-form'); + const ruleFormTitle = document.getElementById('schedule-rule-form-title'); + const ruleIdInput = document.getElementById('schedule-rule-id'); + const ruleSubjectSelect = document.getElementById('schedule-rule-subject'); + const ruleSemesterSelect = document.getElementById('schedule-rule-semester'); + const ruleActiveFromInput = document.getElementById('schedule-rule-active-from'); + const ruleHoursInput = document.getElementById('schedule-rule-hours'); + const ruleGroupsContainer = document.getElementById('schedule-rule-groups'); + const ruleSlotsContainer = document.getElementById('schedule-rule-slots'); + const ruleResetButton = document.getElementById('schedule-rule-reset'); + const slotAddButton = document.getElementById('schedule-slot-add'); + const rulesTbody = document.getElementById('schedule-tbody'); + const refreshRulesButton = document.getElementById('schedule-refresh'); - let lessonsData = []; - let sortKey = null; - let sortDir = 'asc'; + const timeSlotForm = document.getElementById('time-slot-form'); + const timeSlotFormTitle = document.getElementById('time-slot-form-title'); + const timeSlotIdInput = document.getElementById('time-slot-id'); + const timeSlotOrderInput = document.getElementById('time-slot-order'); + const timeSlotStartInput = document.getElementById('time-slot-start'); + const timeSlotEndInput = document.getElementById('time-slot-end'); + const timeSlotDurationInput = document.getElementById('time-slot-duration'); + const timeSlotResetButton = document.getElementById('time-slot-reset'); + const timeSlotsTbody = document.getElementById('time-slots-tbody'); + const refreshSlotsButton = document.getElementById('time-slots-refresh'); - // Активные фильтры: { teacher: Set, group: Set, subject: Set, day: Set } - const activeFilters = {}; + const academicYearForm = document.getElementById('academic-year-form'); + const academicYearFormTitle = document.getElementById('academic-year-form-title'); + const academicYearIdInput = document.getElementById('academic-year-id'); + const academicYearTitleInput = document.getElementById('academic-year-title'); + const academicYearStartInput = document.getElementById('academic-year-start'); + const academicYearEndInput = document.getElementById('academic-year-end'); + const academicYearResetButton = document.getElementById('academic-year-reset'); + const academicYearsTbody = document.getElementById('academic-years-tbody'); + const academicYearsRefreshButton = document.getElementById('academic-years-refresh'); - // Маппинг дней недели для корректной сортировки - const dayOrder = { - 'понедельник': 1, 'вторник': 2, 'среда': 3, - 'четверг': 4, 'пятница': 5, 'суббота': 6, 'воскресенье': 7 - }; + const semesterForm = document.getElementById('semester-form'); + const semesterFormTitle = document.getElementById('semester-form-title'); + const semesterIdInput = document.getElementById('semester-id'); + const semesterYearSelect = document.getElementById('semester-year'); + const semesterTypeSelect = document.getElementById('semester-type'); + const semesterStartInput = document.getElementById('semester-start'); + const semesterEndInput = document.getElementById('semester-end'); + const semesterResetButton = document.getElementById('semester-reset'); - // ===================== Фильтрация ===================== + const holidayForm = document.getElementById('holiday-form'); + const holidayIdInput = document.getElementById('holiday-id'); + const holidayYearSelect = document.getElementById('holiday-year'); + const holidayDateInput = document.getElementById('holiday-date'); + const holidayDescriptionInput = document.getElementById('holiday-description'); + const holidayResetButton = document.getElementById('holiday-reset'); + const holidaysTbody = document.getElementById('holidays-tbody'); - function getDisplayValue(lesson, key) { - switch (key) { - case 'teacher': - return lesson.teacher?.username || lesson.teacherName || '—'; - case 'group': - return lesson.group?.name || lesson.groupName || '—'; - case 'subject': - return lesson.subject?.name || lesson.subjectName || '—'; - case 'day': - return lesson.day || '—'; - case 'educationForm': - return lesson.educationForm?.name || lesson.educationFormName || '—'; - default: - return ''; - } + const matrixSemesterSelect = document.getElementById('matrix-semester'); + const matrixCourseInput = document.getElementById('matrix-course'); + const matrixSpecialtySelect = document.getElementById('matrix-specialty'); + const matrixLoadButton = document.getElementById('matrix-load'); + const matrixSaveButton = document.getElementById('matrix-save'); + const matrixGrid = document.getElementById('matrix-grid'); + + let rules = []; + let subjects = []; + let groups = []; + let teachers = []; + let classrooms = []; + let lessonTypes = []; + let timeSlots = []; + let academicYears = []; + let semesters = []; + let specialties = []; + let holidays = []; + + bindEvents(); + + try { + await Promise.all([loadBaseLists(), loadTimeSlots(), loadYears()]); + renderRuleGroups([]); + renderRuleSlots([{}]); + await Promise.all([loadRules(), loadHolidays()]); + } catch (error) { + showAlert('schedule-rule-alert', error.message || 'Ошибка загрузки данных расписания', 'error'); } - function getUniqueValues(key) { - const vals = new Set(); - lessonsData.forEach(lesson => { - vals.add(getDisplayValue(lesson, key)); + function bindEvents() { + refreshRulesButton?.addEventListener('click', loadRules); + refreshSlotsButton?.addEventListener('click', loadTimeSlots); + academicYearsRefreshButton?.addEventListener('click', loadYears); + ruleResetButton?.addEventListener('click', resetRuleForm); + slotAddButton?.addEventListener('click', () => renderRuleSlots([...readRuleSlots(false), {}])); + timeSlotResetButton?.addEventListener('click', resetTimeSlotForm); + academicYearResetButton?.addEventListener('click', resetAcademicYearForm); + semesterResetButton?.addEventListener('click', resetSemesterForm); + holidayResetButton?.addEventListener('click', resetHolidayForm); + holidayYearSelect?.addEventListener('change', loadHolidays); + matrixLoadButton?.addEventListener('click', loadMatrix); + matrixSaveButton?.addEventListener('click', saveMatrix); + + ruleSlotsContainer.addEventListener('click', (event) => { + const removeButton = event.target.closest('.schedule-slot-remove'); + if (!removeButton) return; + const slots = readRuleSlots(false); + slots.splice(Number(removeButton.dataset.index), 1); + renderRuleSlots(slots.length ? slots : [{}]); }); - if (key === 'day') { - return [...vals].sort((a, b) => (dayOrder[a.toLowerCase()] ?? 99) - (dayOrder[b.toLowerCase()] ?? 99)); - } - return [...vals].sort((a, b) => a.localeCompare(b, 'ru')); + + ruleForm.addEventListener('submit', saveRule); + timeSlotForm.addEventListener('submit', saveTimeSlot); + academicYearForm.addEventListener('submit', saveAcademicYear); + semesterForm.addEventListener('submit', saveSemester); + holidayForm.addEventListener('submit', saveHoliday); + + rulesTbody.addEventListener('click', handleRuleTableClick); + timeSlotsTbody.addEventListener('click', handleTimeSlotTableClick); + academicYearsTbody.addEventListener('click', handleAcademicYearTableClick); + holidaysTbody.addEventListener('click', handleHolidayTableClick); } - function applyFilters(lessons) { - return lessons.filter(lesson => { - for (const key of Object.keys(activeFilters)) { - const filterSet = activeFilters[key]; - if (filterSet && filterSet.size > 0) { - const val = getDisplayValue(lesson, key); - if (!filterSet.has(val)) return false; - } - } - return true; - }); + async function loadBaseLists() { + [subjects, groups, teachers, classrooms, lessonTypes, specialties] = await Promise.all([ + api.get('/api/subjects'), + api.get('/api/groups'), + api.get('/api/users/teachers'), + api.get('/api/classrooms'), + api.get('/api/lesson-types'), + api.get('/api/specialties') + ]); + + populateSubjectSelects(); + populateSpecialtySelects(); } - // ===================== Попап фильтра ===================== - - let currentPopup = null; - - function closePopup() { - if (currentPopup) { - currentPopup.remove(); - currentPopup = null; - } - document.removeEventListener('click', onDocumentClick, true); - } - - function onDocumentClick(e) { - if (currentPopup && !currentPopup.contains(e.target)) { - if (!e.target.closest('.filter-icon')) { - closePopup(); - } - } - } - - function openFilterPopup(th, filterKey) { - if (currentPopup && currentPopup.dataset.filterKey === filterKey) { - closePopup(); - return; - } - closePopup(); - - const uniqueValues = getUniqueValues(filterKey); - const currentFilter = activeFilters[filterKey]; - - const popup = document.createElement('div'); - popup.className = 'filter-popup'; - popup.dataset.filterKey = filterKey; - - const searchInput = document.createElement('input'); - searchInput.type = 'text'; - searchInput.className = 'filter-search'; - searchInput.placeholder = 'Поиск...'; - popup.appendChild(searchInput); - - const btnRow = document.createElement('div'); - btnRow.className = 'filter-btn-row'; - - const btnAll = document.createElement('button'); - btnAll.className = 'filter-btn-action'; - btnAll.textContent = 'Все'; - btnAll.addEventListener('click', (e) => { - e.stopPropagation(); - checkboxes.forEach(cb => { cb.checked = true; }); - }); - - const btnNone = document.createElement('button'); - btnNone.className = 'filter-btn-action'; - btnNone.textContent = 'Сбросить'; - btnNone.addEventListener('click', (e) => { - e.stopPropagation(); - checkboxes.forEach(cb => { cb.checked = false; }); - }); - - btnRow.appendChild(btnAll); - btnRow.appendChild(btnNone); - popup.appendChild(btnRow); - - const listWrap = document.createElement('div'); - listWrap.className = 'filter-list'; - - const checkboxes = []; - - uniqueValues.forEach(val => { - const label = document.createElement('label'); - label.className = 'filter-item'; - - const cb = document.createElement('input'); - cb.type = 'checkbox'; - cb.value = val; - cb.checked = currentFilter ? currentFilter.has(val) : true; - - const span = document.createElement('span'); - span.textContent = val; - - label.appendChild(cb); - label.appendChild(span); - listWrap.appendChild(label); - checkboxes.push(cb); - }); - - popup.appendChild(listWrap); - - const btnApply = document.createElement('button'); - btnApply.className = 'filter-btn-apply'; - btnApply.textContent = 'Применить'; - btnApply.addEventListener('click', (e) => { - e.stopPropagation(); - const selected = new Set(); - checkboxes.forEach(cb => { - if (cb.checked) selected.add(cb.value); - }); - - if (selected.size === uniqueValues.length) { - delete activeFilters[filterKey]; - th.classList.remove('filter-active'); - } else { - activeFilters[filterKey] = selected; - th.classList.add('filter-active'); - } - - closePopup(); - renderSchedule(lessonsData); - }); - popup.appendChild(btnApply); - - searchInput.addEventListener('input', () => { - const query = searchInput.value.toLowerCase(); - listWrap.querySelectorAll('.filter-item').forEach(item => { - const text = item.querySelector('span').textContent.toLowerCase(); - item.style.display = text.includes(query) ? '' : 'none'; - }); - }); - - popup.addEventListener('click', (e) => e.stopPropagation()); - searchInput.addEventListener('click', (e) => e.stopPropagation()); - - th.style.position = 'relative'; - th.appendChild(popup); - currentPopup = popup; - - setTimeout(() => searchInput.focus(), 50); - - setTimeout(() => { - document.addEventListener('click', onDocumentClick, true); - }, 10); - } - - table.querySelectorAll('thead th.filterable').forEach(th => { - th.addEventListener('click', (e) => { - if (e.target.closest('.filter-popup')) return; - const filterKey = th.dataset.filterKey; - openFilterPopup(th, filterKey); - }); - }); - - // ===================== Сортировка ===================== - - function getSortValue(lesson, key) { - switch (key) { - case 'id': - return lesson.id ?? 0; - case 'teacher': - return (lesson.teacher?.username || lesson.teacherName || '').toLowerCase(); - case 'group': - return (lesson.group?.name || lesson.groupName || '').toLowerCase(); - case 'classroomName': - return (lesson.classroomName?.name || lesson.classroomName || '').toLowerCase(); - case 'educationForm': - return (lesson.educationForm?.name || lesson.educationFormName || '').toLowerCase(); - case 'subject': - return (lesson.subject?.name || lesson.subjectName || '').toLowerCase(); - case 'lessonFormat': - return (lesson.lessonFormat?.name || lesson.lessonFormat || '').toLowerCase(); - case 'typeLesson': - return (lesson.typeLesson?.name || lesson.typeLesson || '').toLowerCase(); - case 'day': { - const d = (lesson.day || '').toLowerCase(); - return dayOrder[d] ?? 99; - } - case 'week': - return (lesson.week || '').toLowerCase(); - case 'time': { - const d = (lesson.day || '').toLowerCase(); - const dayNum = dayOrder[d] ?? 99; - const t = lesson.time || '99:99'; - return String(dayNum).padStart(2, '0') + '_' + t; - } - default: - return ''; - } - } - - function sortLessons(lessons) { - if (!sortKey) return lessons; - - return [...lessons].sort((a, b) => { - let va = getSortValue(a, sortKey); - let vb = getSortValue(b, sortKey); - - if (typeof va === 'number' && typeof vb === 'number') { - return sortDir === 'asc' ? va - vb : vb - va; - } - - va = String(va); - vb = String(vb); - const cmp = va.localeCompare(vb, 'ru'); - return sortDir === 'asc' ? cmp : -cmp; - }); - } - - function updateSortHeaders() { - table.querySelectorAll('thead th.sortable').forEach(th => { - th.classList.remove('sort-asc', 'sort-desc', 'sort-active'); - if (th.dataset.sortKey === sortKey) { - th.classList.add('sort-active', sortDir === 'asc' ? 'sort-asc' : 'sort-desc'); - } - }); - } - - table.querySelectorAll('thead th.sortable').forEach(th => { - th.addEventListener('click', (e) => { - if (e.target.closest('.filter-icon') || e.target.closest('.filter-popup')) return; - - const key = th.dataset.sortKey; - if (sortKey === key) { - if (sortDir === 'asc') { - sortDir = 'desc'; - } else { - sortKey = null; - sortDir = 'asc'; - } - } else { - sortKey = key; - sortDir = 'asc'; - } - updateSortHeaders(); - renderSchedule(lessonsData); - }); - }); - - // ===================== Загрузка и рендер таблицы ===================== - - async function loadSchedule() { + async function loadRules() { + rulesTbody.innerHTML = 'Загрузка...'; try { - const lessons = await api.get('/api/users/lessons'); - lessonsData = lessons; - renderSchedule(lessons); - } catch (e) { - tbody.innerHTML = `Ошибка загрузки: ${escapeHtml(e.message)}`; + rules = await api.get('/api/admin/schedule-rules'); + renderRules(); + } catch (error) { + rulesTbody.innerHTML = `Ошибка загрузки: ${escapeHtml(error.message)}`; } } - function renderSchedule(lessons) { - if (!lessons || !lessons.length) { - tbody.innerHTML = 'Нет занятий'; + function renderRules() { + if (!rules || !rules.length) { + rulesTbody.innerHTML = 'Правила расписания не созданы'; return; } - const filtered = applyFilters(lessons); + rulesTbody.innerHTML = rules.map(rule => ` + + ${rule.id} + ${escapeHtml(rule.subjectName || '-')} + ${escapeHtml(formatSemester(rule))} + ${escapeHtml((rule.groupNames || []).join(', ') || '-')} + ${escapeHtml(rule.activeFromDate || '-')} + ${escapeHtml(String(rule.totalAcademicHours ?? '-'))} + ${renderRuleSlotsSummary(rule.slots)} + + + + + + `).join(''); + } - if (!filtered.length) { - tbody.innerHTML = 'Нет занятий по выбранным фильтрам'; - return; - } + function renderRuleSlotsSummary(slots) { + if (!slots || !slots.length) return 'Нет слотов'; - const sorted = sortLessons(filtered); - - tbody.innerHTML = sorted.map(lesson => { - const teacherName = lesson.teacher?.username || lesson.teacherName || '—'; - const groupName = lesson.group?.name || lesson.groupName || '—'; - const classroomName = lesson.classroom?.name || lesson.classroomName || '—'; - const educationForm = lesson.educationForm?.name || lesson.educationFormName || '-'; - const subjectName = lesson.subject?.name || lesson.subjectName || '—'; - const lessonFormat = lesson.lessonFormat?.name || lesson.lessonFormat || '—'; - const typeLesson = lesson.typeLesson?.name || lesson.typeLesson || '—'; - const day = lesson.day || '—'; - const week = lesson.week || '—'; - const time = lesson.time || '—'; - - return ` - ${escapeHtml(lesson.id)} - ${escapeHtml(teacherName)} - ${escapeHtml(groupName)} - ${escapeHtml(classroomName)} - ${escapeHtml(educationForm)} - ${escapeHtml(subjectName)} - ${escapeHtml(lessonFormat)} - ${escapeHtml(typeLesson)} - ${escapeHtml(day)} - ${escapeHtml(week)} - ${escapeHtml(time)} - `; + return slots.map(slot => { + const parity = PARITY_LABELS[slot.parity] || slot.parity || '-'; + const parts = [ + slot.dayName, + slot.timeSlotLabel, + parity, + slot.teacherName, + slot.classroomName, + slot.lessonTypeName, + slot.lessonFormat + ].filter(Boolean); + return `
${escapeHtml(parts.join(' · '))}
`; }).join(''); } - // ===================== Модалки добавления занятия ===================== + async function handleRuleTableClick(event) { + const editButton = event.target.closest('.btn-edit-rule'); + const deleteButton = event.target.closest('.btn-delete-rule'); - const overlay = document.getElementById('sch-overlay'); - const modalForm = document.getElementById('sch-modal-form'); - const modalLessons = document.getElementById('sch-modal-lessons'); - const btnAddLesson = document.getElementById('sch-btn-add-lesson'); - const btnClose = document.getElementById('sch-modal-close'); - const addForm = document.getElementById('sch-add-lesson-form'); - - const schTeacherSelect = document.getElementById('sch-teacher'); - const schGroupSelect = document.getElementById('sch-group'); - const schDisciplineSelect = document.getElementById('sch-discipline'); - const schClassroomSelect = document.getElementById('sch-classroom'); - const schDaySelect = document.getElementById('sch-day'); - const schTimeSelect = document.getElementById('sch-time'); - const schTypeSelect = document.getElementById('sch-type'); - const schWeekUpper = document.getElementById('sch-week-upper'); - const schWeekLower = document.getElementById('sch-week-lower'); - const schFormatOffline = document.getElementById('sch-format-offline'); - - const schTeacherName = document.getElementById('sch-teacher-name'); - const schLessonsContainer = document.getElementById('sch-lessons-container'); - - let groups = []; - let subjects = []; - let classrooms = []; - let teachers = []; - - const weekdaysTimes = [ - "8:00-9:30", "9:40-11:10", "11:40-13:10", - "13:20-14:50", "15:00-16:30", "16:50-18:20", "18:30-19:00" - ]; - const saturdayTimes = [ - "8:20-9:50", "10:00-11:30", "11:40-13:10", "13:20-14:50" - ]; - - // ===== Загрузка справочников ===== - async function loadGroups() { - try { - groups = await api.get('/api/groups'); - schGroupSelect.innerHTML = '' + - groups.map(g => { - let text = escapeHtml(g.name); - if (g.groupSize) text += ` (числ: ${g.groupSize} чел.)`; - return ``; - }).join(''); - } catch (e) { console.error('Ошибка загрузки групп:', e); } - } - - async function loadSubjects() { - try { - subjects = await api.get('/api/subjects'); - schDisciplineSelect.innerHTML = '' + - subjects.map(s => ``).join(''); - } catch (e) { console.error('Ошибка загрузки дисциплин:', e); } - } - - async function loadClassrooms() { - try { - classrooms = await api.get('/api/classrooms'); - renderClassroomOptions(); - } catch (e) { console.error('Ошибка загрузки аудиторий:', e); } - } - - async function loadTeachers() { - try { - teachers = await api.get('/api/users/teachers'); - schTeacherSelect.innerHTML = '' + - teachers.map(t => ``).join(''); - } catch (e) { console.error('Ошибка загрузки преподавателей:', e); } - } - - function renderClassroomOptions() { - if (!classrooms || classrooms.length === 0) { - schClassroomSelect.innerHTML = ''; + if (editButton) { + const rule = rules.find(item => item.id == editButton.dataset.id) + || await api.get('/api/admin/schedule-rules/' + editButton.dataset.id); + fillRuleForm(rule); return; } - const selectedGroupId = schGroupSelect.value; - const selectedGroup = groups?.find(g => g.id == selectedGroupId); - const groupSize = selectedGroup?.groupSize || 0; - schClassroomSelect.innerHTML = '' + - classrooms.map(c => { - let text = escapeHtml(c.name); - if (c.capacity) text += ` (вместимость: ${c.capacity} чел.)`; - if (c.isAvailable === false) { - text += ` ❌ Занята`; - } else if (selectedGroupId && groupSize > 0 && c.capacity && groupSize > c.capacity) { - text += ` ⚠️ Недостаточно места`; - } - return ``; - }).join(''); - } - - schGroupSelect.addEventListener('change', () => renderClassroomOptions()); - - function updateTimeOptions(dayValue) { - let times = []; - if (dayValue === "Суббота") { - times = saturdayTimes; - } else if (dayValue && dayValue !== '') { - times = weekdaysTimes; - } else { - schTimeSelect.innerHTML = ''; - schTimeSelect.disabled = true; - return; - } - schTimeSelect.innerHTML = '' + - times.map(t => ``).join(''); - schTimeSelect.disabled = false; - } - - schDaySelect.addEventListener('change', function () { - updateTimeOptions(this.value); - }); - - // ===== Автозаполнение преподавателя из фильтра ===== - function getFilteredTeacherId() { - const teacherFilter = activeFilters['teacher']; - if (teacherFilter && teacherFilter.size === 1) { - const teacherName = [...teacherFilter][0]; - // Сопоставляем по username, fullName и их комбинациям - const match = teachers.find(t => - t.username === teacherName || - t.fullName === teacherName || - (t.fullName || t.username) === teacherName - ); - return match ? String(match.id) : ''; - } - return ''; - } - - // ===== Загрузка занятий преподавателя ===== - async function loadTeacherLessons(teacherId) { - const teacher = teachers.find(t => t.id == teacherId); - const name = teacher ? (teacher.fullName || teacher.username) : ''; - schTeacherName.textContent = name - ? `Занятия преподавателя: ${name}` - : 'Занятия преподавателя'; - - modalLessons.style.display = ''; - schLessonsContainer.innerHTML = '
Загрузка занятий...
'; - - try { - const lessons = await api.get(`/api/users/lessons/${teacherId}`); - - if (!lessons || !Array.isArray(lessons) || lessons.length === 0) { - schLessonsContainer.innerHTML = '
У преподавателя пока нет занятий
'; - return; + if (deleteButton) { + if (!confirm('Удалить правило расписания?')) return; + try { + await api.delete('/api/admin/schedule-rules/' + deleteButton.dataset.id); + showAlert('schedule-rule-alert', 'Правило расписания удалено', 'success'); + await loadRules(); + } catch (error) { + showAlert('schedule-rule-alert', error.message || 'Ошибка удаления правила', 'error'); } - - const daysOrder = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота']; - const lessonsByDay = {}; - lessons.forEach(l => { - if (!lessonsByDay[l.day]) lessonsByDay[l.day] = []; - lessonsByDay[l.day].push(l); - }); - Object.keys(lessonsByDay).forEach(day => { - lessonsByDay[day].sort((a, b) => a.time.localeCompare(b.time)); - }); - - let html = ''; - daysOrder.forEach(day => { - if (!lessonsByDay[day]) return; - html += `
${day}
`; - lessonsByDay[day].forEach(lesson => { - html += ` -
-
- ${escapeHtml(lesson.groupName)} - ${escapeHtml(lesson.time)} -
-
-
${escapeHtml(lesson.subjectName)}
-
- ${escapeHtml(lesson.typeLesson)} - ${escapeHtml(lesson.lessonFormat)} - ${escapeHtml(lesson.week)} - ${escapeHtml(lesson.classroomName)} -
-
-
`; - }); - }); - - schLessonsContainer.innerHTML = html; - } catch (e) { - schLessonsContainer.innerHTML = `
Ошибка загрузки: ${escapeHtml(e.message)}
`; } } - // ===== При смене преподавателя — подгрузить его занятия ===== - schTeacherSelect.addEventListener('change', function () { - const teacherId = this.value; - if (teacherId) { - loadTeacherLessons(teacherId); - } else { - modalLessons.style.display = 'none'; - schLessonsContainer.innerHTML = '
Выберите преподавателя для просмотра занятий
'; - } - }); + async function saveRule(event) { + event.preventDefault(); + hideAlert('schedule-rule-alert'); - // ===== Открытие / закрытие оверлея ===== - function openOverlay() { - // Автозаполнение преподавателя из фильтра таблицы - const autoTeacherId = getFilteredTeacherId(); - if (autoTeacherId) { - schTeacherSelect.value = autoTeacherId; - loadTeacherLessons(autoTeacherId); + try { + const payload = buildRulePayload(); + const id = ruleIdInput.value; + const saved = id + ? await api.put('/api/admin/schedule-rules/' + id, payload) + : await api.post('/api/admin/schedule-rules', payload); + showAlert('schedule-rule-alert', `Правило "${saved.subjectName || 'расписания'}" сохранено`, 'success'); + resetRuleForm(); + await loadRules(); + } catch (error) { + showAlert('schedule-rule-alert', error.message || 'Ошибка сохранения правила', 'error'); } - - overlay.classList.add('open'); } - function closeOverlay() { - overlay.classList.remove('open'); - resetForm(); + function buildRulePayload() { + const groupIds = Array.from(ruleGroupsContainer.querySelectorAll('input[type="checkbox"]:checked')) + .map(input => Number(input.value)); + const slots = readRuleSlots(true); + + if (!ruleSubjectSelect.value) { + throw new Error('Выберите дисциплину'); + } + if (!ruleSemesterSelect.value) { + throw new Error('Выберите семестр'); + } + if (!ruleActiveFromInput.value) { + throw new Error('Укажите дату начала правила'); + } + if (!ruleHoursInput.value || Number(ruleHoursInput.value) <= 0) { + throw new Error('Укажите количество академических часов'); + } + if (!groupIds.length) { + throw new Error('Выберите хотя бы одну группу'); + } + if (!slots.length) { + throw new Error('Добавьте хотя бы один слот занятия'); + } + + return { + id: ruleIdInput.value ? Number(ruleIdInput.value) : null, + subjectId: Number(ruleSubjectSelect.value), + semesterId: Number(ruleSemesterSelect.value), + activeFromDate: ruleActiveFromInput.value, + totalAcademicHours: Number(ruleHoursInput.value), + groupIds, + slots + }; } - function resetForm() { - addForm.reset(); - schTeacherSelect.value = ''; - schGroupSelect.value = ''; - schDisciplineSelect.value = ''; - schClassroomSelect.value = ''; - schDaySelect.value = ''; - schTypeSelect.value = ''; - schTimeSelect.innerHTML = ''; - schTimeSelect.disabled = true; - if (schWeekUpper) schWeekUpper.checked = false; - if (schWeekLower) schWeekLower.checked = false; - if (schFormatOffline) schFormatOffline.checked = true; - modalLessons.style.display = 'none'; - schLessonsContainer.innerHTML = '
Выберите преподавателя для просмотра занятий
'; - hideAlert('sch-add-alert'); + function fillRuleForm(rule) { + ruleFormTitle.textContent = 'Редактирование правила расписания'; + ruleIdInput.value = rule.id; + ruleSubjectSelect.value = rule.subjectId || ''; + ruleSemesterSelect.value = rule.semesterId || ''; + ruleActiveFromInput.value = rule.activeFromDate || ''; + ruleHoursInput.value = rule.totalAcademicHours || ''; + syncSelects(ruleSubjectSelect, ruleSemesterSelect); + renderRuleGroups(rule.groupIds || []); + renderRuleSlots(rule.slots && rule.slots.length ? rule.slots : [{}]); + hideAlert('schedule-rule-alert'); + ruleForm.scrollIntoView({ behavior: 'smooth', block: 'start' }); } - btnAddLesson.addEventListener('click', openOverlay); - btnClose.addEventListener('click', closeOverlay); + function resetRuleForm() { + ruleForm.reset(); + ruleFormTitle.textContent = 'Новое правило расписания'; + ruleIdInput.value = ''; + renderRuleGroups([]); + renderRuleSlots([{}]); + syncSelects(ruleSubjectSelect, ruleSemesterSelect); + hideAlert('schedule-rule-alert'); + } - // Закрытие по клику на оверлей (мимо модалок) - overlay.addEventListener('click', (e) => { - if (e.target === overlay || e.target.classList.contains('cs-overlay-scroll')) { - closeOverlay(); - } - }); - - // Закрытие по Escape - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && overlay.classList.contains('open')) { - closeOverlay(); - } - }); - - // ===== Отправка формы ===== - addForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideAlert('sch-add-alert'); - - const teacherId = schTeacherSelect.value; - const groupId = schGroupSelect.value; - const subjectId = schDisciplineSelect.value; - const classroomId = schClassroomSelect.value; - const lessonType = schTypeSelect.value; - const dayOfWeek = schDaySelect.value; - const timeSlot = schTimeSelect.value; - const lessonFormat = document.querySelector('input[name="schLessonFormat"]:checked')?.value; - - if (!teacherId) { showAlert('sch-add-alert', 'Выберите преподавателя', 'error'); return; } - if (!groupId) { showAlert('sch-add-alert', 'Выберите группу', 'error'); return; } - if (!subjectId) { showAlert('sch-add-alert', 'Выберите дисциплину', 'error'); return; } - if (!classroomId) { showAlert('sch-add-alert', 'Выберите аудиторию', 'error'); return; } - if (!dayOfWeek) { showAlert('sch-add-alert', 'Выберите день недели', 'error'); return; } - if (!timeSlot) { showAlert('sch-add-alert', 'Выберите время', 'error'); return; } - - const weekUpperChecked = schWeekUpper?.checked || false; - const weekLowerChecked = schWeekLower?.checked || false; - - if (!weekUpperChecked && !weekLowerChecked) { - showAlert('sch-add-alert', 'Не выбран тип недели', 'error'); + function renderRuleGroups(selectedIds) { + if (!groups.length) { + ruleGroupsContainer.innerHTML = '
Нет групп
'; return; } - let weekType = null; - if (weekUpperChecked && weekLowerChecked) weekType = 'Обе'; - else if (weekUpperChecked) weekType = 'Верхняя'; - else if (weekLowerChecked) weekType = 'Нижняя'; + const selected = new Set((selectedIds || []).map(String)); + ruleGroupsContainer.innerHTML = groups.map(group => ` + + `).join(''); + } + + function renderRuleSlots(slots) { + ruleSlotsContainer.innerHTML = (slots || [{}]).map((slot, index) => ` +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ `).join(''); + } + + function readRuleSlots(validate) { + const rows = Array.from(ruleSlotsContainer.querySelectorAll('.schedule-slot-row')); + return rows.map((row, index) => { + const slot = { + id: null, + dayOfWeek: Number(row.querySelector('.slot-day').value), + parity: row.querySelector('.slot-parity').value, + timeSlotId: Number(row.querySelector('.slot-time').value), + subgroupId: null, + teacherId: Number(row.querySelector('.slot-teacher').value), + classroomId: Number(row.querySelector('.slot-classroom').value), + lessonTypeId: Number(row.querySelector('.slot-lesson-type').value), + lessonFormat: row.querySelector('.slot-format').value + }; + + if (validate && (!slot.dayOfWeek || !slot.parity || !slot.timeSlotId + || !slot.teacherId || !slot.classroomId || !slot.lessonTypeId || !slot.lessonFormat)) { + throw new Error(`Заполните слот ${index + 1}`); + } + return slot; + }); + } + + async function loadTimeSlots() { + timeSlotsTbody.innerHTML = 'Загрузка...'; + try { + timeSlots = await api.get('/api/admin/time-slots'); + renderTimeSlots(); + } catch (error) { + timeSlotsTbody.innerHTML = `Ошибка загрузки: ${escapeHtml(error.message)}`; + } + } + + function renderTimeSlots() { + if (!timeSlots || !timeSlots.length) { + timeSlotsTbody.innerHTML = 'Сетка пар не настроена'; + return; + } + + timeSlotsTbody.innerHTML = timeSlots.map(slot => ` + + ${slot.orderNumber} + ${escapeHtml(trimTime(slot.startTime))} + ${escapeHtml(trimTime(slot.endTime))} + ${escapeHtml(String(slot.durationMinutes ?? '-'))} + + + + + + `).join(''); + } + + async function saveTimeSlot(event) { + event.preventDefault(); + hideAlert('time-slot-alert'); + + const payload = { + id: timeSlotIdInput.value ? Number(timeSlotIdInput.value) : null, + orderNumber: Number(timeSlotOrderInput.value), + startTime: timeSlotStartInput.value, + endTime: timeSlotEndInput.value, + durationMinutes: timeSlotDurationInput.value ? Number(timeSlotDurationInput.value) : null + }; try { - await api.post('/api/users/lessons/create', { - teacherId: parseInt(teacherId), - groupId: parseInt(groupId), - subjectId: parseInt(subjectId), - classroomId: parseInt(classroomId), - typeLesson: lessonType, - lessonFormat: lessonFormat, - day: dayOfWeek, - week: weekType, - time: timeSlot - }); - - showAlert('sch-add-alert', 'Занятие добавлено ✓', 'success'); - - // Очистить все поля кроме преподавателя (для массового добавления) - schGroupSelect.selectedIndex = 0; - schDisciplineSelect.selectedIndex = 0; - schClassroomSelect.selectedIndex = 0; - schTypeSelect.selectedIndex = 0; - schDaySelect.selectedIndex = 0; - schTimeSelect.innerHTML = ''; - schTimeSelect.disabled = true; - schWeekUpper.checked = false; - schWeekLower.checked = false; - document.querySelector('input[name="schLessonFormat"][value="Очно"]').checked = true; - - // Обновить занятия преподавателя в модалке 2 - if (teacherId) { - await loadTeacherLessons(teacherId); + if (!payload.orderNumber || !payload.startTime || !payload.endTime) { + throw new Error('Заполните номер пары, начало и окончание'); } - - // Обновить основную таблицу - await loadSchedule(); - - setTimeout(() => { - hideAlert('sch-add-alert'); - }, 4000); - } catch (err) { - showAlert('sch-add-alert', err.message || 'Ошибка добавления занятия', 'error'); + const id = timeSlotIdInput.value; + await (id + ? api.put('/api/admin/time-slots/' + id, payload) + : api.post('/api/admin/time-slots', payload)); + showAlert('time-slot-alert', 'Временной слот сохранён', 'success'); + resetTimeSlotForm(); + await loadTimeSlots(); + renderRuleSlots(readRuleSlots(false)); + } catch (error) { + showAlert('time-slot-alert', error.message || 'Ошибка сохранения временного слота', 'error'); } - }); + } - // ===================== Инициализация ===================== - await Promise.all([ - loadSchedule(), - loadGroups(), - loadSubjects(), - loadClassrooms(), - loadTeachers() - ]); -} \ No newline at end of file + async function handleTimeSlotTableClick(event) { + const editButton = event.target.closest('.btn-edit-time-slot'); + const deleteButton = event.target.closest('.btn-delete-time-slot'); + + if (editButton) { + const slot = timeSlots.find(item => item.id == editButton.dataset.id); + if (!slot) return; + timeSlotFormTitle.textContent = 'Редактирование временного слота'; + timeSlotIdInput.value = slot.id; + timeSlotOrderInput.value = slot.orderNumber || ''; + timeSlotStartInput.value = trimTime(slot.startTime); + timeSlotEndInput.value = trimTime(slot.endTime); + timeSlotDurationInput.value = slot.durationMinutes || ''; + hideAlert('time-slot-alert'); + timeSlotForm.scrollIntoView({ behavior: 'smooth', block: 'start' }); + return; + } + + if (deleteButton) { + if (!confirm('Удалить временной слот?')) return; + try { + await api.delete('/api/admin/time-slots/' + deleteButton.dataset.id); + showAlert('time-slot-alert', 'Временной слот удалён', 'success'); + await loadTimeSlots(); + } catch (error) { + showAlert('time-slot-alert', error.message || 'Ошибка удаления временного слота', 'error'); + } + } + } + + function resetTimeSlotForm() { + timeSlotForm.reset(); + timeSlotFormTitle.textContent = 'Новый временной слот'; + timeSlotIdInput.value = ''; + hideAlert('time-slot-alert'); + } + + async function loadYears() { + academicYearsTbody.innerHTML = 'Загрузка...'; + try { + academicYears = await api.get('/api/admin/calendar/years'); + semesters = academicYears.flatMap(year => year.semesters || []); + renderAcademicYears(); + populateYearSelects(); + populateSemesterSelects(); + } catch (error) { + academicYearsTbody.innerHTML = `Ошибка загрузки: ${escapeHtml(error.message)}`; + } + } + + function renderAcademicYears() { + if (!academicYears || !academicYears.length) { + academicYearsTbody.innerHTML = 'Учебные годы не созданы'; + return; + } + + academicYearsTbody.innerHTML = academicYears.map(year => ` + + ${escapeHtml(year.title)} + ${escapeHtml(year.startDate)} — ${escapeHtml(year.endDate)} + ${renderSemesterList(year.semesters || [])} + + + + + + `).join(''); + } + + function renderSemesterList(items) { + if (!items.length) return 'Нет семестров'; + return items.map(semester => ` +
+ ${escapeHtml(semesterLabel(semester))} + +
+ `).join(''); + } + + async function saveAcademicYear(event) { + event.preventDefault(); + hideAlert('academic-year-alert'); + + const payload = { + title: academicYearTitleInput.value.trim(), + startDate: academicYearStartInput.value, + endDate: academicYearEndInput.value + }; + + try { + if (!payload.title || !payload.startDate || !payload.endDate) { + throw new Error('Заполните название и даты учебного года'); + } + const id = academicYearIdInput.value; + await (id + ? api.put('/api/admin/calendar/years/' + id, payload) + : api.post('/api/admin/calendar/years', payload)); + showAlert('academic-year-alert', 'Учебный год сохранён', 'success'); + resetAcademicYearForm(); + await loadYears(); + } catch (error) { + showAlert('academic-year-alert', error.message || 'Ошибка сохранения учебного года', 'error'); + } + } + + async function handleAcademicYearTableClick(event) { + const editYearButton = event.target.closest('.btn-edit-year'); + const deleteYearButton = event.target.closest('.btn-delete-year'); + const editSemesterButton = event.target.closest('.btn-edit-semester'); + + if (editYearButton) { + const year = academicYears.find(item => item.id == editYearButton.dataset.id); + if (!year) return; + academicYearFormTitle.textContent = 'Редактирование учебного года'; + academicYearIdInput.value = year.id; + academicYearTitleInput.value = year.title || ''; + academicYearStartInput.value = year.startDate || ''; + academicYearEndInput.value = year.endDate || ''; + hideAlert('academic-year-alert'); + academicYearForm.scrollIntoView({ behavior: 'smooth', block: 'start' }); + return; + } + + if (editSemesterButton) { + const semester = semesters.find(item => item.id == editSemesterButton.dataset.id); + if (!semester) return; + semesterFormTitle.textContent = 'Редактирование семестра'; + semesterIdInput.value = semester.id; + semesterYearSelect.value = semester.academicYearId || ''; + semesterTypeSelect.value = normalizeSemesterType(semester.semesterType); + semesterStartInput.value = semester.startDate || ''; + semesterEndInput.value = semester.endDate || ''; + syncSelects(semesterYearSelect, semesterTypeSelect); + hideAlert('semester-alert'); + semesterForm.scrollIntoView({ behavior: 'smooth', block: 'start' }); + return; + } + + if (deleteYearButton) { + if (!confirm('Удалить учебный год вместе с семестрами?')) return; + try { + await api.delete('/api/admin/calendar/years/' + deleteYearButton.dataset.id); + showAlert('academic-year-alert', 'Учебный год удалён', 'success'); + await loadYears(); + } catch (error) { + showAlert('academic-year-alert', error.message || 'Ошибка удаления учебного года', 'error'); + } + } + } + + function resetAcademicYearForm() { + academicYearForm.reset(); + academicYearFormTitle.textContent = 'Учебный год'; + academicYearIdInput.value = ''; + hideAlert('academic-year-alert'); + } + + async function saveSemester(event) { + event.preventDefault(); + hideAlert('semester-alert'); + + const yearId = semesterYearSelect.value; + const payload = { + semesterType: semesterTypeSelect.value, + startDate: semesterStartInput.value, + endDate: semesterEndInput.value + }; + + try { + if (!yearId || !payload.semesterType || !payload.startDate || !payload.endDate) { + throw new Error('Заполните учебный год, тип и даты семестра'); + } + const id = semesterIdInput.value; + await (id + ? api.put('/api/admin/calendar/semesters/' + id, payload) + : api.post(`/api/admin/calendar/years/${yearId}/semesters`, payload)); + showAlert('semester-alert', 'Семестр сохранён', 'success'); + resetSemesterForm(); + await loadYears(); + } catch (error) { + showAlert('semester-alert', error.message || 'Ошибка сохранения семестра', 'error'); + } + } + + function resetSemesterForm() { + semesterForm.reset(); + semesterFormTitle.textContent = 'Семестр'; + semesterIdInput.value = ''; + syncSelects(semesterYearSelect, semesterTypeSelect); + hideAlert('semester-alert'); + } + + async function saveHoliday(event) { + event.preventDefault(); + hideAlert('holiday-alert'); + + const payload = { + id: holidayIdInput.value ? Number(holidayIdInput.value) : null, + academicYearId: Number(holidayYearSelect.value), + date: holidayDateInput.value, + description: holidayDescriptionInput.value.trim() + }; + + try { + if (!payload.academicYearId || !payload.date) { + throw new Error('Выберите учебный год и дату'); + } + const id = holidayIdInput.value; + await (id + ? api.put('/api/admin/calendar/holidays/' + id, payload) + : api.post('/api/admin/calendar/holidays', payload)); + showAlert('holiday-alert', 'Дата сохранена', 'success'); + resetHolidayForm(false); + await loadHolidays(); + } catch (error) { + showAlert('holiday-alert', error.message || 'Ошибка сохранения даты', 'error'); + } + } + + async function loadHolidays() { + const yearId = holidayYearSelect.value; + if (!yearId) { + holidaysTbody.innerHTML = 'Выберите учебный год'; + return; + } + + holidaysTbody.innerHTML = 'Загрузка...'; + try { + holidays = await api.get('/api/admin/calendar/holidays?academicYearId=' + encodeURIComponent(yearId)); + renderHolidays(); + } catch (error) { + holidaysTbody.innerHTML = `Ошибка загрузки: ${escapeHtml(error.message)}`; + } + } + + function renderHolidays() { + if (!holidays || !holidays.length) { + holidaysTbody.innerHTML = 'Исключения не добавлены'; + return; + } + + holidaysTbody.innerHTML = holidays.map(holiday => ` + + ${escapeHtml(holiday.date)} + ${escapeHtml(holiday.description || '-')} + + + + + + `).join(''); + } + + async function handleHolidayTableClick(event) { + const editButton = event.target.closest('.btn-edit-holiday'); + const deleteButton = event.target.closest('.btn-delete-holiday'); + + if (editButton) { + const holiday = holidays.find(item => item.id == editButton.dataset.id); + if (!holiday) return; + holidayIdInput.value = holiday.id; + holidayYearSelect.value = holiday.academicYearId || holidayYearSelect.value; + holidayDateInput.value = holiday.date || ''; + holidayDescriptionInput.value = holiday.description || ''; + syncSelects(holidayYearSelect); + hideAlert('holiday-alert'); + holidayForm.scrollIntoView({ behavior: 'smooth', block: 'start' }); + return; + } + + if (deleteButton) { + if (!confirm('Удалить дату-исключение?')) return; + try { + await api.delete('/api/admin/calendar/holidays/' + deleteButton.dataset.id); + showAlert('holiday-alert', 'Дата удалена', 'success'); + await loadHolidays(); + } catch (error) { + showAlert('holiday-alert', error.message || 'Ошибка удаления даты', 'error'); + } + } + } + + function resetHolidayForm(clearYear = true) { + const yearId = holidayYearSelect.value; + holidayForm.reset(); + if (!clearYear) holidayYearSelect.value = yearId; + holidayIdInput.value = ''; + syncSelects(holidayYearSelect); + hideAlert('holiday-alert'); + } + + async function loadMatrix() { + hideAlert('matrix-alert'); + const semesterId = matrixSemesterSelect.value; + const courseNumber = Number(matrixCourseInput.value); + const specialtyId = matrixSpecialtySelect.value; + + if (!semesterId || !courseNumber || !specialtyId) { + showAlert('matrix-alert', 'Выберите семестр, курс и специальность', 'error'); + return; + } + + try { + const params = new URLSearchParams({ semesterId, courseNumber: String(courseNumber), specialtyId }); + const rows = await api.get('/api/admin/calendar/matrix?' + params.toString()); + renderMatrix(rows && rows.length ? rows : buildDefaultMatrixRows(semesterId, courseNumber, specialtyId)); + showAlert('matrix-alert', rows && rows.length ? 'Матрица загружена' : 'Матрица не найдена, подготовлена новая сетка', 'success'); + } catch (error) { + showAlert('matrix-alert', error.message || 'Ошибка загрузки матрицы', 'error'); + } + } + + async function saveMatrix() { + hideAlert('matrix-alert'); + const rows = Array.from(matrixGrid.querySelectorAll('.matrix-week')).map(item => ({ + id: item.dataset.id ? Number(item.dataset.id) : null, + semesterId: Number(item.dataset.semesterId), + courseNumber: Number(item.dataset.courseNumber), + specialtyId: Number(item.dataset.specialtyId), + weekNumber: Number(item.dataset.weekNumber), + activityType: item.querySelector('select').value + })); + + if (!rows.length) { + showAlert('matrix-alert', 'Сначала загрузите матрицу', 'error'); + return; + } + + try { + await api.put('/api/admin/calendar/matrix', rows); + showAlert('matrix-alert', 'Матрица учебного графика сохранена', 'success'); + await loadMatrix(); + } catch (error) { + showAlert('matrix-alert', error.message || 'Ошибка сохранения матрицы', 'error'); + } + } + + function renderMatrix(rows) { + matrixGrid.innerHTML = rows.map(row => ` +
+ ${row.weekNumber} + +
+ `).join(''); + } + + function buildDefaultMatrixRows(semesterId, courseNumber, specialtyId) { + return Array.from({ length: 22 }, (_, index) => ({ + id: null, + semesterId: Number(semesterId), + courseNumber, + specialtyId: Number(specialtyId), + weekNumber: index + 1, + activityType: 'THEORY' + })); + } + + function populateSubjectSelects() { + ruleSubjectSelect.innerHTML = '' + + subjects.map(subject => ``).join(''); + } + + function populateYearSelects() { + const yearOptions = '' + + academicYears.map(year => ``).join(''); + semesterYearSelect.innerHTML = yearOptions; + holidayYearSelect.innerHTML = yearOptions; + + if (!holidayYearSelect.value && academicYears.length) { + holidayYearSelect.value = String(academicYears[0].id); + } + syncSelects(semesterYearSelect, holidayYearSelect); + } + + function populateSemesterSelects() { + const semesterOptions = '' + + semesters.map(semester => ``).join(''); + ruleSemesterSelect.innerHTML = semesterOptions; + matrixSemesterSelect.innerHTML = semesterOptions; + syncSelects(ruleSemesterSelect, matrixSemesterSelect); + } + + function populateSpecialtySelects() { + matrixSpecialtySelect.innerHTML = '' + + specialties.map(speciality => ``).join(''); + syncSelects(matrixSpecialtySelect); + } + + function options(items, selectedValue) { + const selected = selectedValue == null ? '' : String(selectedValue); + return '' + items.map(item => { + const value = String(item.value); + return ``; + }).join(''); + } + + function activityOptions() { + return Object.entries(ACTIVITY_LABELS).map(([value, label]) => ({ value, label })); + } + + function toTimeSlotOption(slot) { + return { value: slot.id, label: `${slot.orderNumber} пара, ${trimTime(slot.startTime)} - ${trimTime(slot.endTime)}` }; + } + + function toTeacherOption(teacher) { + return { value: teacher.id, label: teacher.fullName || teacher.username || `ID ${teacher.id}` }; + } + + function toClassroomOption(classroom) { + const capacity = classroom.capacity ? `, ${classroom.capacity} чел.` : ''; + return { value: classroom.id, label: `${classroom.name}${capacity}` }; + } + + function toLessonTypeOption(lessonType) { + return { value: lessonType.id, label: lessonType.name || lessonType.lessonType || `ID ${lessonType.id}` }; + } + + function semesterLabel(semester) { + const type = normalizeSemesterType(semester.semesterType); + return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${semester.startDate} — ${semester.endDate})`; + } + + function formatSemester(rule) { + const type = normalizeSemesterType(rule.semesterType); + return `${rule.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type || '-'}`; + } + + function normalizeSemesterType(type) { + return String(type || '').toLowerCase(); + } + + function specialityLabel(speciality) { + const code = speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code || speciality.id; + const name = speciality.specialityName || speciality.name || ''; + return name ? `${code} — ${name}` : String(code); + } + + function trimTime(value) { + if (!value) return ''; + return String(value).slice(0, 5); + } + + function syncSelects(...selects) { + selects.filter(Boolean).forEach(select => { + select.dispatchEvent(new Event('change', { bubbles: true })); + }); + } +} diff --git a/frontend/admin/js/views/users.js b/frontend/admin/js/views/users.js old mode 100755 new mode 100644 index 2189442..2008ca3 --- a/frontend/admin/js/views/users.js +++ b/frontend/admin/js/views/users.js @@ -8,173 +8,20 @@ export async function initUsers() { const usersTbody = document.getElementById('users-tbody'); const createForm = document.getElementById('create-form'); - // ===== Оверлей (cs-overlay) ===== - const usersOverlay = document.getElementById('users-overlay'); - - // ===== 1-е модальное окно: Добавить занятие ===== - const modalAddLesson = document.getElementById('modal-add-lesson'); - const modalAddLessonClose = document.getElementById('modal-add-lesson-close'); - const addLessonForm = document.getElementById('add-lesson-form'); - - const lessonGroupSelect = document.getElementById('lesson-group'); - const lessonDisciplineSelect = document.getElementById('lesson-discipline'); - const lessonClassroomSelect = document.getElementById('lesson-classroom'); - const lessonTypeSelect = document.getElementById('lesson-type'); - const lessonOnlineFormat = document.getElementById('format-online'); - const lessonOfflineFormat = document.getElementById('format-offline'); - const lessonUserId = document.getElementById('lesson-user-id'); - const lessonDaySelect = document.getElementById('lesson-day'); - const weekUpper = document.getElementById('week-upper'); - const weekLower = document.getElementById('week-lower'); - const lessonTimeSelect = document.getElementById('lesson-time'); - - // ===== 2-е модальное окно: Просмотр занятий ===== - const modalViewLessons = document.getElementById('modal-view-lessons'); - const lessonsContainer = document.getElementById('lessons-container'); - const modalTeacherName = document.getElementById('modal-teacher-name'); - - let currentLessonsTeacherId = null; - let currentLessonsTeacherName = ''; - // ===== Данные ===== - let groups = []; - let subjects = []; - let classrooms = []; - - const weekdaysTimes = [ - "8:00-9:30", - "9:40-11:10", - "11:40-13:10", - "13:20-14:50", - "15:00-16:30", - "16:50-18:20", - "18:30-19:00" - ]; - - const saturdayTimes = [ - "8:20-9:50", - "10:00-11:30", - "11:40-13:10", - "13:20-14:50" - ]; - - // ========================================================= - // Загрузка справочников - // ========================================================= - async function loadGroups() { - try { - groups = await api.get('/api/groups'); - renderGroupOptions(); - } catch (e) { - console.error('Ошибка загрузки групп:', e); - } - } - - async function loadSubjects() { - try { - subjects = await api.get('/api/subjects'); - renderSubjectOptions(); - } catch (e) { - console.error('Ошибка загрузки дисциплин:', e); - } - } - - async function loadClassrooms() { - try { - classrooms = await api.get('/api/classrooms'); - renderClassroomsOptions(); - } catch (e) { - console.error('Ошибка загрузки аудиторий:', e); - } - } - - function renderGroupOptions() { - if (!groups || groups.length === 0) { - lessonGroupSelect.innerHTML = ''; - return; - } - - lessonGroupSelect.innerHTML = - '' + - groups.map(g => { - let optionText = escapeHtml(g.name); - if (g.groupSize) optionText += ` (численность: ${g.groupSize} чел.)`; - return ``; - }).join(''); - } - - function renderSubjectOptions() { - lessonDisciplineSelect.innerHTML = - '' + - subjects.map(s => ``).join(''); - } - - function renderClassroomsOptions() { - if (!classrooms || classrooms.length === 0) { - lessonClassroomSelect.innerHTML = ''; - return; - } - - const selectedGroupId = lessonGroupSelect.value; - const selectedGroup = groups?.find(g => g.id == selectedGroupId); - const groupSize = selectedGroup?.groupSize || 0; - - lessonClassroomSelect.innerHTML = - '' + - classrooms.map(c => { - let optionText = escapeHtml(c.name); - - if (c.capacity) optionText += ` (вместимость: ${c.capacity} чел.)`; - - if (c.isAvailable === false) { - optionText += ` ❌ Занята`; - } else if (selectedGroupId && groupSize > 0 && c.capacity && groupSize > c.capacity) { - optionText += ` ⚠️ Недостаточно места`; - } - - return ``; - }).join(''); - } - - lessonGroupSelect.addEventListener('change', function () { - renderClassroomsOptions(); - requestAnimationFrame(() => syncAddLessonHeight()); - }); - - function updateTimeOptions(dayValue) { - let times = []; - if (dayValue === "Суббота") { - times = saturdayTimes; - } else if (dayValue && dayValue !== '') { - times = weekdaysTimes; - } else { - lessonTimeSelect.innerHTML = ''; - lessonTimeSelect.disabled = true; - return; - } - - lessonTimeSelect.innerHTML = - '' + - times.map(t => ``).join(''); - lessonTimeSelect.disabled = false; - } - - // ========================================================= - // Пользователи - // ========================================================= async function loadUsers() { try { const users = await api.get('/api/users'); renderUsers(users); } catch (e) { usersTbody.innerHTML = - 'Ошибка загрузки: ' + + 'Ошибка загрузки: ' + escapeHtml(e.message) + ''; } } function renderUsers(users) { if (!users || !users.length) { - usersTbody.innerHTML = 'Нет пользователей'; + usersTbody.innerHTML = 'Нет пользователей'; return; } @@ -184,148 +31,15 @@ export async function initUsers() { ${escapeHtml(u.username)} ${escapeHtml(u.fullName || '-')} ${escapeHtml(u.jobTitle || '-')} - ${u.departmentName || '-'} + ${escapeHtml(u.departmentName || '-')} ${ROLE_LABELS[u.role] || escapeHtml(u.role)} - - - `).join(''); } - // ===== Открытие / закрытие оверлея ===== - function openOverlay() { - if (usersOverlay) usersOverlay.classList.add('open'); - } - function closeOverlay() { - if (usersOverlay) usersOverlay.classList.remove('open'); - if (modalViewLessons) modalViewLessons.style.display = 'none'; - resetLessonForm(); - } - - // ========================================================= - // 1-я модалка: добавление занятия - // ========================================================= - function resetLessonForm() { - addLessonForm.reset(); - lessonUserId.value = ''; - - if (weekUpper) weekUpper.checked = false; - if (weekLower) weekLower.checked = false; - - if (lessonOfflineFormat) lessonOfflineFormat.checked = true; - if (lessonOnlineFormat) lessonOnlineFormat.checked = false; - - lessonTimeSelect.innerHTML = ''; - lessonTimeSelect.disabled = true; - - hideAlert('add-lesson-alert'); - } - - function openAddLessonModal(userId) { - lessonUserId.value = userId; - - lessonDaySelect.value = ''; - updateTimeOptions(''); - - openOverlay(); - } - - addLessonForm.addEventListener('submit', async (e) => { - e.preventDefault(); - hideAlert('add-lesson-alert'); - - const userId = lessonUserId.value; - const groupId = lessonGroupSelect.value; - const subjectId = lessonDisciplineSelect.value; - const classroomId = lessonClassroomSelect.value; - const lessonType = lessonTypeSelect.value; - const dayOfWeek = lessonDaySelect.value; - const timeSlot = lessonTimeSelect.value; - - const lessonFormat = document.querySelector('input[name="lessonFormat"]:checked')?.value; - - if (!groupId) { showAlert('add-lesson-alert', 'Выберите группу', 'error'); return; } - if (!subjectId) { showAlert('add-lesson-alert', 'Выберите дисциплину', 'error'); return; } - if (!classroomId) { showAlert('add-lesson-alert', 'Выберите аудиторию', 'error'); return; } - if (!dayOfWeek) { showAlert('add-lesson-alert', 'Выберите день недели', 'error'); return; } - if (!timeSlot) { showAlert('add-lesson-alert', 'Выберите время', 'error'); return; } - - const weekUpperChecked = weekUpper?.checked || false; - const weekLowerChecked = weekLower?.checked || false; - - if (!weekUpperChecked && !weekLowerChecked) { - showAlert('add-lesson-alert', 'Не выбран тип недели', 'error'); - return; - } - - let weekType = null; - if (weekUpperChecked && weekLowerChecked) weekType = 'Обе'; - else if (weekUpperChecked) weekType = 'Верхняя'; - else if (weekLowerChecked) weekType = 'Нижняя'; - - try { - await api.post('/api/users/lessons/create', { - teacherId: parseInt(userId), - groupId: parseInt(groupId), - subjectId: parseInt(subjectId), - classroomId: parseInt(classroomId), - typeLesson: lessonType, - lessonFormat: lessonFormat, - day: dayOfWeek, - week: weekType, - time: timeSlot - }); - - if (modalViewLessons?.style.display !== 'none' && currentLessonsTeacherId == userId) { - await loadTeacherLessons(currentLessonsTeacherId, currentLessonsTeacherName); - } - - showAlert('add-lesson-alert', 'Занятие добавлено ✓', 'success'); - - lessonGroupSelect.selectedIndex = 0; - lessonDisciplineSelect.selectedIndex = 0; - lessonClassroomSelect.selectedIndex = 0; - lessonTypeSelect.selectedIndex = 0; - lessonDaySelect.selectedIndex = 0; - lessonTimeSelect.innerHTML = ''; - lessonTimeSelect.disabled = true; - - weekUpper.checked = false; - weekLower.checked = false; - document.querySelector('input[name="lessonFormat"][value="Очно"]').checked = true; - - setTimeout(() => { - hideAlert('add-lesson-alert'); - }, 3000); - } catch (err) { - showAlert('add-lesson-alert', err.message || 'Ошибка добавления занятия', 'error'); - } - }); - - lessonDaySelect.addEventListener('change', function () { - updateTimeOptions(this.value); - }); - - if (modalAddLessonClose) { - modalAddLessonClose.addEventListener('click', () => closeOverlay()); - } - - // Клик по оверлею (мимо модалок) закрывает всё - if (usersOverlay) { - usersOverlay.querySelector('.cs-overlay-scroll')?.addEventListener('click', (e) => { - if (e.target.classList.contains('cs-overlay-scroll')) { - closeOverlay(); - } - }); - } - - // ========================================================= - // Создание пользователя - // ========================================================= createForm.addEventListener('submit', async (e) => { e.preventDefault(); hideAlert('create-alert'); @@ -343,9 +57,9 @@ export async function initUsers() { } try { - const data = await api.post('/api/users', { - username, - password, + const data = await api.post('/api/users', { + username, + password, role, fullName, jobTitle, @@ -359,133 +73,18 @@ export async function initUsers() { } }); - // ========================================================= - // Инициализация - // ========================================================= - await Promise.all([loadUsers(), loadGroups(), loadSubjects(), loadClassrooms()]); - - // ========================================================= - // 2-я модалка: просмотр занятий - // ========================================================= - async function loadTeacherLessons(teacherId, teacherName) { - try { - lessonsContainer.innerHTML = '
Загрузка занятий...
'; - - modalTeacherName.textContent = teacherName - ? `Занятия преподавателя: ${teacherName}` - : 'Занятия преподавателя'; - - const lessons = await api.get(`/api/users/lessons/${teacherId}`); - - if (!lessons || lessons.length === 0) { - lessonsContainer.innerHTML = '
У преподавателя пока нет занятий
'; - return; - } - - const daysOrder = ['Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота']; - const lessonsByDay = {}; - - lessons.forEach(lesson => { - if (!lessonsByDay[lesson.day]) lessonsByDay[lesson.day] = []; - lessonsByDay[lesson.day].push(lesson); - }); - - Object.keys(lessonsByDay).forEach(day => { - lessonsByDay[day].sort((a, b) => a.time.localeCompare(b.time)); - }); - - let html = ''; - - daysOrder.forEach(day => { - if (!lessonsByDay[day]) return; - - html += `
${day}
`; - - lessonsByDay[day].forEach(lesson => { - html += ` -
-
- ${escapeHtml(lesson.groupName)} - ${escapeHtml(lesson.time)} -
-
-
${escapeHtml(lesson.subjectName)}
-
- ${escapeHtml(lesson.typeLesson)} - ${escapeHtml(lesson.lessonFormat)} - ${escapeHtml(lesson.week)} - ${escapeHtml(lesson.classroomName)} -
-
-
- `; - }); - }); - - lessonsContainer.innerHTML = html; - } catch (e) { - lessonsContainer.innerHTML = `
Ошибка загрузки: ${escapeHtml(e.message)}
`; - console.error('Ошибка загрузки занятий:', e); - } - } - - function openViewLessonsModal(teacherId, teacherName) { - currentLessonsTeacherId = teacherId; - currentLessonsTeacherName = teacherName || ''; - - if (modalViewLessons) modalViewLessons.style.display = ''; - loadTeacherLessons(teacherId, teacherName); - } - - function closeViewLessonsModal() { - if (modalViewLessons) modalViewLessons.style.display = 'none'; - currentLessonsTeacherId = null; - currentLessonsTeacherName = ''; - } - - document.addEventListener('keydown', (e) => { - if (e.key !== 'Escape') return; - if (usersOverlay?.classList.contains('open')) { - closeOverlay(); - } - }); - - // ========================================================= - // ЕДИНЫЙ обработчик кликов по таблице (ВАЖНО: без дубля) - // ========================================================= usersTbody.addEventListener('click', async (e) => { const deleteBtn = e.target.closest('.btn-delete'); - if (deleteBtn) { - if (!confirm('Удалить пользователя?')) return; - try { - await api.delete('/api/users/' + deleteBtn.dataset.id); - loadUsers(); - } catch (err) { - alert(err.message || 'Ошибка удаления'); - } - return; - } + if (!deleteBtn) return; - const addLessonBtn = e.target.closest('.btn-add-lesson'); - if (addLessonBtn) { - e.preventDefault(); - - const teacherId = addLessonBtn.dataset.id; - const teacherName = addLessonBtn.dataset.name; - - openAddLessonModal(teacherId); - openViewLessonsModal(teacherId, teacherName); - - return; - } - - const viewLessonsBtn = e.target.closest('.btn-view-lessons'); - if (viewLessonsBtn) { - e.preventDefault(); - const teacherId = viewLessonsBtn.dataset.id; - const teacherName = viewLessonsBtn.dataset.name; - openViewLessonsModal(teacherId, teacherName); - return; + if (!confirm('Удалить пользователя?')) return; + try { + await api.delete('/api/users/' + deleteBtn.dataset.id); + loadUsers(); + } catch (err) { + alert(err.message || 'Ошибка удаления'); } }); + + await loadUsers(); } diff --git a/frontend/admin/views/department.html b/frontend/admin/views/department.html deleted file mode 100644 index 11bf25b..0000000 --- a/frontend/admin/views/department.html +++ /dev/null @@ -1,180 +0,0 @@ -
-
-

Запрос расписания кафедры

- -
-
-
-
- - -
- -
- -
- - -
-
- -
- - -
- - -
- -
-
- - -
-
- - - - - - - -
-
- -
- -
\ No newline at end of file diff --git a/frontend/admin/views/departments-data.html b/frontend/admin/views/departments-data.html index a7ae105..dd31814 100644 --- a/frontend/admin/views/departments-data.html +++ b/frontend/admin/views/departments-data.html @@ -28,11 +28,12 @@ ID Название Код + Действия - Загрузка... + Загрузка... @@ -68,13 +69,58 @@ ID Название Код специальности + Действия - Загрузка... + Загрузка... + + + + diff --git a/frontend/admin/views/groups.html b/frontend/admin/views/groups.html index 1bb9015..c05b8af 100755 --- a/frontend/admin/views/groups.html +++ b/frontend/admin/views/groups.html @@ -9,7 +9,7 @@
- +
@@ -18,16 +18,20 @@
- - + +
- +
- - + +
@@ -53,9 +57,9 @@ Название Численность (чел.) Форма обучения - ID кафедры + Кафедра Курс - Код специальности + Специальность Действия @@ -66,4 +70,4 @@ - \ No newline at end of file + diff --git a/frontend/admin/views/schedule.html b/frontend/admin/views/schedule.html old mode 100755 new mode 100644 index 8104356..b5faf14 --- a/frontend/admin/views/schedule.html +++ b/frontend/admin/views/schedule.html @@ -1,179 +1,281 @@ +
+
+

Новое правило расписания

+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ +
+
+

Слоты правила

+ +
+
+
+ +
+ +
+ +
+
+
-

Расписание занятий

- +

Правила динамического расписания

+
- - - - - - - - - - - + + + + + + + + - +
ID - Преподаватель - - Группа - - Аудитория - - Форма обучения - - Дисциплина - - Формат занятия - - Тип занятия - - День недели - НеделяВремяIDДисциплинаСеместрГруппыНачалоЧасыСлотыДействия
Загрузка...Загрузка...
- -
-
- - -
-
-

Добавить занятие

- -
- -
-
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- - -
- -
- - -
-
- - -
- - -
- - -
- -
- - -
-
- - -
- - -
- - -
- -
- -
- - -
-
- - - - +
+
+

Новый временной слот

+
-
\ No newline at end of file +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ +
+
+ +
+
+

Сетка пар

+ +
+
+ + + + + + + + + + + + + + + +
ПараНачалоОкончаниеМинутДействия
Загрузка...
+
+
+ +
+

Учебный год

+
+ +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+
+ +
+
+

Учебные годы

+ +
+
+ + + + + + + + + + + + + + +
НазваниеПериодСеместрыДействия
Загрузка...
+
+
+ +
+

Семестр

+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+
+ +
+

Праздники и исключения

+
+ +
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+
+ + + + + + + + + + + + + +
ДатаОписаниеДействия
Выберите учебный год
+
+
+ +
+

Матрица учебного графика

+
+
+ + +
+
+ + +
+
+ + +
+ + +
+ +
+
diff --git a/frontend/admin/views/users.html b/frontend/admin/views/users.html index bf282f3..6c9327e 100755 --- a/frontend/admin/views/users.html +++ b/frontend/admin/views/users.html @@ -49,141 +49,14 @@ Должность Кафедра Роль - Действия + Действия - Загрузка... + Загрузка...
- - - -
-
- - - - - - - -
-
\ No newline at end of file +