начало работы построения динамического расписания и документация
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.dto.*;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
import com.magistr.app.service.ScheduleGeneratorService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/calendar")
|
||||
public class AcademicCalendarAdminController {
|
||||
|
||||
private final AcademicYearRepository academicYearRepository;
|
||||
private final SemesterRepository semesterRepository;
|
||||
private final HolidayRepository holidayRepository;
|
||||
private final AcademicCalendarMatrixRepository calendarMatrixRepository;
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
|
||||
public AcademicCalendarAdminController(AcademicYearRepository academicYearRepository,
|
||||
SemesterRepository semesterRepository,
|
||||
HolidayRepository holidayRepository,
|
||||
AcademicCalendarMatrixRepository calendarMatrixRepository,
|
||||
ScheduleGeneratorService scheduleGeneratorService) {
|
||||
this.academicYearRepository = academicYearRepository;
|
||||
this.semesterRepository = semesterRepository;
|
||||
this.holidayRepository = holidayRepository;
|
||||
this.calendarMatrixRepository = calendarMatrixRepository;
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
}
|
||||
|
||||
@GetMapping("/years")
|
||||
public List<AcademicYearDto> getYears() {
|
||||
return academicYearRepository.findAll().stream()
|
||||
.sorted(Comparator.comparing(AcademicYear::getStartDate).reversed())
|
||||
.map(this::toAcademicYearDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping("/years")
|
||||
public ResponseEntity<?> createYear(@RequestBody AcademicYearDto request) {
|
||||
String validationError = validateYear(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
if (academicYearRepository.findByTitle(request.title()).isPresent()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год с таким названием уже существует"));
|
||||
}
|
||||
|
||||
AcademicYear year = new AcademicYear();
|
||||
applyYear(year, request);
|
||||
return ResponseEntity.ok(toAcademicYearDto(academicYearRepository.save(year)));
|
||||
}
|
||||
|
||||
@PutMapping("/years/{id}")
|
||||
public ResponseEntity<?> updateYear(@PathVariable Long id, @RequestBody AcademicYearDto request) {
|
||||
AcademicYear year = academicYearRepository.findById(id).orElse(null);
|
||||
if (year == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
String validationError = validateYear(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
applyYear(year, request);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toAcademicYearDto(academicYearRepository.save(year)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/years/{id}")
|
||||
public ResponseEntity<?> deleteYear(@PathVariable Long id) {
|
||||
if (!academicYearRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
academicYearRepository.deleteById(id);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Учебный год удалён"));
|
||||
}
|
||||
|
||||
@GetMapping("/years/{academicYearId}/semesters")
|
||||
public List<SemesterDto> getSemesters(@PathVariable Long academicYearId) {
|
||||
return semesterRepository.findByAcademicYearIdOrderByStartDateAsc(academicYearId).stream()
|
||||
.map(this::toSemesterDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping("/years/{academicYearId}/semesters")
|
||||
public ResponseEntity<?> createSemester(@PathVariable Long academicYearId, @RequestBody SemesterDto request) {
|
||||
AcademicYear year = academicYearRepository.findById(academicYearId).orElse(null);
|
||||
if (year == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год не найден"));
|
||||
}
|
||||
String validationError = validateSemester(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
if (semesterRepository.findByAcademicYearIdAndSemesterType(academicYearId, request.semesterType()).isPresent()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Такой семестр уже есть в учебном году"));
|
||||
}
|
||||
|
||||
Semester semester = new Semester();
|
||||
semester.setAcademicYear(year);
|
||||
applySemester(semester, request);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toSemesterDto(semesterRepository.save(semester)));
|
||||
}
|
||||
|
||||
@PutMapping("/semesters/{id}")
|
||||
public ResponseEntity<?> updateSemester(@PathVariable Long id, @RequestBody SemesterDto request) {
|
||||
Semester semester = semesterRepository.findById(id).orElse(null);
|
||||
if (semester == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
String validationError = validateSemester(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
applySemester(semester, request);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toSemesterDto(semesterRepository.save(semester)));
|
||||
}
|
||||
|
||||
@GetMapping("/holidays")
|
||||
public ResponseEntity<?> getHolidays(@RequestParam Long academicYearId) {
|
||||
if (!academicYearRepository.existsById(academicYearId)) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год не найден"));
|
||||
}
|
||||
return ResponseEntity.ok(holidayRepository.findByAcademicYearIdOrderByDateAsc(academicYearId).stream()
|
||||
.map(this::toHolidayDto)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/holidays")
|
||||
public ResponseEntity<?> createHoliday(@RequestBody HolidayDto request) {
|
||||
AcademicYear year = academicYearRepository.findById(request.academicYearId()).orElse(null);
|
||||
if (year == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год не найден"));
|
||||
}
|
||||
if (request.date() == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Дата праздника обязательна"));
|
||||
}
|
||||
if (holidayRepository.existsByAcademicYearIdAndDate(year.getId(), request.date())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Эта дата уже добавлена в исключения"));
|
||||
}
|
||||
|
||||
Holiday holiday = new Holiday();
|
||||
holiday.setAcademicYear(year);
|
||||
holiday.setDate(request.date());
|
||||
holiday.setDescription(request.description());
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toHolidayDto(holidayRepository.save(holiday)));
|
||||
}
|
||||
|
||||
@PutMapping("/holidays/{id}")
|
||||
public ResponseEntity<?> updateHoliday(@PathVariable Long id, @RequestBody HolidayDto request) {
|
||||
Holiday holiday = holidayRepository.findById(id).orElse(null);
|
||||
if (holiday == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (request.date() == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Дата праздника обязательна"));
|
||||
}
|
||||
holiday.setDate(request.date());
|
||||
holiday.setDescription(request.description());
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toHolidayDto(holidayRepository.save(holiday)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/holidays/{id}")
|
||||
public ResponseEntity<?> deleteHoliday(@PathVariable Long id) {
|
||||
if (!holidayRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
holidayRepository.deleteById(id);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Праздник удалён"));
|
||||
}
|
||||
|
||||
@GetMapping("/matrix")
|
||||
public ResponseEntity<?> getMatrix(@RequestParam Long semesterId,
|
||||
@RequestParam Integer courseNumber,
|
||||
@RequestParam Long specialtyId) {
|
||||
if (!semesterRepository.existsById(semesterId)) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Семестр не найден"));
|
||||
}
|
||||
return ResponseEntity.ok(calendarMatrixRepository
|
||||
.findBySemesterIdAndCourseNumberAndSpecialtyIdOrderByWeekNumberAsc(semesterId, courseNumber, specialtyId)
|
||||
.stream()
|
||||
.map(this::toMatrixDto)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@PutMapping("/matrix")
|
||||
@Transactional
|
||||
public ResponseEntity<?> saveMatrix(@RequestBody List<AcademicCalendarMatrixDto> rows) {
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Передайте хотя бы одну строку матрицы"));
|
||||
}
|
||||
|
||||
try {
|
||||
for (AcademicCalendarMatrixDto row : rows) {
|
||||
Semester semester = semesterRepository.findById(row.semesterId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Семестр не найден"));
|
||||
if (row.courseNumber() == null || row.courseNumber() <= 0
|
||||
|| row.specialtyId() == null
|
||||
|| row.weekNumber() == null || row.weekNumber() <= 0) {
|
||||
throw new IllegalArgumentException("Курс, специальность и номер недели обязательны");
|
||||
}
|
||||
AcademicCalendarMatrix matrix = calendarMatrixRepository
|
||||
.findBySemesterIdAndCourseNumberAndSpecialtyIdAndWeekNumber(
|
||||
row.semesterId(),
|
||||
row.courseNumber(),
|
||||
row.specialtyId(),
|
||||
row.weekNumber()
|
||||
)
|
||||
.orElseGet(AcademicCalendarMatrix::new);
|
||||
matrix.setSemester(semester);
|
||||
matrix.setCourseNumber(row.courseNumber());
|
||||
matrix.setSpecialtyId(row.specialtyId());
|
||||
matrix.setWeekNumber(row.weekNumber());
|
||||
matrix.setActivityType(row.activityType() == null ? AcademicActivityType.THEORY : row.activityType());
|
||||
calendarMatrixRepository.save(matrix);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Матрица учебного графика сохранена"));
|
||||
}
|
||||
|
||||
private String validateYear(AcademicYearDto request) {
|
||||
if (request.title() == null || request.title().isBlank()) {
|
||||
return "Название учебного года обязательно";
|
||||
}
|
||||
if (request.startDate() == null || request.endDate() == null) {
|
||||
return "Даты начала и окончания учебного года обязательны";
|
||||
}
|
||||
if (request.endDate().isBefore(request.startDate())) {
|
||||
return "Дата окончания не может быть раньше даты начала";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String validateSemester(SemesterDto request) {
|
||||
if (request.semesterType() == null) {
|
||||
return "Тип семестра обязателен";
|
||||
}
|
||||
if (request.startDate() == null || request.endDate() == null) {
|
||||
return "Даты начала и окончания семестра обязательны";
|
||||
}
|
||||
if (request.endDate().isBefore(request.startDate())) {
|
||||
return "Дата окончания семестра не может быть раньше даты начала";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyYear(AcademicYear year, AcademicYearDto request) {
|
||||
year.setTitle(request.title().trim());
|
||||
year.setStartDate(request.startDate());
|
||||
year.setEndDate(request.endDate());
|
||||
}
|
||||
|
||||
private void applySemester(Semester semester, SemesterDto request) {
|
||||
semester.setSemesterType(request.semesterType());
|
||||
semester.setStartDate(request.startDate());
|
||||
semester.setEndDate(request.endDate());
|
||||
}
|
||||
|
||||
private AcademicYearDto toAcademicYearDto(AcademicYear year) {
|
||||
List<SemesterDto> semesters = semesterRepository.findByAcademicYearIdOrderByStartDateAsc(year.getId())
|
||||
.stream()
|
||||
.map(this::toSemesterDto)
|
||||
.toList();
|
||||
return new AcademicYearDto(year.getId(), year.getTitle(), year.getStartDate(), year.getEndDate(), semesters);
|
||||
}
|
||||
|
||||
private SemesterDto toSemesterDto(Semester semester) {
|
||||
return new SemesterDto(
|
||||
semester.getId(),
|
||||
semester.getAcademicYear().getId(),
|
||||
semester.getAcademicYear().getTitle(),
|
||||
semester.getSemesterType(),
|
||||
semester.getStartDate(),
|
||||
semester.getEndDate()
|
||||
);
|
||||
}
|
||||
|
||||
private HolidayDto toHolidayDto(Holiday holiday) {
|
||||
return new HolidayDto(
|
||||
holiday.getId(),
|
||||
holiday.getDate(),
|
||||
holiday.getAcademicYear().getId(),
|
||||
holiday.getAcademicYear().getTitle(),
|
||||
holiday.getDescription()
|
||||
);
|
||||
}
|
||||
|
||||
private AcademicCalendarMatrixDto toMatrixDto(AcademicCalendarMatrix matrix) {
|
||||
return new AcademicCalendarMatrixDto(
|
||||
matrix.getId(),
|
||||
matrix.getSemester().getId(),
|
||||
matrix.getCourseNumber(),
|
||||
matrix.getSpecialtyId(),
|
||||
matrix.getWeekNumber(),
|
||||
matrix.getActivityType()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,6 @@ public class AuthController {
|
||||
String redirect = ROLE_REDIRECTS.getOrDefault(roleName, "/");
|
||||
Long departmentId = user.getDepartmentId();
|
||||
|
||||
return ResponseEntity.ok(new LoginResponse(true, "OK", token, roleName, redirect, departmentId));
|
||||
return ResponseEntity.ok(new LoginResponse(true, "OK", token, roleName, redirect, departmentId, user.getId()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ public class LessonsController {
|
||||
}
|
||||
|
||||
//Запрос для получения всего списка занятий
|
||||
@Deprecated
|
||||
@GetMapping
|
||||
public List<LessonResponse> getAllLessons() {
|
||||
logger.info("Запрос на получение всех занятий");
|
||||
@@ -221,6 +222,7 @@ public class LessonsController {
|
||||
}
|
||||
|
||||
//Запрос на получение всех занятий для конкретного преподавателя
|
||||
@Deprecated
|
||||
@GetMapping("/{teacherId}")
|
||||
public ResponseEntity<?> getLessonsById(@PathVariable Long teacherId) {
|
||||
logger.info("Запрос на получение занятий для преподавателя с ID: {}", teacherId);
|
||||
@@ -504,4 +506,4 @@ public class LessonsController {
|
||||
response.put("time", lesson.getTime());
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.dto.RenderedLessonDto;
|
||||
import com.magistr.app.service.ScheduleGeneratorService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/schedule")
|
||||
public class ScheduleController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScheduleController.class);
|
||||
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
|
||||
public ScheduleController(ScheduleGeneratorService scheduleGeneratorService) {
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> getSchedule(
|
||||
@RequestParam(required = false) Long groupId,
|
||||
@RequestParam(required = false) Long teacherId,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate
|
||||
) {
|
||||
logger.info("Запрос динамического расписания: groupId={}, teacherId={}, startDate={}, endDate={}",
|
||||
groupId, teacherId, startDate, endDate);
|
||||
|
||||
if ((groupId == null && teacherId == null) || (groupId != null && teacherId != null)) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("message", "Передайте ровно один параметр: groupId или teacherId"));
|
||||
}
|
||||
|
||||
try {
|
||||
List<RenderedLessonDto> lessons = groupId != null
|
||||
? scheduleGeneratorService.buildScheduleForGroup(groupId, startDate, endDate)
|
||||
: scheduleGeneratorService.buildScheduleForTeacher(teacherId, startDate, endDate);
|
||||
return ResponseEntity.ok(lessons);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.info("Ошибка запроса динамического расписания: {}", e.getMessage());
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка генерации динамического расписания: {}", e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError()
|
||||
.body(Map.of("message", "Произошла ошибка при генерации расписания"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.dto.ScheduleRuleDto;
|
||||
import com.magistr.app.dto.ScheduleRuleSlotDto;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
import com.magistr.app.service.ScheduleGeneratorService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/schedule-rules")
|
||||
public class ScheduleRuleAdminController {
|
||||
|
||||
private final ScheduleRuleRepository scheduleRuleRepository;
|
||||
private final SubjectRepository subjectRepository;
|
||||
private final SemesterRepository semesterRepository;
|
||||
private final GroupRepository groupRepository;
|
||||
private final TimeSlotRepository timeSlotRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final ClassroomRepository classroomRepository;
|
||||
private final LessonTypesRepository lessonTypesRepository;
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
|
||||
public ScheduleRuleAdminController(ScheduleRuleRepository scheduleRuleRepository,
|
||||
SubjectRepository subjectRepository,
|
||||
SemesterRepository semesterRepository,
|
||||
GroupRepository groupRepository,
|
||||
TimeSlotRepository timeSlotRepository,
|
||||
UserRepository userRepository,
|
||||
ClassroomRepository classroomRepository,
|
||||
LessonTypesRepository lessonTypesRepository,
|
||||
ScheduleGeneratorService scheduleGeneratorService) {
|
||||
this.scheduleRuleRepository = scheduleRuleRepository;
|
||||
this.subjectRepository = subjectRepository;
|
||||
this.semesterRepository = semesterRepository;
|
||||
this.groupRepository = groupRepository;
|
||||
this.timeSlotRepository = timeSlotRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.classroomRepository = classroomRepository;
|
||||
this.lessonTypesRepository = lessonTypesRepository;
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<ScheduleRuleDto> getAll(@RequestParam(required = false) Long semesterId,
|
||||
@RequestParam(required = false) Long groupId) {
|
||||
return scheduleRuleRepository.findAllWithDetails().stream()
|
||||
.filter(rule -> semesterId == null || Objects.equals(rule.getSemester().getId(), semesterId))
|
||||
.filter(rule -> groupId == null || rule.getGroups().stream().anyMatch(group -> Objects.equals(group.getId(), groupId)))
|
||||
.map(this::toDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<?> getById(@PathVariable Long id) {
|
||||
return scheduleRuleRepository.findByIdWithDetails(id)
|
||||
.<ResponseEntity<?>>map(rule -> ResponseEntity.ok(toDto(rule)))
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Transactional
|
||||
public ResponseEntity<?> create(@RequestBody ScheduleRuleDto request) {
|
||||
String validationError = validateRule(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
try {
|
||||
ScheduleRule rule = new ScheduleRule();
|
||||
applyRule(rule, request);
|
||||
ScheduleRule saved = scheduleRuleRepository.save(rule);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toDto(saved));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@Transactional
|
||||
public ResponseEntity<?> update(@PathVariable Long id, @RequestBody ScheduleRuleDto request) {
|
||||
ScheduleRule rule = scheduleRuleRepository.findByIdWithDetails(id).orElse(null);
|
||||
if (rule == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
String validationError = validateRule(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
try {
|
||||
applyRule(rule, request);
|
||||
ScheduleRule saved = scheduleRuleRepository.save(rule);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toDto(saved));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> delete(@PathVariable Long id) {
|
||||
if (!scheduleRuleRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
scheduleRuleRepository.deleteById(id);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Правило расписания удалено"));
|
||||
}
|
||||
|
||||
private String validateRule(ScheduleRuleDto request) {
|
||||
if (request.subjectId() == null) {
|
||||
return "Дисциплина обязательна";
|
||||
}
|
||||
if (request.semesterId() == null) {
|
||||
return "Семестр обязателен";
|
||||
}
|
||||
if (request.activeFromDate() == null) {
|
||||
return "Дата начала правила обязательна";
|
||||
}
|
||||
if (request.totalAcademicHours() == null || request.totalAcademicHours() <= 0) {
|
||||
return "Количество академических часов должно быть больше нуля";
|
||||
}
|
||||
if (request.groupIds() == null || request.groupIds().isEmpty()) {
|
||||
return "Нужно выбрать хотя бы одну группу";
|
||||
}
|
||||
if (request.slots() == null || request.slots().isEmpty()) {
|
||||
return "Добавьте хотя бы один слот занятия";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void applyRule(ScheduleRule rule, ScheduleRuleDto request) {
|
||||
Subject subject = subjectRepository.findById(request.subjectId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Дисциплина не найдена"));
|
||||
Semester semester = semesterRepository.findById(request.semesterId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Семестр не найден"));
|
||||
List<StudentGroup> groups = groupRepository.findAllById(request.groupIds());
|
||||
if (groups.size() != new HashSet<>(request.groupIds()).size()) {
|
||||
throw new IllegalArgumentException("Одна или несколько групп не найдены");
|
||||
}
|
||||
|
||||
rule.setSubject(subject);
|
||||
rule.setSemester(semester);
|
||||
rule.setActiveFromDate(request.activeFromDate());
|
||||
rule.setTotalAcademicHours(request.totalAcademicHours());
|
||||
rule.getGroups().clear();
|
||||
rule.getGroups().addAll(groups);
|
||||
|
||||
rule.getSlots().clear();
|
||||
for (ScheduleRuleSlotDto slotDto : request.slots()) {
|
||||
rule.getSlots().add(buildSlot(rule, slotDto));
|
||||
}
|
||||
}
|
||||
|
||||
private ScheduleRuleSlot buildSlot(ScheduleRule rule, ScheduleRuleSlotDto slotDto) {
|
||||
if (slotDto.dayOfWeek() == null || slotDto.dayOfWeek() < 1 || slotDto.dayOfWeek() > 7) {
|
||||
throw new IllegalArgumentException("День недели должен быть от 1 до 7");
|
||||
}
|
||||
if (slotDto.parity() == null) {
|
||||
throw new IllegalArgumentException("Чётность недели обязательна");
|
||||
}
|
||||
TimeSlot timeSlot = timeSlotRepository.findById(slotDto.timeSlotId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Временной слот не найден"));
|
||||
User teacher = userRepository.findById(slotDto.teacherId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Преподаватель не найден"));
|
||||
Classroom classroom = classroomRepository.findById(slotDto.classroomId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Аудитория не найдена"));
|
||||
LessonType lessonType = lessonTypesRepository.findById(slotDto.lessonTypeId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Тип занятия не найден"));
|
||||
if (slotDto.lessonFormat() == null || slotDto.lessonFormat().isBlank()) {
|
||||
throw new IllegalArgumentException("Формат занятия обязателен");
|
||||
}
|
||||
|
||||
ScheduleRuleSlot slot = new ScheduleRuleSlot();
|
||||
slot.setScheduleRule(rule);
|
||||
slot.setDayOfWeek(slotDto.dayOfWeek());
|
||||
slot.setParity(slotDto.parity());
|
||||
slot.setTimeSlot(timeSlot);
|
||||
slot.setSubgroupId(slotDto.subgroupId());
|
||||
slot.setTeacher(teacher);
|
||||
slot.setClassroom(classroom);
|
||||
slot.setLessonType(lessonType);
|
||||
slot.setLessonFormat(slotDto.lessonFormat().trim());
|
||||
return slot;
|
||||
}
|
||||
|
||||
private ScheduleRuleDto toDto(ScheduleRule rule) {
|
||||
List<StudentGroup> groups = rule.getGroups().stream()
|
||||
.sorted(Comparator.comparing(StudentGroup::getName))
|
||||
.toList();
|
||||
List<ScheduleRuleSlotDto> slots = rule.getSlots().stream()
|
||||
.sorted(Comparator
|
||||
.comparing(ScheduleRuleSlot::getDayOfWeek)
|
||||
.thenComparing(slot -> slot.getTimeSlot().getOrderNumber())
|
||||
.thenComparing(ScheduleRuleSlot::getId))
|
||||
.map(this::toSlotDto)
|
||||
.toList();
|
||||
|
||||
return new ScheduleRuleDto(
|
||||
rule.getId(),
|
||||
rule.getSubject().getId(),
|
||||
rule.getSubject().getName(),
|
||||
rule.getSemester().getId(),
|
||||
rule.getSemester().getAcademicYear().getTitle(),
|
||||
rule.getSemester().getSemesterType(),
|
||||
rule.getActiveFromDate(),
|
||||
rule.getTotalAcademicHours(),
|
||||
groups.stream().map(StudentGroup::getId).toList(),
|
||||
groups.stream().map(StudentGroup::getName).toList(),
|
||||
slots
|
||||
);
|
||||
}
|
||||
|
||||
private ScheduleRuleSlotDto toSlotDto(ScheduleRuleSlot slot) {
|
||||
TimeSlot timeSlot = slot.getTimeSlot();
|
||||
return new ScheduleRuleSlotDto(
|
||||
slot.getId(),
|
||||
slot.getDayOfWeek(),
|
||||
dayName(slot.getDayOfWeek()),
|
||||
slot.getParity(),
|
||||
timeSlot.getId(),
|
||||
timeSlot.getOrderNumber(),
|
||||
formatTimeSlot(timeSlot),
|
||||
slot.getSubgroupId(),
|
||||
slot.getTeacher().getId(),
|
||||
ScheduleGeneratorService.displayUserName(slot.getTeacher()),
|
||||
slot.getClassroom().getId(),
|
||||
slot.getClassroom().getName(),
|
||||
slot.getLessonType().getId(),
|
||||
slot.getLessonType().getLessonType(),
|
||||
slot.getLessonFormat()
|
||||
);
|
||||
}
|
||||
|
||||
private String dayName(Integer dayOfWeek) {
|
||||
if (dayOfWeek == null || dayOfWeek < 1 || dayOfWeek > 7) {
|
||||
return "Неизвестно";
|
||||
}
|
||||
return ScheduleGeneratorService.dayName(DayOfWeek.of(dayOfWeek));
|
||||
}
|
||||
|
||||
private String formatTimeSlot(TimeSlot timeSlot) {
|
||||
return timeSlot.getStartTime() + " - " + timeSlot.getEndTime();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.dto.TimeSlotDto;
|
||||
import com.magistr.app.model.TimeSlot;
|
||||
import com.magistr.app.repository.TimeSlotRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/time-slots")
|
||||
public class TimeSlotAdminController {
|
||||
|
||||
private final TimeSlotRepository timeSlotRepository;
|
||||
|
||||
public TimeSlotAdminController(TimeSlotRepository timeSlotRepository) {
|
||||
this.timeSlotRepository = timeSlotRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<TimeSlotDto> getAll() {
|
||||
return timeSlotRepository.findAllByOrderByOrderNumberAsc().stream()
|
||||
.map(this::toDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> create(@RequestBody TimeSlotDto request) {
|
||||
String validationError = validate(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
if (timeSlotRepository.existsByOrderNumber(request.orderNumber())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Слот с таким номером пары уже существует"));
|
||||
}
|
||||
|
||||
TimeSlot timeSlot = new TimeSlot();
|
||||
apply(timeSlot, request);
|
||||
return ResponseEntity.ok(toDto(timeSlotRepository.save(timeSlot)));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> update(@PathVariable Long id, @RequestBody TimeSlotDto request) {
|
||||
TimeSlot timeSlot = timeSlotRepository.findById(id).orElse(null);
|
||||
if (timeSlot == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
String validationError = validate(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
if (timeSlotRepository.existsByOrderNumber(request.orderNumber())
|
||||
&& !timeSlot.getOrderNumber().equals(request.orderNumber())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Слот с таким номером пары уже существует"));
|
||||
}
|
||||
|
||||
apply(timeSlot, request);
|
||||
return ResponseEntity.ok(toDto(timeSlotRepository.save(timeSlot)));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> delete(@PathVariable Long id) {
|
||||
if (!timeSlotRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
timeSlotRepository.deleteById(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Временной слот удалён"));
|
||||
}
|
||||
|
||||
private String validate(TimeSlotDto request) {
|
||||
if (request.orderNumber() == null || request.orderNumber() <= 0) {
|
||||
return "Номер пары должен быть больше нуля";
|
||||
}
|
||||
if (request.startTime() == null || request.endTime() == null) {
|
||||
return "Время начала и окончания обязательно";
|
||||
}
|
||||
if (!request.startTime().isBefore(request.endTime())) {
|
||||
return "Время начала должно быть раньше времени окончания";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void apply(TimeSlot timeSlot, TimeSlotDto request) {
|
||||
timeSlot.setOrderNumber(request.orderNumber());
|
||||
timeSlot.setStartTime(request.startTime());
|
||||
timeSlot.setEndTime(request.endTime());
|
||||
int duration = request.durationMinutes() != null && request.durationMinutes() > 0
|
||||
? request.durationMinutes()
|
||||
: (int) Duration.between(request.startTime(), request.endTime()).toMinutes();
|
||||
timeSlot.setDurationMinutes(duration);
|
||||
}
|
||||
|
||||
private TimeSlotDto toDto(TimeSlot timeSlot) {
|
||||
return new TimeSlotDto(
|
||||
timeSlot.getId(),
|
||||
timeSlot.getOrderNumber(),
|
||||
timeSlot.getStartTime(),
|
||||
timeSlot.getEndTime(),
|
||||
timeSlot.getDurationMinutes()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.AcademicActivityType;
|
||||
|
||||
public record AcademicCalendarMatrixDto(
|
||||
Long id,
|
||||
Long semesterId,
|
||||
Integer courseNumber,
|
||||
Long specialtyId,
|
||||
Integer weekNumber,
|
||||
AcademicActivityType activityType
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public record AcademicYearDto(
|
||||
Long id,
|
||||
String title,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
List<SemesterDto> semesters
|
||||
) {
|
||||
}
|
||||
12
backend/src/main/java/com/magistr/app/dto/HolidayDto.java
Normal file
12
backend/src/main/java/com/magistr/app/dto/HolidayDto.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record HolidayDto(
|
||||
Long id,
|
||||
LocalDate date,
|
||||
Long academicYearId,
|
||||
String academicYearTitle,
|
||||
String description
|
||||
) {
|
||||
}
|
||||
@@ -8,17 +8,23 @@ public class LoginResponse {
|
||||
private String role;
|
||||
private String redirect;
|
||||
private Long departmentId;
|
||||
private Long userId;
|
||||
|
||||
public LoginResponse() {
|
||||
}
|
||||
|
||||
public LoginResponse(boolean success, String message, String token, String role, String redirect, Long departmentId) {
|
||||
this(success, message, token, role, redirect, departmentId, null);
|
||||
}
|
||||
|
||||
public LoginResponse(boolean success, String message, String token, String role, String redirect, Long departmentId, Long userId) {
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
this.token = token;
|
||||
this.role = role;
|
||||
this.redirect = redirect;
|
||||
this.departmentId = departmentId;
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public boolean isSuccess() {
|
||||
@@ -68,4 +74,12 @@ public class LoginResponse {
|
||||
public void setDepartmentId(Long departmentId) {
|
||||
this.departmentId = departmentId;
|
||||
}
|
||||
|
||||
public Long getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.AcademicActivityType;
|
||||
import com.magistr.app.model.ScheduleParity;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
|
||||
public record RenderedLessonDto(
|
||||
Long scheduleRuleId,
|
||||
Long scheduleRuleSlotId,
|
||||
LocalDate date,
|
||||
Integer dayOfWeek,
|
||||
String dayName,
|
||||
Integer weekNumber,
|
||||
ScheduleParity parity,
|
||||
Long timeSlotId,
|
||||
Integer timeSlotOrder,
|
||||
LocalTime startTime,
|
||||
LocalTime endTime,
|
||||
Long subjectId,
|
||||
String subjectName,
|
||||
Long teacherId,
|
||||
String teacherName,
|
||||
Long classroomId,
|
||||
String classroomName,
|
||||
Long lessonTypeId,
|
||||
String lessonTypeName,
|
||||
String lessonFormat,
|
||||
Long subgroupId,
|
||||
List<Long> groupIds,
|
||||
List<String> groupNames,
|
||||
AcademicActivityType activityType,
|
||||
Integer totalAcademicHours,
|
||||
Integer consumedAcademicHoursBeforeLesson,
|
||||
Integer remainingAcademicHoursAfterLesson
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.SemesterType;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public record ScheduleRuleDto(
|
||||
Long id,
|
||||
Long subjectId,
|
||||
String subjectName,
|
||||
Long semesterId,
|
||||
String academicYearTitle,
|
||||
SemesterType semesterType,
|
||||
LocalDate activeFromDate,
|
||||
Integer totalAcademicHours,
|
||||
List<Long> groupIds,
|
||||
List<String> groupNames,
|
||||
List<ScheduleRuleSlotDto> slots
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.ScheduleParity;
|
||||
|
||||
public record ScheduleRuleSlotDto(
|
||||
Long id,
|
||||
Integer dayOfWeek,
|
||||
String dayName,
|
||||
ScheduleParity parity,
|
||||
Long timeSlotId,
|
||||
Integer timeSlotOrder,
|
||||
String timeSlotLabel,
|
||||
Long subgroupId,
|
||||
Long teacherId,
|
||||
String teacherName,
|
||||
Long classroomId,
|
||||
String classroomName,
|
||||
Long lessonTypeId,
|
||||
String lessonTypeName,
|
||||
String lessonFormat
|
||||
) {
|
||||
}
|
||||
15
backend/src/main/java/com/magistr/app/dto/SemesterDto.java
Normal file
15
backend/src/main/java/com/magistr/app/dto/SemesterDto.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.SemesterType;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record SemesterDto(
|
||||
Long id,
|
||||
Long academicYearId,
|
||||
String academicYearTitle,
|
||||
SemesterType semesterType,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate
|
||||
) {
|
||||
}
|
||||
12
backend/src/main/java/com/magistr/app/dto/TimeSlotDto.java
Normal file
12
backend/src/main/java/com/magistr/app/dto/TimeSlotDto.java
Normal file
@@ -0,0 +1,12 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalTime;
|
||||
|
||||
public record TimeSlotDto(
|
||||
Long id,
|
||||
Integer orderNumber,
|
||||
LocalTime startTime,
|
||||
LocalTime endTime,
|
||||
Integer durationMinutes
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
public enum AcademicActivityType {
|
||||
THEORY,
|
||||
EXAM,
|
||||
VACATION,
|
||||
PRACTICE
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "academic_calendar_matrix")
|
||||
public class AcademicCalendarMatrix {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "semester_id", nullable = false)
|
||||
private Semester semester;
|
||||
|
||||
@Column(name = "course_number", nullable = false)
|
||||
private Integer courseNumber;
|
||||
|
||||
@Column(name = "specialty_id", nullable = false)
|
||||
private Long specialtyId;
|
||||
|
||||
@Column(name = "week_number", nullable = false)
|
||||
private Integer weekNumber;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "activity_type", nullable = false, length = 20)
|
||||
private AcademicActivityType activityType;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Semester getSemester() {
|
||||
return semester;
|
||||
}
|
||||
|
||||
public void setSemester(Semester semester) {
|
||||
this.semester = semester;
|
||||
}
|
||||
|
||||
public Integer getCourseNumber() {
|
||||
return courseNumber;
|
||||
}
|
||||
|
||||
public void setCourseNumber(Integer courseNumber) {
|
||||
this.courseNumber = courseNumber;
|
||||
}
|
||||
|
||||
public Long getSpecialtyId() {
|
||||
return specialtyId;
|
||||
}
|
||||
|
||||
public void setSpecialtyId(Long specialtyId) {
|
||||
this.specialtyId = specialtyId;
|
||||
}
|
||||
|
||||
public Integer getWeekNumber() {
|
||||
return weekNumber;
|
||||
}
|
||||
|
||||
public void setWeekNumber(Integer weekNumber) {
|
||||
this.weekNumber = weekNumber;
|
||||
}
|
||||
|
||||
public AcademicActivityType getActivityType() {
|
||||
return activityType;
|
||||
}
|
||||
|
||||
public void setActivityType(AcademicActivityType activityType) {
|
||||
this.activityType = activityType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "academic_years")
|
||||
public class AcademicYear {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false, unique = true, length = 20)
|
||||
private String title;
|
||||
|
||||
@Column(name = "start_date", nullable = false)
|
||||
private LocalDate startDate;
|
||||
|
||||
@Column(name = "end_date", nullable = false)
|
||||
private LocalDate endDate;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public LocalDate getStartDate() {
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setStartDate(LocalDate startDate) {
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public LocalDate getEndDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setEndDate(LocalDate endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
}
|
||||
56
backend/src/main/java/com/magistr/app/model/Holiday.java
Normal file
56
backend/src/main/java/com/magistr/app/model/Holiday.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "holidays")
|
||||
public class Holiday {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDate date;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "academic_year_id", nullable = false)
|
||||
private AcademicYear academicYear;
|
||||
|
||||
@Column(length = 255)
|
||||
private String description;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public LocalDate getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(LocalDate date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public AcademicYear getAcademicYear() {
|
||||
return academicYear;
|
||||
}
|
||||
|
||||
public void setAcademicYear(AcademicYear academicYear) {
|
||||
this.academicYear = academicYear;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
public enum ScheduleParity {
|
||||
BOTH,
|
||||
EVEN,
|
||||
ODD
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "schedule_rules")
|
||||
public class ScheduleRule {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "subject_id", nullable = false)
|
||||
private Subject subject;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "semester_id", nullable = false)
|
||||
private Semester semester;
|
||||
|
||||
@Column(name = "active_from_date", nullable = false)
|
||||
private LocalDate activeFromDate;
|
||||
|
||||
@Column(name = "total_academic_hours", nullable = false)
|
||||
private Integer totalAcademicHours;
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "schedule_rule_groups",
|
||||
joinColumns = @JoinColumn(name = "schedule_rule_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "group_id")
|
||||
)
|
||||
private Set<StudentGroup> groups = new HashSet<>();
|
||||
|
||||
@OneToMany(mappedBy = "scheduleRule", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
private Set<ScheduleRuleSlot> slots = new HashSet<>();
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Subject getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public void setSubject(Subject subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public Semester getSemester() {
|
||||
return semester;
|
||||
}
|
||||
|
||||
public void setSemester(Semester semester) {
|
||||
this.semester = semester;
|
||||
}
|
||||
|
||||
public LocalDate getActiveFromDate() {
|
||||
return activeFromDate;
|
||||
}
|
||||
|
||||
public void setActiveFromDate(LocalDate activeFromDate) {
|
||||
this.activeFromDate = activeFromDate;
|
||||
}
|
||||
|
||||
public Integer getTotalAcademicHours() {
|
||||
return totalAcademicHours;
|
||||
}
|
||||
|
||||
public void setTotalAcademicHours(Integer totalAcademicHours) {
|
||||
this.totalAcademicHours = totalAcademicHours;
|
||||
}
|
||||
|
||||
public Set<StudentGroup> getGroups() {
|
||||
return groups;
|
||||
}
|
||||
|
||||
public void setGroups(Set<StudentGroup> groups) {
|
||||
this.groups = groups;
|
||||
}
|
||||
|
||||
public Set<ScheduleRuleSlot> getSlots() {
|
||||
return slots;
|
||||
}
|
||||
|
||||
public void setSlots(Set<ScheduleRuleSlot> slots) {
|
||||
this.slots = slots;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "schedule_rule_slots")
|
||||
public class ScheduleRuleSlot {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "schedule_rule_id", nullable = false)
|
||||
private ScheduleRule scheduleRule;
|
||||
|
||||
@Column(name = "day_of_week", nullable = false)
|
||||
private Integer dayOfWeek;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false, length = 10)
|
||||
private ScheduleParity parity;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "time_slot_id", nullable = false)
|
||||
private TimeSlot timeSlot;
|
||||
|
||||
@Column(name = "subgroup_id")
|
||||
private Long subgroupId;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "teacher_id", nullable = false)
|
||||
private User teacher;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "classroom_id", nullable = false)
|
||||
private Classroom classroom;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "lesson_type_id", nullable = false)
|
||||
private LessonType lessonType;
|
||||
|
||||
@Column(name = "lesson_format", nullable = false, length = 30)
|
||||
private String lessonFormat;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public ScheduleRule getScheduleRule() {
|
||||
return scheduleRule;
|
||||
}
|
||||
|
||||
public void setScheduleRule(ScheduleRule scheduleRule) {
|
||||
this.scheduleRule = scheduleRule;
|
||||
}
|
||||
|
||||
public Integer getDayOfWeek() {
|
||||
return dayOfWeek;
|
||||
}
|
||||
|
||||
public void setDayOfWeek(Integer dayOfWeek) {
|
||||
this.dayOfWeek = dayOfWeek;
|
||||
}
|
||||
|
||||
public ScheduleParity getParity() {
|
||||
return parity;
|
||||
}
|
||||
|
||||
public void setParity(ScheduleParity parity) {
|
||||
this.parity = parity;
|
||||
}
|
||||
|
||||
public TimeSlot getTimeSlot() {
|
||||
return timeSlot;
|
||||
}
|
||||
|
||||
public void setTimeSlot(TimeSlot timeSlot) {
|
||||
this.timeSlot = timeSlot;
|
||||
}
|
||||
|
||||
public Long getSubgroupId() {
|
||||
return subgroupId;
|
||||
}
|
||||
|
||||
public void setSubgroupId(Long subgroupId) {
|
||||
this.subgroupId = subgroupId;
|
||||
}
|
||||
|
||||
public User getTeacher() {
|
||||
return teacher;
|
||||
}
|
||||
|
||||
public void setTeacher(User teacher) {
|
||||
this.teacher = teacher;
|
||||
}
|
||||
|
||||
public Classroom getClassroom() {
|
||||
return classroom;
|
||||
}
|
||||
|
||||
public void setClassroom(Classroom classroom) {
|
||||
this.classroom = classroom;
|
||||
}
|
||||
|
||||
public LessonType getLessonType() {
|
||||
return lessonType;
|
||||
}
|
||||
|
||||
public void setLessonType(LessonType lessonType) {
|
||||
this.lessonType = lessonType;
|
||||
}
|
||||
|
||||
public String getLessonFormat() {
|
||||
return lessonFormat;
|
||||
}
|
||||
|
||||
public void setLessonFormat(String lessonFormat) {
|
||||
this.lessonFormat = lessonFormat;
|
||||
}
|
||||
}
|
||||
68
backend/src/main/java/com/magistr/app/model/Semester.java
Normal file
68
backend/src/main/java/com/magistr/app/model/Semester.java
Normal file
@@ -0,0 +1,68 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Entity
|
||||
@Table(name = "semesters")
|
||||
public class Semester {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "academic_year_id", nullable = false)
|
||||
private AcademicYear academicYear;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "semester_type", nullable = false, length = 20)
|
||||
private SemesterType semesterType;
|
||||
|
||||
@Column(name = "start_date", nullable = false)
|
||||
private LocalDate startDate;
|
||||
|
||||
@Column(name = "end_date", nullable = false)
|
||||
private LocalDate endDate;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public AcademicYear getAcademicYear() {
|
||||
return academicYear;
|
||||
}
|
||||
|
||||
public void setAcademicYear(AcademicYear academicYear) {
|
||||
this.academicYear = academicYear;
|
||||
}
|
||||
|
||||
public SemesterType getSemesterType() {
|
||||
return semesterType;
|
||||
}
|
||||
|
||||
public void setSemesterType(SemesterType semesterType) {
|
||||
this.semesterType = semesterType;
|
||||
}
|
||||
|
||||
public LocalDate getStartDate() {
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setStartDate(LocalDate startDate) {
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public LocalDate getEndDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setEndDate(LocalDate endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
}
|
||||
66
backend/src/main/java/com/magistr/app/model/TimeSlot.java
Normal file
66
backend/src/main/java/com/magistr/app/model/TimeSlot.java
Normal file
@@ -0,0 +1,66 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "time_slots")
|
||||
public class TimeSlot {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "order_number", nullable = false)
|
||||
private Integer orderNumber;
|
||||
|
||||
@Column(name = "start_time", nullable = false)
|
||||
private LocalTime startTime;
|
||||
|
||||
@Column(name = "end_time", nullable = false)
|
||||
private LocalTime endTime;
|
||||
|
||||
@Column(name = "duration_minutes", nullable = false)
|
||||
private Integer durationMinutes;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public Integer getOrderNumber() {
|
||||
return orderNumber;
|
||||
}
|
||||
|
||||
public void setOrderNumber(Integer orderNumber) {
|
||||
this.orderNumber = orderNumber;
|
||||
}
|
||||
|
||||
public LocalTime getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public void setStartTime(LocalTime startTime) {
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public LocalTime getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public void setEndTime(LocalTime endTime) {
|
||||
this.endTime = endTime;
|
||||
}
|
||||
|
||||
public Integer getDurationMinutes() {
|
||||
return durationMinutes;
|
||||
}
|
||||
|
||||
public void setDurationMinutes(Integer durationMinutes) {
|
||||
this.durationMinutes = durationMinutes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.AcademicCalendarMatrix;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AcademicCalendarMatrixRepository extends JpaRepository<AcademicCalendarMatrix, Long> {
|
||||
|
||||
Optional<AcademicCalendarMatrix> findBySemesterIdAndCourseNumberAndSpecialtyIdAndWeekNumber(
|
||||
Long semesterId,
|
||||
Integer courseNumber,
|
||||
Long specialtyId,
|
||||
Integer weekNumber
|
||||
);
|
||||
|
||||
List<AcademicCalendarMatrix> findBySemesterIdAndCourseNumberAndSpecialtyIdOrderByWeekNumberAsc(
|
||||
Long semesterId,
|
||||
Integer courseNumber,
|
||||
Long specialtyId
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.AcademicYear;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AcademicYearRepository extends JpaRepository<AcademicYear, Long> {
|
||||
|
||||
Optional<AcademicYear> findByTitle(String title);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.Holiday;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public interface HolidayRepository extends JpaRepository<Holiday, Long> {
|
||||
|
||||
List<Holiday> findByAcademicYearIdOrderByDateAsc(Long academicYearId);
|
||||
|
||||
List<Holiday> findByAcademicYearIdAndDateBetween(Long academicYearId, LocalDate startDate, LocalDate endDate);
|
||||
|
||||
boolean existsByAcademicYearIdAndDate(Long academicYearId, LocalDate date);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.ScheduleRule;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ScheduleRuleRepository extends JpaRepository<ScheduleRule, Long> {
|
||||
|
||||
@Query("""
|
||||
select distinct r
|
||||
from ScheduleRule r
|
||||
join r.groups g
|
||||
left join fetch r.subject
|
||||
left join fetch r.semester sem
|
||||
left join fetch sem.academicYear
|
||||
left join fetch r.groups groups
|
||||
left join fetch r.slots slots
|
||||
left join fetch slots.timeSlot
|
||||
left join fetch slots.teacher
|
||||
left join fetch slots.classroom
|
||||
left join fetch slots.lessonType
|
||||
where g.id = :groupId
|
||||
and sem.id = :semesterId
|
||||
""")
|
||||
List<ScheduleRule> findByGroupIdAndSemesterId(
|
||||
@Param("groupId") Long groupId,
|
||||
@Param("semesterId") Long semesterId
|
||||
);
|
||||
|
||||
@Query("""
|
||||
select distinct r
|
||||
from ScheduleRule r
|
||||
join r.slots teacherSlot
|
||||
left join fetch r.subject
|
||||
left join fetch r.semester sem
|
||||
left join fetch sem.academicYear
|
||||
left join fetch r.groups groups
|
||||
left join fetch r.slots slots
|
||||
left join fetch slots.timeSlot
|
||||
left join fetch slots.teacher
|
||||
left join fetch slots.classroom
|
||||
left join fetch slots.lessonType
|
||||
where teacherSlot.teacher.id = :teacherId
|
||||
and sem.id = :semesterId
|
||||
""")
|
||||
List<ScheduleRule> findByTeacherIdAndSemesterId(
|
||||
@Param("teacherId") Long teacherId,
|
||||
@Param("semesterId") Long semesterId
|
||||
);
|
||||
|
||||
@Query("""
|
||||
select distinct r
|
||||
from ScheduleRule r
|
||||
left join fetch r.subject
|
||||
left join fetch r.semester sem
|
||||
left join fetch sem.academicYear
|
||||
left join fetch r.groups groups
|
||||
left join fetch r.slots slots
|
||||
left join fetch slots.timeSlot
|
||||
left join fetch slots.teacher
|
||||
left join fetch slots.classroom
|
||||
left join fetch slots.lessonType
|
||||
order by r.id desc
|
||||
""")
|
||||
List<ScheduleRule> findAllWithDetails();
|
||||
|
||||
@Query("""
|
||||
select distinct r
|
||||
from ScheduleRule r
|
||||
left join fetch r.subject
|
||||
left join fetch r.semester sem
|
||||
left join fetch sem.academicYear
|
||||
left join fetch r.groups groups
|
||||
left join fetch r.slots slots
|
||||
left join fetch slots.timeSlot
|
||||
left join fetch slots.teacher
|
||||
left join fetch slots.classroom
|
||||
left join fetch slots.lessonType
|
||||
where r.id = :id
|
||||
""")
|
||||
java.util.Optional<ScheduleRule> findByIdWithDetails(@Param("id") Long id);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.ScheduleRuleSlot;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface ScheduleRuleSlotRepository extends JpaRepository<ScheduleRuleSlot, Long> {
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.Semester;
|
||||
import com.magistr.app.model.SemesterType;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface SemesterRepository extends JpaRepository<Semester, Long> {
|
||||
|
||||
Optional<Semester> findFirstByStartDateLessThanEqualAndEndDateGreaterThanEqual(LocalDate startDate, LocalDate endDate);
|
||||
|
||||
List<Semester> findByAcademicYearIdOrderByStartDateAsc(Long academicYearId);
|
||||
|
||||
Optional<Semester> findByAcademicYearIdAndSemesterType(Long academicYearId, SemesterType semesterType);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.TimeSlot;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface TimeSlotRepository extends JpaRepository<TimeSlot, Long> {
|
||||
|
||||
List<TimeSlot> findAllByOrderByOrderNumberAsc();
|
||||
|
||||
boolean existsByOrderNumber(Integer orderNumber);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.magistr.app.service;
|
||||
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.AcademicCalendarMatrixRepository;
|
||||
import com.magistr.app.repository.HolidayRepository;
|
||||
import com.magistr.app.repository.SemesterRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
public class AcademicDateService {
|
||||
|
||||
private final SemesterRepository semesterRepository;
|
||||
private final HolidayRepository holidayRepository;
|
||||
private final AcademicCalendarMatrixRepository calendarMatrixRepository;
|
||||
private final boolean evenWeekIsUpper;
|
||||
|
||||
public AcademicDateService(SemesterRepository semesterRepository,
|
||||
HolidayRepository holidayRepository,
|
||||
AcademicCalendarMatrixRepository calendarMatrixRepository,
|
||||
@Value("${app.schedule.even-week-is-upper:false}") boolean evenWeekIsUpper) {
|
||||
this.semesterRepository = semesterRepository;
|
||||
this.holidayRepository = holidayRepository;
|
||||
this.calendarMatrixRepository = calendarMatrixRepository;
|
||||
this.evenWeekIsUpper = evenWeekIsUpper;
|
||||
}
|
||||
|
||||
public Optional<Semester> findSemester(LocalDate date) {
|
||||
return semesterRepository.findFirstByStartDateLessThanEqualAndEndDateGreaterThanEqual(date, date);
|
||||
}
|
||||
|
||||
public int getWeekNumber(Semester semester, LocalDate date) {
|
||||
long days = ChronoUnit.DAYS.between(semester.getStartDate(), date);
|
||||
if (days < 0) {
|
||||
return 0;
|
||||
}
|
||||
return (int) (days / 7) + 1;
|
||||
}
|
||||
|
||||
public ScheduleParity getParity(Semester semester, LocalDate date) {
|
||||
int weekNumber = getWeekNumber(semester, date);
|
||||
boolean evenWeek = weekNumber % 2 == 0;
|
||||
if (evenWeekIsUpper) {
|
||||
return evenWeek ? ScheduleParity.ODD : ScheduleParity.EVEN;
|
||||
}
|
||||
return evenWeek ? ScheduleParity.EVEN : ScheduleParity.ODD;
|
||||
}
|
||||
|
||||
public boolean isHoliday(Semester semester, LocalDate date) {
|
||||
if (semester == null || semester.getAcademicYear() == null) {
|
||||
return false;
|
||||
}
|
||||
return holidayRepository.existsByAcademicYearIdAndDate(semester.getAcademicYear().getId(), date);
|
||||
}
|
||||
|
||||
public int getCourseNumber(StudentGroup group, LocalDate date) {
|
||||
int academicYearStart = date.getMonthValue() >= 9 ? date.getYear() : date.getYear() - 1;
|
||||
return academicYearStart - group.getYearStartStudy() + 1;
|
||||
}
|
||||
|
||||
public AcademicActivityType getActivityType(StudentGroup group, Semester semester, LocalDate date) {
|
||||
int weekNumber = getWeekNumber(semester, date);
|
||||
int courseNumber = getCourseNumber(group, date);
|
||||
if (weekNumber < 1 || courseNumber < 1) {
|
||||
return AcademicActivityType.VACATION;
|
||||
}
|
||||
|
||||
return calendarMatrixRepository
|
||||
.findBySemesterIdAndCourseNumberAndSpecialtyIdAndWeekNumber(
|
||||
semester.getId(),
|
||||
courseNumber,
|
||||
group.getSpecialityCode(),
|
||||
weekNumber
|
||||
)
|
||||
.map(AcademicCalendarMatrix::getActivityType)
|
||||
.orElse(AcademicActivityType.THEORY);
|
||||
}
|
||||
|
||||
public boolean isTheoryDay(StudentGroup group, Semester semester, LocalDate date) {
|
||||
return getActivityType(group, semester, date) == AcademicActivityType.THEORY
|
||||
&& !isHoliday(semester, date);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package com.magistr.app.service;
|
||||
|
||||
import com.magistr.app.dto.RenderedLessonDto;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.GroupRepository;
|
||||
import com.magistr.app.repository.ScheduleRuleRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.TextStyle;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class ScheduleGeneratorService {
|
||||
|
||||
private static final int ACADEMIC_HOURS_PER_SLOT = 2;
|
||||
private static final long MAX_RANGE_DAYS = 120;
|
||||
|
||||
private final ScheduleRuleRepository scheduleRuleRepository;
|
||||
private final GroupRepository groupRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final AcademicDateService academicDateService;
|
||||
private final Map<String, Integer> consumedHoursCache = new ConcurrentHashMap<>();
|
||||
|
||||
public ScheduleGeneratorService(ScheduleRuleRepository scheduleRuleRepository,
|
||||
GroupRepository groupRepository,
|
||||
UserRepository userRepository,
|
||||
AcademicDateService academicDateService) {
|
||||
this.scheduleRuleRepository = scheduleRuleRepository;
|
||||
this.groupRepository = groupRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.academicDateService = academicDateService;
|
||||
}
|
||||
|
||||
public List<RenderedLessonDto> buildScheduleForGroup(Long groupId, LocalDate startDate, LocalDate endDate) {
|
||||
validateRange(startDate, endDate);
|
||||
StudentGroup group = groupRepository.findById(groupId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Группа не найдена"));
|
||||
|
||||
Map<Long, List<ScheduleRule>> rulesBySemester = new HashMap<>();
|
||||
List<RenderedLessonDto> result = new ArrayList<>();
|
||||
|
||||
for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
|
||||
Optional<Semester> semesterOpt = academicDateService.findSemester(date);
|
||||
if (semesterOpt.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Semester semester = semesterOpt.get();
|
||||
if (!academicDateService.isTheoryDay(group, semester, date)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<ScheduleRule> rules = rulesBySemester.computeIfAbsent(
|
||||
semester.getId(),
|
||||
semesterId -> scheduleRuleRepository.findByGroupIdAndSemesterId(groupId, semesterId)
|
||||
);
|
||||
|
||||
for (ScheduleRule rule : sortRules(rules)) {
|
||||
renderRuleForDate(rule, date, group, null, result);
|
||||
}
|
||||
}
|
||||
|
||||
return sortRenderedLessons(result);
|
||||
}
|
||||
|
||||
public List<RenderedLessonDto> buildScheduleForTeacher(Long teacherId, LocalDate startDate, LocalDate endDate) {
|
||||
validateRange(startDate, endDate);
|
||||
if (!userRepository.existsById(teacherId)) {
|
||||
throw new IllegalArgumentException("Преподаватель не найден");
|
||||
}
|
||||
|
||||
Map<Long, List<ScheduleRule>> rulesBySemester = new HashMap<>();
|
||||
List<RenderedLessonDto> result = new ArrayList<>();
|
||||
|
||||
for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
|
||||
Optional<Semester> semesterOpt = academicDateService.findSemester(date);
|
||||
if (semesterOpt.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Semester semester = semesterOpt.get();
|
||||
List<ScheduleRule> rules = rulesBySemester.computeIfAbsent(
|
||||
semester.getId(),
|
||||
semesterId -> scheduleRuleRepository.findByTeacherIdAndSemesterId(teacherId, semesterId)
|
||||
);
|
||||
|
||||
for (ScheduleRule rule : sortRules(rules)) {
|
||||
renderRuleForDate(rule, date, null, teacherId, result);
|
||||
}
|
||||
}
|
||||
|
||||
return sortRenderedLessons(result);
|
||||
}
|
||||
|
||||
public void clearCache() {
|
||||
consumedHoursCache.clear();
|
||||
}
|
||||
|
||||
private void validateRange(LocalDate startDate, LocalDate endDate) {
|
||||
if (startDate == null || endDate == null) {
|
||||
throw new IllegalArgumentException("Дата начала и дата окончания обязательны");
|
||||
}
|
||||
if (endDate.isBefore(startDate)) {
|
||||
throw new IllegalArgumentException("Дата окончания не может быть раньше даты начала");
|
||||
}
|
||||
if (java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate) > MAX_RANGE_DAYS) {
|
||||
throw new IllegalArgumentException("Диапазон расписания не может превышать 120 дней");
|
||||
}
|
||||
}
|
||||
|
||||
private List<ScheduleRule> sortRules(List<ScheduleRule> rules) {
|
||||
return rules.stream()
|
||||
.sorted(Comparator
|
||||
.comparing((ScheduleRule rule) -> rule.getSubject().getName(), Comparator.nullsLast(String::compareTo))
|
||||
.thenComparing(ScheduleRule::getId))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<RenderedLessonDto> sortRenderedLessons(List<RenderedLessonDto> lessons) {
|
||||
return lessons.stream()
|
||||
.sorted(Comparator
|
||||
.comparing(RenderedLessonDto::date)
|
||||
.thenComparing(RenderedLessonDto::timeSlotOrder, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(RenderedLessonDto::subjectName, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private void renderRuleForDate(ScheduleRule rule,
|
||||
LocalDate date,
|
||||
StudentGroup targetGroup,
|
||||
Long targetTeacherId,
|
||||
List<RenderedLessonDto> result) {
|
||||
if (rule.getActiveFromDate() != null && date.isBefore(rule.getActiveFromDate())) {
|
||||
return;
|
||||
}
|
||||
if (rule.getTotalAcademicHours() == null || rule.getTotalAcademicHours() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Semester> semesterOpt = academicDateService.findSemester(date);
|
||||
if (semesterOpt.isEmpty() || !Objects.equals(semesterOpt.get().getId(), rule.getSemester().getId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Semester semester = semesterOpt.get();
|
||||
if (academicDateService.isHoliday(semester, date)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<StudentGroup> eligibleGroups = eligibleGroups(rule, targetGroup, semester, date);
|
||||
if (eligibleGroups.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ScheduleParity dateParity = academicDateService.getParity(semester, date);
|
||||
int dayOfWeek = date.getDayOfWeek().getValue();
|
||||
List<ScheduleRuleSlot> slots = rule.getSlots().stream()
|
||||
.filter(slot -> Objects.equals(slot.getDayOfWeek(), dayOfWeek))
|
||||
.filter(slot -> slot.getParity() == ScheduleParity.BOTH || slot.getParity() == dateParity)
|
||||
.filter(slot -> targetTeacherId == null || Objects.equals(slot.getTeacher().getId(), targetTeacherId))
|
||||
.sorted(Comparator
|
||||
.comparing((ScheduleRuleSlot slot) -> slot.getTimeSlot().getOrderNumber(), Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(ScheduleRuleSlot::getId))
|
||||
.toList();
|
||||
|
||||
if (slots.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int consumedHours = calculateConsumedHoursBeforeDate(rule, targetGroup, date);
|
||||
int weekNumber = academicDateService.getWeekNumber(semester, date);
|
||||
|
||||
for (ScheduleRuleSlot slot : slots) {
|
||||
if (consumedHours >= rule.getTotalAcademicHours()) {
|
||||
break;
|
||||
}
|
||||
int remainingAfterLesson = Math.max(0, rule.getTotalAcademicHours() - consumedHours - ACADEMIC_HOURS_PER_SLOT);
|
||||
result.add(toRenderedLesson(rule, slot, date, weekNumber, dateParity, eligibleGroups, consumedHours, remainingAfterLesson));
|
||||
consumedHours += ACADEMIC_HOURS_PER_SLOT;
|
||||
}
|
||||
}
|
||||
|
||||
private List<StudentGroup> eligibleGroups(ScheduleRule rule, StudentGroup targetGroup, Semester semester, LocalDate date) {
|
||||
if (targetGroup != null) {
|
||||
return academicDateService.isTheoryDay(targetGroup, semester, date) ? List.of(targetGroup) : List.of();
|
||||
}
|
||||
|
||||
return rule.getGroups().stream()
|
||||
.filter(group -> academicDateService.isTheoryDay(group, semester, date))
|
||||
.sorted(Comparator.comparing(StudentGroup::getName))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private int calculateConsumedHoursBeforeDate(ScheduleRule rule, StudentGroup targetGroup, LocalDate targetDate) {
|
||||
Long groupId = targetGroup == null ? null : targetGroup.getId();
|
||||
String cacheKey = rule.getId() + ":" + (groupId == null ? "all" : groupId) + ":" + targetDate;
|
||||
return consumedHoursCache.computeIfAbsent(cacheKey, ignored -> calculateConsumedHours(rule, targetGroup, targetDate));
|
||||
}
|
||||
|
||||
private int calculateConsumedHours(ScheduleRule rule, StudentGroup targetGroup, LocalDate targetDate) {
|
||||
LocalDate start = rule.getActiveFromDate() == null
|
||||
? rule.getSemester().getStartDate()
|
||||
: rule.getActiveFromDate();
|
||||
if (start.isBefore(rule.getSemester().getStartDate())) {
|
||||
start = rule.getSemester().getStartDate();
|
||||
}
|
||||
|
||||
int consumed = 0;
|
||||
for (LocalDate date = start; date.isBefore(targetDate); date = date.plusDays(1)) {
|
||||
Semester semester = rule.getSemester();
|
||||
if (date.isBefore(semester.getStartDate()) || date.isAfter(semester.getEndDate())) {
|
||||
continue;
|
||||
}
|
||||
if (academicDateService.isHoliday(semester, date)) {
|
||||
continue;
|
||||
}
|
||||
if (eligibleGroups(rule, targetGroup, semester, date).isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int dayOfWeek = date.getDayOfWeek().getValue();
|
||||
ScheduleParity dateParity = academicDateService.getParity(semester, date);
|
||||
long activeSlots = rule.getSlots().stream()
|
||||
.filter(slot -> Objects.equals(slot.getDayOfWeek(), dayOfWeek))
|
||||
.filter(slot -> slot.getParity() == ScheduleParity.BOTH || slot.getParity() == dateParity)
|
||||
.count();
|
||||
consumed += (int) activeSlots * ACADEMIC_HOURS_PER_SLOT;
|
||||
if (consumed >= rule.getTotalAcademicHours()) {
|
||||
return consumed;
|
||||
}
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
|
||||
private RenderedLessonDto toRenderedLesson(ScheduleRule rule,
|
||||
ScheduleRuleSlot slot,
|
||||
LocalDate date,
|
||||
int weekNumber,
|
||||
ScheduleParity dateParity,
|
||||
List<StudentGroup> groups,
|
||||
int consumedBeforeLesson,
|
||||
int remainingAfterLesson) {
|
||||
TimeSlot timeSlot = slot.getTimeSlot();
|
||||
List<StudentGroup> sortedGroups = groups.stream()
|
||||
.sorted(Comparator.comparing(StudentGroup::getName))
|
||||
.toList();
|
||||
|
||||
return new RenderedLessonDto(
|
||||
rule.getId(),
|
||||
slot.getId(),
|
||||
date,
|
||||
date.getDayOfWeek().getValue(),
|
||||
dayName(date.getDayOfWeek()),
|
||||
weekNumber,
|
||||
dateParity,
|
||||
timeSlot.getId(),
|
||||
timeSlot.getOrderNumber(),
|
||||
timeSlot.getStartTime(),
|
||||
timeSlot.getEndTime(),
|
||||
rule.getSubject().getId(),
|
||||
rule.getSubject().getName(),
|
||||
slot.getTeacher().getId(),
|
||||
displayUserName(slot.getTeacher()),
|
||||
slot.getClassroom().getId(),
|
||||
slot.getClassroom().getName(),
|
||||
slot.getLessonType().getId(),
|
||||
slot.getLessonType().getLessonType(),
|
||||
slot.getLessonFormat(),
|
||||
slot.getSubgroupId(),
|
||||
sortedGroups.stream().map(StudentGroup::getId).toList(),
|
||||
sortedGroups.stream().map(StudentGroup::getName).collect(Collectors.toList()),
|
||||
AcademicActivityType.THEORY,
|
||||
rule.getTotalAcademicHours(),
|
||||
consumedBeforeLesson,
|
||||
remainingAfterLesson
|
||||
);
|
||||
}
|
||||
|
||||
public static String dayName(DayOfWeek dayOfWeek) {
|
||||
String value = dayOfWeek.getDisplayName(TextStyle.FULL, Locale.forLanguageTag("ru-RU"));
|
||||
return value.substring(0, 1).toUpperCase(Locale.forLanguageTag("ru-RU")) + value.substring(1);
|
||||
}
|
||||
|
||||
public static String displayUserName(User user) {
|
||||
if (user.getFullName() != null && !user.getFullName().isBlank()) {
|
||||
return user.getFullName();
|
||||
}
|
||||
return user.getUsername();
|
||||
}
|
||||
}
|
||||
@@ -210,7 +210,227 @@ CREATE TABLE IF NOT EXISTS teacher_lesson_types (
|
||||
);
|
||||
|
||||
-- ==========================================
|
||||
-- Основная таблица Расписания (Lessons)
|
||||
-- Динамическое расписание: календарь, слоты и правила
|
||||
-- ==========================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS time_slots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
order_number INT NOT NULL,
|
||||
start_time TIME NOT NULL,
|
||||
end_time TIME NOT NULL,
|
||||
duration_minutes INT NOT NULL,
|
||||
CONSTRAINT chk_time_slots_order_positive CHECK (order_number > 0),
|
||||
CONSTRAINT chk_time_slots_time_range CHECK (start_time < end_time),
|
||||
CONSTRAINT chk_time_slots_duration_positive CHECK (duration_minutes > 0),
|
||||
CONSTRAINT uq_time_slots_order UNIQUE (order_number),
|
||||
CONSTRAINT uq_time_slots_range UNIQUE (start_time, end_time)
|
||||
);
|
||||
|
||||
INSERT INTO time_slots (order_number, start_time, end_time, duration_minutes) VALUES
|
||||
(1, '08:00', '09:30', 90),
|
||||
(2, '09:40', '11:10', 90),
|
||||
(3, '11:40', '13:10', 90),
|
||||
(4, '13:20', '14:50', 90),
|
||||
(5, '15:00', '16:30', 90),
|
||||
(6, '16:50', '18:20', 90),
|
||||
(7, '18:30', '20:00', 90)
|
||||
ON CONFLICT (start_time, end_time) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_years (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
title VARCHAR(20) NOT NULL UNIQUE,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
CONSTRAINT chk_academic_years_dates CHECK (start_date <= end_date)
|
||||
);
|
||||
|
||||
INSERT INTO academic_years (title, start_date, end_date) VALUES
|
||||
('2024-2025', '2024-09-01', '2025-06-30'),
|
||||
('2025-2026', '2025-09-01', '2026-06-30')
|
||||
ON CONFLICT (title) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS semesters (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
|
||||
semester_type VARCHAR(20) NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
CONSTRAINT chk_semesters_type CHECK (semester_type IN ('autumn', 'spring')),
|
||||
CONSTRAINT chk_semesters_dates CHECK (start_date <= end_date),
|
||||
CONSTRAINT uq_semesters_year_type UNIQUE (academic_year_id, semester_type)
|
||||
);
|
||||
|
||||
INSERT INTO semesters (academic_year_id, semester_type, start_date, end_date)
|
||||
SELECT id, 'autumn', start_date, MAKE_DATE(SPLIT_PART(title, '-', 2)::INT, 1, 31)
|
||||
FROM academic_years
|
||||
ON CONFLICT (academic_year_id, semester_type) DO NOTHING;
|
||||
|
||||
INSERT INTO semesters (academic_year_id, semester_type, start_date, end_date)
|
||||
SELECT id, 'spring', MAKE_DATE(SPLIT_PART(title, '-', 2)::INT, 2, 1), end_date
|
||||
FROM academic_years
|
||||
ON CONFLICT (academic_year_id, semester_type) DO NOTHING;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_semesters_dates ON semesters(start_date, end_date);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS holidays (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
date DATE NOT NULL,
|
||||
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
|
||||
description VARCHAR(255),
|
||||
CONSTRAINT uq_holidays_year_date UNIQUE (academic_year_id, date)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_holidays_date ON holidays(date);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS academic_calendar_matrix (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
semester_id BIGINT NOT NULL REFERENCES semesters(id) ON DELETE CASCADE,
|
||||
course_number INT NOT NULL,
|
||||
specialty_id BIGINT NOT NULL REFERENCES specialties(id),
|
||||
week_number INT NOT NULL,
|
||||
activity_type VARCHAR(20) NOT NULL,
|
||||
CONSTRAINT chk_calendar_course_positive CHECK (course_number > 0),
|
||||
CONSTRAINT chk_calendar_week_positive CHECK (week_number > 0),
|
||||
CONSTRAINT chk_calendar_activity_type CHECK (activity_type IN ('THEORY', 'EXAM', 'VACATION', 'PRACTICE')),
|
||||
CONSTRAINT uq_calendar_matrix UNIQUE (semester_id, course_number, specialty_id, week_number)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_calendar_lookup
|
||||
ON academic_calendar_matrix(semester_id, course_number, specialty_id, week_number);
|
||||
|
||||
INSERT INTO academic_calendar_matrix (semester_id, course_number, specialty_id, week_number, activity_type)
|
||||
SELECT
|
||||
s.id,
|
||||
SPLIT_PART(ay.title, '-', 1)::INT - sg.year_start_study + 1 AS course_number,
|
||||
sg.specialty_code AS specialty_id,
|
||||
weeks.week_number,
|
||||
'THEORY'
|
||||
FROM semesters s
|
||||
JOIN academic_years ay ON ay.id = s.academic_year_id
|
||||
CROSS JOIN student_groups sg
|
||||
CROSS JOIN GENERATE_SERIES(1, 22) AS weeks(week_number)
|
||||
WHERE SPLIT_PART(ay.title, '-', 1)::INT - sg.year_start_study + 1 > 0
|
||||
ON CONFLICT (semester_id, course_number, specialty_id, week_number) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schedule_rules (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
subject_id BIGINT NOT NULL REFERENCES subjects(id),
|
||||
semester_id BIGINT NOT NULL REFERENCES semesters(id) ON DELETE CASCADE,
|
||||
active_from_date DATE NOT NULL,
|
||||
total_academic_hours INT NOT NULL,
|
||||
CONSTRAINT chk_schedule_rules_hours_positive CHECK (total_academic_hours > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_rules_semester ON schedule_rules(semester_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_rules_subject ON schedule_rules(subject_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schedule_rule_groups (
|
||||
schedule_rule_id BIGINT NOT NULL REFERENCES schedule_rules(id) ON DELETE CASCADE,
|
||||
group_id BIGINT NOT NULL REFERENCES student_groups(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (schedule_rule_id, group_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_rule_groups_group ON schedule_rule_groups(group_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schedule_rule_slots (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
schedule_rule_id BIGINT NOT NULL REFERENCES schedule_rules(id) ON DELETE CASCADE,
|
||||
day_of_week INT NOT NULL,
|
||||
parity VARCHAR(10) NOT NULL,
|
||||
time_slot_id BIGINT NOT NULL REFERENCES time_slots(id),
|
||||
subgroup_id BIGINT NULL REFERENCES subgroups(id) ON DELETE SET NULL,
|
||||
teacher_id BIGINT NOT NULL REFERENCES users(id),
|
||||
classroom_id BIGINT NOT NULL REFERENCES classrooms(id),
|
||||
lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id),
|
||||
lesson_format VARCHAR(30) NOT NULL,
|
||||
CONSTRAINT chk_schedule_rule_slots_day CHECK (day_of_week BETWEEN 1 AND 7),
|
||||
CONSTRAINT chk_schedule_rule_slots_parity CHECK (parity IN ('BOTH', 'EVEN', 'ODD')),
|
||||
CONSTRAINT chk_schedule_rule_slots_format CHECK (lesson_format IN ('Очно', 'Онлайн'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_rule ON schedule_rule_slots(schedule_rule_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_teacher ON schedule_rule_slots(teacher_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_classroom ON schedule_rule_slots(classroom_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_time ON schedule_rule_slots(day_of_week, parity, time_slot_id);
|
||||
|
||||
CREATE TEMP TABLE tmp_default_rules AS
|
||||
SELECT
|
||||
NEXTVAL(PG_GET_SERIAL_SEQUENCE('schedule_rules', 'id')) AS schedule_rule_id,
|
||||
rule_key,
|
||||
subject_id,
|
||||
semester_id,
|
||||
active_from_date,
|
||||
total_academic_hours
|
||||
FROM (
|
||||
SELECT
|
||||
data.rule_key,
|
||||
subj.id AS subject_id,
|
||||
sem.id AS semester_id,
|
||||
data.active_from_date,
|
||||
data.total_academic_hours
|
||||
FROM (VALUES
|
||||
('stream_math', 'Высшая математика', '2025-2026', 'spring', DATE '2026-02-02', 72),
|
||||
('ivt_informatics_lab', 'Информатика', '2025-2026', 'spring', DATE '2026-02-02', 72),
|
||||
('ivt_databases_practice', 'Базы данных', '2025-2026', 'spring', DATE '2026-02-02', 72),
|
||||
('ib_philosophy', 'Философия', '2025-2026', 'spring', DATE '2026-02-02', 54)
|
||||
) AS data(rule_key, subject_name, year_title, semester_type, active_from_date, total_academic_hours)
|
||||
JOIN subjects subj ON subj.name = data.subject_name
|
||||
JOIN academic_years ay ON ay.title = data.year_title
|
||||
JOIN semesters sem ON sem.academic_year_id = ay.id AND sem.semester_type = data.semester_type
|
||||
) prepared_rules;
|
||||
|
||||
INSERT INTO schedule_rules (id, subject_id, semester_id, active_from_date, total_academic_hours)
|
||||
SELECT schedule_rule_id, subject_id, semester_id, active_from_date, total_academic_hours
|
||||
FROM tmp_default_rules;
|
||||
|
||||
INSERT INTO schedule_rule_groups (schedule_rule_id, group_id)
|
||||
SELECT
|
||||
rules.schedule_rule_id,
|
||||
sg.id
|
||||
FROM (VALUES
|
||||
('stream_math', 'ИВТ-21-1'),
|
||||
('stream_math', 'ИБ-41м'),
|
||||
('ivt_informatics_lab', 'ИВТ-21-1'),
|
||||
('ivt_databases_practice', 'ИВТ-21-1'),
|
||||
('ib_philosophy', 'ИБ-41м')
|
||||
) AS data(rule_key, group_name)
|
||||
JOIN tmp_default_rules rules ON rules.rule_key = data.rule_key
|
||||
JOIN student_groups sg ON sg.name = data.group_name
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
INSERT INTO schedule_rule_slots (
|
||||
schedule_rule_id,
|
||||
day_of_week,
|
||||
parity,
|
||||
time_slot_id,
|
||||
teacher_id,
|
||||
classroom_id,
|
||||
lesson_type_id,
|
||||
lesson_format
|
||||
)
|
||||
SELECT
|
||||
rules.schedule_rule_id,
|
||||
data.day_of_week,
|
||||
data.parity,
|
||||
ts.id,
|
||||
teacher.id,
|
||||
classroom.id,
|
||||
lt.id,
|
||||
data.lesson_format
|
||||
FROM (VALUES
|
||||
('stream_math', 1, 'BOTH', 3, 'Тестовый преподаватель', '101 Ленинская', 'Лекция', 'Очно'),
|
||||
('ivt_informatics_lab', 3, 'BOTH', 1, 'Тестовый преподаватель', '202 IT Lab', 'Лабораторная работа', 'Очно'),
|
||||
('ivt_databases_practice', 5, 'EVEN', 5, 'Тестовый преподаватель', '303 Обычная', 'Практика', 'Онлайн'),
|
||||
('ib_philosophy', 2, 'ODD', 2, 'Тестовый преподаватель', '101 Ленинская', 'Лекция', 'Онлайн')
|
||||
) AS data(rule_key, day_of_week, parity, time_slot_order, teacher_username, classroom_name, lesson_type_name, lesson_format)
|
||||
JOIN tmp_default_rules rules ON rules.rule_key = data.rule_key
|
||||
JOIN time_slots ts ON ts.order_number = data.time_slot_order
|
||||
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,
|
||||
@@ -225,17 +445,6 @@ CREATE TABLE IF NOT EXISTS lessons (
|
||||
time VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO lessons (teacher_id, group_id, subject_id, lesson_format, type_lesson, classroom_id, day, week, time) VALUES
|
||||
(2, 1, 1, 'Очно', 'Лекция', 1, 'Понедельник', 'Верхняя', '11:40 - 13:10'),
|
||||
(1, 1, 2, 'Онлайн', 'Практическая работа', 2, 'Вторник', 'Нижняя', '15:00 - 16:30'),
|
||||
(2, 1, 3, 'Очно', 'Лабораторная работа', 3, 'Среда', 'Верхняя', '8:00 - 9:30'),
|
||||
(1, 1, 4, 'Онлайн', 'Лекция', 1, 'Четверг', 'Нижняя', '11:40 - 13:10'),
|
||||
(2, 1, 5, 'Очно', 'Практическая работа', 2, 'Пятница', 'Верхняя', '15:00 - 16:30'),
|
||||
(1, 1, 3, 'Онлайн', 'Лабораторная работа', 3, 'Суббота', 'Нижняя', '8:00 - 9:30');
|
||||
|
||||
-- ===============================
|
||||
-- Создание таблицы данных расписания (schedule_data)
|
||||
-- ===============================
|
||||
CREATE TABLE IF NOT EXISTS schedule_data (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
department_id BIGINT NOT NULL REFERENCES departments(id),
|
||||
@@ -249,18 +458,6 @@ CREATE TABLE IF NOT EXISTS schedule_data (
|
||||
period VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO schedule_data (department_id, group_id, subjects_id, lesson_type_id, number_of_hours, is_division, teacher_id, semester_type, period)
|
||||
VALUES (1, 1, 1, 3, 2, true, 1, 'autumn', '2024-2025'),
|
||||
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
||||
(3, 1, 2, 1, 3, true, 1, 'autumn', '2023-2024'),
|
||||
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
||||
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
||||
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
|
||||
(1, 1, 1, 1, 2, true, 2, 'autumn', '2024-2025'),
|
||||
(1, 2, 2, 3, 4, false, 2, 'autumn', '2024-2025'),
|
||||
(1, 1, 4, 2, 1, false, 1, 'autumn', '2024-2025'),
|
||||
(1, 2, 5, 1, 7, true, 1, 'autumn', '2024-2025');
|
||||
|
||||
-- ==========================================
|
||||
-- Функция обновления timestamp
|
||||
-- ==========================================
|
||||
@@ -281,10 +478,18 @@ CREATE TRIGGER update_users_updated_at
|
||||
-- Комментарии к таблицам и полям (для документации)
|
||||
-- ==========================================
|
||||
COMMENT ON TABLE users IS 'Пользователи системы (студенты, преподаватели, администраторы)';
|
||||
COMMENT ON TABLE lessons 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 schedule_data IS 'Временная таблица старой модели плановой нагрузки до удаления старого кода';
|
||||
COMMENT ON TABLE time_slots IS 'Настраиваемая сетка временных слотов занятий';
|
||||
COMMENT ON TABLE academic_years IS 'Учебные годы';
|
||||
COMMENT ON TABLE semesters IS 'Семестры учебного года';
|
||||
COMMENT ON TABLE holidays IS 'Праздники и неучебные даты';
|
||||
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 'Идентификатор предмета';
|
||||
@@ -324,6 +529,8 @@ COMMENT ON COLUMN student_groups.name IS 'Название группы';
|
||||
COMMENT ON COLUMN student_groups.group_size IS 'Количество студентов';
|
||||
COMMENT ON COLUMN student_groups.education_form_id IS 'ID формы обучения, к которой относится группа';
|
||||
COMMENT ON COLUMN student_groups.department_id IS 'ID кафедры';
|
||||
COMMENT ON COLUMN student_groups.specialty_code IS 'ID специальности';
|
||||
COMMENT ON COLUMN student_groups.year_start_study IS 'Год начала обучения';
|
||||
COMMENT ON COLUMN student_groups.created_at IS 'Дата и время создания';
|
||||
|
||||
COMMENT ON COLUMN subgroups.id IS 'ID подгруппы';
|
||||
@@ -388,4 +595,4 @@ COMMENT ON COLUMN specialties.specialty_code IS 'Код специальност
|
||||
|
||||
COMMENT ON COLUMN teacher_lesson_types.user_id IS 'ID преподавателя';
|
||||
COMMENT ON COLUMN teacher_lesson_types.subject_id IS 'ID предмета';
|
||||
COMMENT ON COLUMN teacher_lesson_types.lesson_type_id IS 'ID типа занятия';
|
||||
COMMENT ON COLUMN teacher_lesson_types.lesson_type_id IS 'ID типа занятия';
|
||||
|
||||
Reference in New Issue
Block a user