This commit is contained in:
@@ -2,7 +2,10 @@ package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.RenderedLessonDto;
|
||||
import com.magistr.app.dto.SemesterDto;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.Semester;
|
||||
import com.magistr.app.repository.SemesterRepository;
|
||||
import com.magistr.app.service.ScheduleQueryService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -11,6 +14,8 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@@ -21,9 +26,20 @@ public class ScheduleController {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScheduleController.class);
|
||||
|
||||
private final ScheduleQueryService scheduleQueryService;
|
||||
private final SemesterRepository semesterRepository;
|
||||
|
||||
public ScheduleController(ScheduleQueryService scheduleQueryService) {
|
||||
public ScheduleController(ScheduleQueryService scheduleQueryService,
|
||||
SemesterRepository semesterRepository) {
|
||||
this.scheduleQueryService = scheduleQueryService;
|
||||
this.semesterRepository = semesterRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/semesters")
|
||||
public List<SemesterDto> getSemesters() {
|
||||
return semesterRepository.findAll().stream()
|
||||
.sorted(Comparator.comparing(Semester::getStartDate).reversed())
|
||||
.map(this::toSemesterDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -54,4 +70,15 @@ public class ScheduleController {
|
||||
endDate
|
||||
));
|
||||
}
|
||||
|
||||
private SemesterDto toSemesterDto(Semester semester) {
|
||||
return new SemesterDto(
|
||||
semester.getId(),
|
||||
semester.getAcademicYear().getId(),
|
||||
semester.getAcademicYear().getTitle(),
|
||||
semester.getSemesterType(),
|
||||
semester.getStartDate(),
|
||||
semester.getEndDate()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.ScheduleOverrideDto;
|
||||
import com.magistr.app.dto.ScheduleOverrideAvailabilityDto;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.service.ScheduleOverrideService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -9,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/edu-office/schedule/overrides")
|
||||
@@ -22,8 +24,19 @@ public class ScheduleOverrideController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<ScheduleOverrideDto> getAll() {
|
||||
return scheduleOverrideService.getAll();
|
||||
public List<ScheduleOverrideDto> getAll(
|
||||
@RequestParam(required = false) LocalDate startDate,
|
||||
@RequestParam(required = false) LocalDate endDate
|
||||
) {
|
||||
return scheduleOverrideService.getAll(startDate, endDate);
|
||||
}
|
||||
|
||||
@GetMapping("/availability")
|
||||
public ScheduleOverrideAvailabilityDto getAvailability(
|
||||
@RequestParam Long baseRuleSlotId,
|
||||
@RequestParam LocalDate lessonDate
|
||||
) {
|
||||
return scheduleOverrideService.getAvailability(baseRuleSlotId, lessonDate);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.magistr.app.repository.TimeSlotDateAssignmentRepository;
|
||||
import com.magistr.app.repository.TimeSlotRepository;
|
||||
import com.magistr.app.repository.TimeSlotScopeRepository;
|
||||
import com.magistr.app.service.TimeSlotService;
|
||||
import com.magistr.app.service.EffectiveTimeSlotService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -35,15 +36,18 @@ public class TimeSlotAdminController {
|
||||
private final TimeSlotScopeRepository timeSlotScopeRepository;
|
||||
private final TimeSlotDateAssignmentRepository dateAssignmentRepository;
|
||||
private final TimeSlotService timeSlotService;
|
||||
private final EffectiveTimeSlotService effectiveTimeSlotService;
|
||||
|
||||
public TimeSlotAdminController(TimeSlotRepository timeSlotRepository,
|
||||
TimeSlotScopeRepository timeSlotScopeRepository,
|
||||
TimeSlotDateAssignmentRepository dateAssignmentRepository,
|
||||
TimeSlotService timeSlotService) {
|
||||
TimeSlotService timeSlotService,
|
||||
EffectiveTimeSlotService effectiveTimeSlotService) {
|
||||
this.timeSlotRepository = timeSlotRepository;
|
||||
this.timeSlotScopeRepository = timeSlotScopeRepository;
|
||||
this.dateAssignmentRepository = dateAssignmentRepository;
|
||||
this.timeSlotService = timeSlotService;
|
||||
this.effectiveTimeSlotService = effectiveTimeSlotService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -55,7 +59,7 @@ public class TimeSlotAdminController {
|
||||
|
||||
@GetMapping("/effective")
|
||||
public ResponseEntity<?> getEffective(@RequestParam LocalDate date) {
|
||||
return ResponseEntity.ok(effectiveSlots(date).stream().map(this::toDto).toList());
|
||||
return ResponseEntity.ok(effectiveTimeSlotService.findEffective(date).stream().map(this::toDto).toList());
|
||||
}
|
||||
|
||||
@GetMapping("/scopes")
|
||||
@@ -167,35 +171,6 @@ public class TimeSlotAdminController {
|
||||
return ResponseEntity.ok(Map.of("message", "Временной слот удалён"));
|
||||
}
|
||||
|
||||
private List<TimeSlot> effectiveSlots(LocalDate date) {
|
||||
TimeSlotScope defaultScope = defaultScope();
|
||||
TimeSlotScope effectiveScope = effectiveScope(date);
|
||||
List<TimeSlot> defaultSlots = timeSlotRepository.findAllByTimeSlotScopeIdOrderByOrderNumberAsc(defaultScope.getId());
|
||||
if (effectiveScope.getId().equals(defaultScope.getId())) {
|
||||
return defaultSlots;
|
||||
}
|
||||
|
||||
Map<Integer, TimeSlot> slotsByOrder = new LinkedHashMap<>();
|
||||
defaultSlots.forEach(slot -> slotsByOrder.put(slot.getOrderNumber(), slot));
|
||||
timeSlotRepository.findAllByTimeSlotScopeIdOrderByOrderNumberAsc(effectiveScope.getId())
|
||||
.forEach(slot -> slotsByOrder.put(slot.getOrderNumber(), slot));
|
||||
return new ArrayList<>(slotsByOrder.values());
|
||||
}
|
||||
|
||||
private TimeSlotScope effectiveScope(LocalDate date) {
|
||||
return dateAssignmentRepository.findByDate(date)
|
||||
.map(TimeSlotDateAssignment::getTimeSlotScope)
|
||||
.or(() -> timeSlotScopeRepository.findByApplyModeAndDayOfWeek(
|
||||
APPLY_MODE_WEEKDAY,
|
||||
date.getDayOfWeek().getValue()))
|
||||
.orElseGet(this::defaultScope);
|
||||
}
|
||||
|
||||
private TimeSlotScope defaultScope() {
|
||||
return timeSlotScopeRepository.findFirstByApplyModeOrderByDisplayOrderAsc(APPLY_MODE_DEFAULT)
|
||||
.orElseThrow(() -> new IllegalStateException("Базовая сетка времени не настроена"));
|
||||
}
|
||||
|
||||
private int nextManualDisplayOrder() {
|
||||
return timeSlotScopeRepository.findAllByOrderByDisplayOrderAscNameAsc().stream()
|
||||
.map(TimeSlotScope::getDisplayOrder)
|
||||
|
||||
@@ -37,6 +37,79 @@ public record RenderedLessonDto(
|
||||
Integer lessonTypeAcademicHours,
|
||||
Integer consumedLessonTypeAcademicHoursBeforeLesson,
|
||||
Integer remainingLessonTypeAcademicHoursAfterLesson,
|
||||
com.magistr.app.model.ScheduleParity ruleParity
|
||||
com.magistr.app.model.ScheduleParity ruleParity,
|
||||
Long scheduleOverrideId,
|
||||
String overrideAction,
|
||||
LocalDate originalLessonDate
|
||||
) {
|
||||
public RenderedLessonDto(
|
||||
Long scheduleRuleId,
|
||||
Long scheduleRuleSlotId,
|
||||
LocalDate date,
|
||||
Integer dayOfWeek,
|
||||
String dayName,
|
||||
Integer weekNumber,
|
||||
ScheduleParity parity,
|
||||
Long timeSlotId,
|
||||
Integer timeSlotOrder,
|
||||
LocalTime startTime,
|
||||
LocalTime endTime,
|
||||
Long subjectId,
|
||||
String subjectName,
|
||||
Long teacherId,
|
||||
String teacherName,
|
||||
Long classroomId,
|
||||
String classroomName,
|
||||
Long lessonTypeId,
|
||||
String lessonTypeName,
|
||||
String lessonFormat,
|
||||
Long subgroupId,
|
||||
String subgroupName,
|
||||
List<Long> subgroupIds,
|
||||
List<String> subgroupNames,
|
||||
List<Long> groupIds,
|
||||
List<String> groupNames,
|
||||
String activityType,
|
||||
Integer lessonTypeAcademicHours,
|
||||
Integer consumedLessonTypeAcademicHoursBeforeLesson,
|
||||
Integer remainingLessonTypeAcademicHoursAfterLesson,
|
||||
com.magistr.app.model.ScheduleParity ruleParity
|
||||
) {
|
||||
this(
|
||||
scheduleRuleId,
|
||||
scheduleRuleSlotId,
|
||||
date,
|
||||
dayOfWeek,
|
||||
dayName,
|
||||
weekNumber,
|
||||
parity,
|
||||
timeSlotId,
|
||||
timeSlotOrder,
|
||||
startTime,
|
||||
endTime,
|
||||
subjectId,
|
||||
subjectName,
|
||||
teacherId,
|
||||
teacherName,
|
||||
classroomId,
|
||||
classroomName,
|
||||
lessonTypeId,
|
||||
lessonTypeName,
|
||||
lessonFormat,
|
||||
subgroupId,
|
||||
subgroupName,
|
||||
subgroupIds,
|
||||
subgroupNames,
|
||||
groupIds,
|
||||
groupNames,
|
||||
activityType,
|
||||
lessonTypeAcademicHours,
|
||||
consumedLessonTypeAcademicHoursBeforeLesson,
|
||||
remainingLessonTypeAcademicHoursAfterLesson,
|
||||
ruleParity,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
public record ScheduleOverrideAvailabilityDto(
|
||||
Long semesterId,
|
||||
LocalDate semesterStartDate,
|
||||
LocalDate semesterEndDate,
|
||||
List<LocalDate> availableDates
|
||||
) {
|
||||
}
|
||||
@@ -2,11 +2,14 @@ package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalTime;
|
||||
import java.util.List;
|
||||
|
||||
public record ScheduleOverrideDto(
|
||||
Long id,
|
||||
Long baseRuleSlotId,
|
||||
LocalDate lessonDate,
|
||||
LocalDate targetLessonDate,
|
||||
String action,
|
||||
Long newTimeSlotId,
|
||||
Long newClassroomId,
|
||||
@@ -14,6 +17,109 @@ public record ScheduleOverrideDto(
|
||||
String newLessonFormat,
|
||||
String comment,
|
||||
Long createdBy,
|
||||
Instant createdAt
|
||||
Instant createdAt,
|
||||
Long semesterId,
|
||||
LocalDate semesterStartDate,
|
||||
LocalDate semesterEndDate,
|
||||
Long sourceTimeSlotId,
|
||||
Integer sourceTimeSlotOrder,
|
||||
LocalTime sourceStartTime,
|
||||
LocalTime sourceEndTime,
|
||||
Long sourceTeacherId,
|
||||
String sourceTeacherName,
|
||||
Long sourceClassroomId,
|
||||
String sourceClassroomName,
|
||||
String sourceLessonFormat,
|
||||
String subjectName,
|
||||
String lessonTypeName,
|
||||
List<String> groupNames
|
||||
) {
|
||||
public ScheduleOverrideDto(
|
||||
Long id,
|
||||
Long baseRuleSlotId,
|
||||
LocalDate lessonDate,
|
||||
LocalDate targetLessonDate,
|
||||
String action,
|
||||
Long newTimeSlotId,
|
||||
Long newClassroomId,
|
||||
Long newTeacherId,
|
||||
String newLessonFormat,
|
||||
String comment,
|
||||
Long createdBy,
|
||||
Instant createdAt
|
||||
) {
|
||||
this(
|
||||
id,
|
||||
baseRuleSlotId,
|
||||
lessonDate,
|
||||
targetLessonDate,
|
||||
action,
|
||||
newTimeSlotId,
|
||||
newClassroomId,
|
||||
newTeacherId,
|
||||
newLessonFormat,
|
||||
comment,
|
||||
createdBy,
|
||||
createdAt,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
|
||||
public ScheduleOverrideDto(
|
||||
Long id,
|
||||
Long baseRuleSlotId,
|
||||
LocalDate lessonDate,
|
||||
String action,
|
||||
Long newTimeSlotId,
|
||||
Long newClassroomId,
|
||||
Long newTeacherId,
|
||||
String newLessonFormat,
|
||||
String comment,
|
||||
Long createdBy,
|
||||
Instant createdAt
|
||||
) {
|
||||
this(
|
||||
id,
|
||||
baseRuleSlotId,
|
||||
lessonDate,
|
||||
null,
|
||||
action,
|
||||
newTimeSlotId,
|
||||
newClassroomId,
|
||||
newTeacherId,
|
||||
newLessonFormat,
|
||||
comment,
|
||||
createdBy,
|
||||
createdAt,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
List.of()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,9 @@ public class ScheduleOverride {
|
||||
@Column(name = "lesson_date", nullable = false)
|
||||
private LocalDate lessonDate;
|
||||
|
||||
@Column(name = "target_lesson_date")
|
||||
private LocalDate targetLessonDate;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String action;
|
||||
|
||||
@@ -67,6 +70,14 @@ public class ScheduleOverride {
|
||||
this.lessonDate = lessonDate;
|
||||
}
|
||||
|
||||
public LocalDate getTargetLessonDate() {
|
||||
return targetLessonDate;
|
||||
}
|
||||
|
||||
public void setTargetLessonDate(LocalDate targetLessonDate) {
|
||||
this.targetLessonDate = targetLessonDate;
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -17,10 +18,13 @@ public interface ScheduleOverrideRepository extends JpaRepository<ScheduleOverri
|
||||
select o
|
||||
from ScheduleOverride o
|
||||
left join fetch o.baseRuleSlot baseSlot
|
||||
left join fetch baseSlot.scheduleRule rule
|
||||
left join fetch rule.semester
|
||||
left join fetch o.newTimeSlot
|
||||
left join fetch o.newClassroom
|
||||
left join fetch o.newTeacher
|
||||
where o.lessonDate between :startDate and :endDate
|
||||
or o.targetLessonDate between :startDate and :endDate
|
||||
""")
|
||||
List<ScheduleOverride> findByLessonDateBetweenWithDetails(
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@@ -29,8 +33,62 @@ public interface ScheduleOverrideRepository extends JpaRepository<ScheduleOverri
|
||||
|
||||
Optional<ScheduleOverride> findByBaseRuleSlotIdAndLessonDate(Long baseRuleSlotId, LocalDate lessonDate);
|
||||
|
||||
@Query("select o.lessonDate from ScheduleOverride o where o.id = :id")
|
||||
Optional<LocalDate> findLessonDateById(@Param("id") Long id);
|
||||
@Query("""
|
||||
select distinct o
|
||||
from ScheduleOverride o
|
||||
join fetch o.baseRuleSlot baseSlot
|
||||
join fetch baseSlot.scheduleRule rule
|
||||
join fetch rule.subject
|
||||
left join fetch rule.groups
|
||||
join fetch rule.semester
|
||||
join fetch baseSlot.timeSlot
|
||||
join fetch baseSlot.teacher
|
||||
join fetch baseSlot.classroom
|
||||
join fetch baseSlot.lessonType
|
||||
left join fetch o.newTimeSlot
|
||||
left join fetch o.newClassroom
|
||||
left join fetch o.newTeacher
|
||||
order by o.lessonDate, o.id
|
||||
""")
|
||||
List<ScheduleOverride> findAllWithRegistryDetails();
|
||||
|
||||
@Query("""
|
||||
select distinct o
|
||||
from ScheduleOverride o
|
||||
join fetch o.baseRuleSlot baseSlot
|
||||
join fetch baseSlot.scheduleRule rule
|
||||
join fetch rule.subject
|
||||
left join fetch rule.groups
|
||||
join fetch rule.semester
|
||||
join fetch baseSlot.timeSlot
|
||||
join fetch baseSlot.teacher
|
||||
join fetch baseSlot.classroom
|
||||
join fetch baseSlot.lessonType
|
||||
left join fetch o.newTimeSlot
|
||||
left join fetch o.newClassroom
|
||||
left join fetch o.newTeacher
|
||||
where o.lessonDate between :startDate and :endDate
|
||||
or o.targetLessonDate between :startDate and :endDate
|
||||
order by o.lessonDate, o.id
|
||||
""")
|
||||
List<ScheduleOverride> findForRegistry(
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate
|
||||
);
|
||||
|
||||
@Query("""
|
||||
select o
|
||||
from ScheduleOverride o
|
||||
left join fetch o.baseRuleSlot baseSlot
|
||||
left join fetch baseSlot.scheduleRule rule
|
||||
left join fetch rule.semester
|
||||
left join fetch o.newTimeSlot
|
||||
left join fetch o.newClassroom
|
||||
left join fetch o.newTeacher
|
||||
where o.lessonDate in :dates
|
||||
or o.targetLessonDate in :dates
|
||||
""")
|
||||
List<ScheduleOverride> findAffectingDatesWithDetails(@Param("dates") Collection<LocalDate> dates);
|
||||
|
||||
@Lock(LockModeType.PESSIMISTIC_WRITE)
|
||||
@Query("select o from ScheduleOverride o where o.id = :id")
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package com.magistr.app.service;
|
||||
|
||||
import com.magistr.app.model.TimeSlot;
|
||||
import com.magistr.app.model.TimeSlotDateAssignment;
|
||||
import com.magistr.app.model.TimeSlotScope;
|
||||
import com.magistr.app.repository.TimeSlotDateAssignmentRepository;
|
||||
import com.magistr.app.repository.TimeSlotRepository;
|
||||
import com.magistr.app.repository.TimeSlotScopeRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
@Service
|
||||
public class EffectiveTimeSlotService {
|
||||
|
||||
private static final String APPLY_MODE_DEFAULT = "DEFAULT";
|
||||
private static final String APPLY_MODE_WEEKDAY = "WEEKDAY";
|
||||
|
||||
private final TimeSlotRepository timeSlotRepository;
|
||||
private final TimeSlotScopeRepository timeSlotScopeRepository;
|
||||
private final TimeSlotDateAssignmentRepository dateAssignmentRepository;
|
||||
|
||||
public EffectiveTimeSlotService(TimeSlotRepository timeSlotRepository,
|
||||
TimeSlotScopeRepository timeSlotScopeRepository,
|
||||
TimeSlotDateAssignmentRepository dateAssignmentRepository) {
|
||||
this.timeSlotRepository = timeSlotRepository;
|
||||
this.timeSlotScopeRepository = timeSlotScopeRepository;
|
||||
this.dateAssignmentRepository = dateAssignmentRepository;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<TimeSlot> findEffective(LocalDate date) {
|
||||
if (date == null) {
|
||||
throw new IllegalArgumentException("Дата временной сетки обязательна");
|
||||
}
|
||||
TimeSlotScope defaultScope = defaultScope();
|
||||
TimeSlotScope effectiveScope = effectiveScope(date, defaultScope);
|
||||
List<TimeSlot> defaultSlots = timeSlotRepository
|
||||
.findAllByTimeSlotScopeIdOrderByOrderNumberAsc(defaultScope.getId());
|
||||
if (Objects.equals(effectiveScope.getId(), defaultScope.getId())) {
|
||||
return defaultSlots;
|
||||
}
|
||||
|
||||
Map<Integer, TimeSlot> slotsByOrder = new LinkedHashMap<>();
|
||||
defaultSlots.forEach(slot -> slotsByOrder.put(slot.getOrderNumber(), slot));
|
||||
timeSlotRepository.findAllByTimeSlotScopeIdOrderByOrderNumberAsc(effectiveScope.getId())
|
||||
.forEach(slot -> slotsByOrder.put(slot.getOrderNumber(), slot));
|
||||
return new ArrayList<>(slotsByOrder.values());
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public boolean isEffectiveOnDate(Long timeSlotId, LocalDate date) {
|
||||
return timeSlotId != null && findEffective(date).stream()
|
||||
.anyMatch(slot -> Objects.equals(slot.getId(), timeSlotId));
|
||||
}
|
||||
|
||||
private TimeSlotScope effectiveScope(LocalDate date, TimeSlotScope defaultScope) {
|
||||
return dateAssignmentRepository.findByDate(date)
|
||||
.map(TimeSlotDateAssignment::getTimeSlotScope)
|
||||
.or(() -> timeSlotScopeRepository.findByApplyModeAndDayOfWeek(
|
||||
APPLY_MODE_WEEKDAY,
|
||||
date.getDayOfWeek().getValue()))
|
||||
.orElse(defaultScope);
|
||||
}
|
||||
|
||||
private TimeSlotScope defaultScope() {
|
||||
return timeSlotScopeRepository.findFirstByApplyModeOrderByDisplayOrderAsc(APPLY_MODE_DEFAULT)
|
||||
.orElseThrow(() -> new IllegalStateException("Базовая сетка времени не настроена"));
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.magistr.app.service;
|
||||
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.dto.RenderedLessonDto;
|
||||
import com.magistr.app.dto.ScheduleOverrideAvailabilityDto;
|
||||
import com.magistr.app.dto.ScheduleOverrideDto;
|
||||
import com.magistr.app.model.Classroom;
|
||||
import com.magistr.app.model.Role;
|
||||
@@ -17,9 +18,11 @@ import com.magistr.app.repository.TimeSlotRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
@@ -31,7 +34,9 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@@ -49,7 +54,10 @@ public class ScheduleOverrideService {
|
||||
private final ScheduleQueryService scheduleQueryService;
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
private final PostgreSqlAdvisoryLock advisoryLock;
|
||||
private final AcademicDateService academicDateService;
|
||||
private final EffectiveTimeSlotService effectiveTimeSlotService;
|
||||
|
||||
@Autowired
|
||||
public ScheduleOverrideService(ScheduleOverrideRepository scheduleOverrideRepository,
|
||||
ScheduleRuleSlotRepository scheduleRuleSlotRepository,
|
||||
TimeSlotRepository timeSlotRepository,
|
||||
@@ -58,7 +66,9 @@ public class ScheduleOverrideService {
|
||||
SubgroupRepository subgroupRepository,
|
||||
ScheduleQueryService scheduleQueryService,
|
||||
ScheduleGeneratorService scheduleGeneratorService,
|
||||
PostgreSqlAdvisoryLock advisoryLock) {
|
||||
PostgreSqlAdvisoryLock advisoryLock,
|
||||
AcademicDateService academicDateService,
|
||||
EffectiveTimeSlotService effectiveTimeSlotService) {
|
||||
this.scheduleOverrideRepository = scheduleOverrideRepository;
|
||||
this.scheduleRuleSlotRepository = scheduleRuleSlotRepository;
|
||||
this.timeSlotRepository = timeSlotRepository;
|
||||
@@ -68,42 +78,124 @@ public class ScheduleOverrideService {
|
||||
this.scheduleQueryService = scheduleQueryService;
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
this.advisoryLock = advisoryLock;
|
||||
this.academicDateService = academicDateService;
|
||||
this.effectiveTimeSlotService = effectiveTimeSlotService;
|
||||
}
|
||||
|
||||
ScheduleOverrideService(ScheduleOverrideRepository scheduleOverrideRepository,
|
||||
ScheduleRuleSlotRepository scheduleRuleSlotRepository,
|
||||
TimeSlotRepository timeSlotRepository,
|
||||
ClassroomRepository classroomRepository,
|
||||
UserRepository userRepository,
|
||||
SubgroupRepository subgroupRepository,
|
||||
ScheduleQueryService scheduleQueryService,
|
||||
ScheduleGeneratorService scheduleGeneratorService,
|
||||
PostgreSqlAdvisoryLock advisoryLock) {
|
||||
this(
|
||||
scheduleOverrideRepository,
|
||||
scheduleRuleSlotRepository,
|
||||
timeSlotRepository,
|
||||
classroomRepository,
|
||||
userRepository,
|
||||
subgroupRepository,
|
||||
scheduleQueryService,
|
||||
scheduleGeneratorService,
|
||||
advisoryLock,
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ScheduleOverrideDto> getAll(LocalDate startDate, LocalDate endDate) {
|
||||
if ((startDate == null) != (endDate == null)) {
|
||||
throw new IllegalArgumentException("Дата начала и дата окончания должны быть указаны вместе");
|
||||
}
|
||||
if (startDate != null) {
|
||||
validateRange(startDate, endDate);
|
||||
}
|
||||
List<ScheduleOverride> overrides = startDate == null
|
||||
? scheduleOverrideRepository.findAllWithRegistryDetails()
|
||||
: scheduleOverrideRepository.findForRegistry(startDate, endDate);
|
||||
return overrides.stream()
|
||||
.map(this::toDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ScheduleOverrideDto> getAll() {
|
||||
return scheduleOverrideRepository.findAll().stream()
|
||||
.map(this::toDto)
|
||||
return getAll(null, null);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public ScheduleOverrideAvailabilityDto getAvailability(Long baseRuleSlotId, LocalDate lessonDate) {
|
||||
if (baseRuleSlotId == null || lessonDate == null) {
|
||||
throw new IllegalArgumentException("Базовый слот и исходная дата пары обязательны");
|
||||
}
|
||||
ScheduleRuleSlot slot = scheduleRuleSlotRepository.findById(baseRuleSlotId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Базовый слот расписания не найден"));
|
||||
ensureSourceOccurrenceExists(baseRuleSlotId, lessonDate);
|
||||
var rule = slot.getScheduleRule();
|
||||
if (rule == null || rule.getSemester() == null) {
|
||||
throw new IllegalArgumentException("Для базового слота не найден семестр");
|
||||
}
|
||||
var semester = rule.getSemester();
|
||||
if (lessonDate.isBefore(semester.getStartDate()) || lessonDate.isAfter(semester.getEndDate())) {
|
||||
throw new IllegalArgumentException("Исходная дата пары находится вне семестра правила");
|
||||
}
|
||||
if (academicDateService == null) {
|
||||
throw new IllegalStateException("Сервис учебных дат не настроен");
|
||||
}
|
||||
List<com.magistr.app.model.StudentGroup> groups = rule.getGroups().stream()
|
||||
.sorted(Comparator.comparing(com.magistr.app.model.StudentGroup::getName))
|
||||
.toList();
|
||||
var snapshot = academicDateService.createScheduleSnapshot(
|
||||
groups,
|
||||
List.of(semester),
|
||||
semester.getEndDate()
|
||||
);
|
||||
List<LocalDate> availableDates = semester.getStartDate()
|
||||
.datesUntil(semester.getEndDate().plusDays(1))
|
||||
.filter(date -> rule.isActiveOn(date))
|
||||
.filter(date -> rule.getSubject().isActiveOn(date))
|
||||
.filter(date -> groups.stream().allMatch(group -> group.isActiveOn(date)
|
||||
&& snapshot.isTheoryDay(group, semester, date)))
|
||||
.toList();
|
||||
return new ScheduleOverrideAvailabilityDto(
|
||||
semester.getId(),
|
||||
semester.getStartDate(),
|
||||
semester.getEndDate(),
|
||||
availableDates
|
||||
);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ScheduleOverrideDto create(ScheduleOverrideDto request) {
|
||||
OverrideCommand command = validateAndNormalize(request);
|
||||
advisoryLock.lockScheduleOverrideDate(command.lessonDate());
|
||||
lockDatesInStableOrder(command.lessonDate(), command.effectiveDate());
|
||||
|
||||
ScheduleOverride current = scheduleOverrideRepository
|
||||
scheduleOverrideRepository
|
||||
.findByBaseRuleSlotIdAndLessonDateForUpdate(command.baseRuleSlotId(), command.lessonDate())
|
||||
.orElse(null);
|
||||
return validateAndPersist(current, command);
|
||||
.ifPresent(existing -> {
|
||||
throw new ScheduleConflictException(
|
||||
"Для выбранной базовой пары и даты уже существует точечное изменение"
|
||||
);
|
||||
});
|
||||
return validateAndPersist(null, command);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ScheduleOverrideDto update(Long id, ScheduleOverrideDto request) {
|
||||
OverrideCommand command = validateAndNormalize(request);
|
||||
advisoryLock.lockScheduleOverrideId(id);
|
||||
|
||||
LocalDate observedOldDate = scheduleOverrideRepository.findLessonDateById(id)
|
||||
.orElseThrow(() -> notFound(id));
|
||||
lockDatesInStableOrder(observedOldDate, command.lessonDate());
|
||||
|
||||
ScheduleOverride current = scheduleOverrideRepository.findByIdForUpdate(id)
|
||||
.orElseThrow(() -> notFound(id));
|
||||
if (!Objects.equals(observedOldDate, current.getLessonDate())) {
|
||||
throw new ScheduleConflictException(
|
||||
"Точечное изменение расписания было изменено параллельно. Повторите запрос"
|
||||
);
|
||||
}
|
||||
lockDatesInStableOrder(
|
||||
current.getLessonDate(),
|
||||
effectiveDate(current),
|
||||
command.lessonDate(),
|
||||
command.effectiveDate()
|
||||
);
|
||||
|
||||
scheduleOverrideRepository
|
||||
.findByBaseRuleSlotIdAndLessonDateForUpdate(command.baseRuleSlotId(), command.lessonDate())
|
||||
@@ -119,24 +211,15 @@ public class ScheduleOverrideService {
|
||||
@Transactional
|
||||
public void delete(Long id) {
|
||||
advisoryLock.lockScheduleOverrideId(id);
|
||||
LocalDate observedDate = scheduleOverrideRepository.findLessonDateById(id)
|
||||
.orElseThrow(() -> notFound(id));
|
||||
advisoryLock.lockScheduleOverrideDate(observedDate);
|
||||
|
||||
ScheduleOverride current = scheduleOverrideRepository.findByIdForUpdate(id)
|
||||
.orElseThrow(() -> notFound(id));
|
||||
if (!Objects.equals(observedDate, current.getLessonDate())) {
|
||||
throw new ScheduleConflictException(
|
||||
"Точечное изменение расписания было изменено параллельно. Повторите запрос"
|
||||
);
|
||||
}
|
||||
lockDatesInStableOrder(current.getLessonDate(), effectiveDate(current));
|
||||
scheduleOverrideRepository.delete(current);
|
||||
scheduleOverrideRepository.flush();
|
||||
scheduleGeneratorService.clearCache();
|
||||
}
|
||||
|
||||
private ScheduleOverrideDto validateAndPersist(ScheduleOverride current, OverrideCommand command) {
|
||||
ResolvedReferences references = resolveReferences(command);
|
||||
List<RenderedLessonDto> baseDay = scheduleQueryService.buildBaseDayForAllGroups(command.lessonDate());
|
||||
List<RenderedLessonDto> sourceOccurrences = baseDay.stream()
|
||||
.filter(lesson -> Objects.equals(lesson.scheduleRuleSlotId(), command.baseRuleSlotId()))
|
||||
@@ -147,6 +230,8 @@ public class ScheduleOverrideService {
|
||||
"На выбранную дату базовая пара не формируется по правилам расписания"
|
||||
);
|
||||
}
|
||||
ResolvedReferences references = resolveReferences(command, sourceOccurrences);
|
||||
validateTargetDate(command, references.baseRuleSlot(), sourceOccurrences);
|
||||
|
||||
ScheduleOverride candidate = buildCandidate(command, references);
|
||||
if (!"CANCEL".equals(command.action())) {
|
||||
@@ -169,22 +254,34 @@ public class ScheduleOverrideService {
|
||||
List<RenderedLessonDto> baseDay,
|
||||
List<RenderedLessonDto> sourceOccurrences) {
|
||||
Long currentId = current == null ? null : current.getId();
|
||||
List<ScheduleOverride> effectiveOverrides = scheduleOverrideRepository
|
||||
.findByLessonDateBetweenWithDetails(command.lessonDate(), command.lessonDate())
|
||||
Set<LocalDate> affectedDates = new TreeSet<>(List.of(command.lessonDate(), command.effectiveDate()));
|
||||
List<ScheduleOverride> storedOverrides = affectedDates.size() == 1
|
||||
? scheduleOverrideRepository.findByLessonDateBetweenWithDetails(
|
||||
command.lessonDate(),
|
||||
command.lessonDate()
|
||||
)
|
||||
: scheduleOverrideRepository.findAffectingDatesWithDetails(affectedDates);
|
||||
List<ScheduleOverride> effectiveOverrides = Optional.ofNullable(storedOverrides)
|
||||
.orElseGet(List::of)
|
||||
.stream()
|
||||
.filter(override -> currentId == null || !Objects.equals(override.getId(), currentId))
|
||||
.filter(override -> !sameKey(override, command.baseRuleSlotId(), command.lessonDate()))
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
effectiveOverrides.add(candidate);
|
||||
|
||||
List<RenderedLessonDto> effectiveDay = scheduleQueryService.buildEffectiveDayForAllGroups(
|
||||
baseDay,
|
||||
command.lessonDate(),
|
||||
effectiveOverrides
|
||||
);
|
||||
List<RenderedLessonDto> candidateOccurrences = effectiveDay.stream()
|
||||
List<RenderedLessonDto> effectiveSchedule = Objects.equals(command.lessonDate(), command.effectiveDate())
|
||||
? scheduleQueryService.buildEffectiveDayForAllGroups(
|
||||
baseDay,
|
||||
command.lessonDate(),
|
||||
effectiveOverrides
|
||||
)
|
||||
: scheduleQueryService.buildEffectiveDaysForAllGroups(affectedDates, effectiveOverrides);
|
||||
List<RenderedLessonDto> candidateOccurrences = effectiveSchedule.stream()
|
||||
.filter(lesson -> Objects.equals(lesson.scheduleRuleSlotId(), command.baseRuleSlotId()))
|
||||
.filter(lesson -> Objects.equals(lesson.date(), command.lessonDate()))
|
||||
.filter(lesson -> Objects.equals(lesson.date(), command.effectiveDate()))
|
||||
.filter(lesson -> Objects.equals(lesson.originalLessonDate(), command.lessonDate())
|
||||
|| (Objects.equals(command.lessonDate(), command.effectiveDate())
|
||||
&& lesson.originalLessonDate() == null))
|
||||
.toList();
|
||||
if (candidateOccurrences.isEmpty()) {
|
||||
throw new IllegalArgumentException("Точечное изменение не формирует результирующую пару");
|
||||
@@ -203,13 +300,22 @@ public class ScheduleOverrideService {
|
||||
);
|
||||
}
|
||||
|
||||
List<RenderedLessonDto> otherOccurrences = effectiveDay.stream()
|
||||
.filter(lesson -> !Objects.equals(lesson.scheduleRuleSlotId(), command.baseRuleSlotId()))
|
||||
List<RenderedLessonDto> otherOccurrences = effectiveSchedule.stream()
|
||||
.filter(lesson -> !isCandidateOccurrence(lesson, command))
|
||||
.filter(lesson -> Objects.equals(lesson.date(), command.effectiveDate()))
|
||||
.toList();
|
||||
Map<Long, Long> subgroupToGroup = loadSubgroupGroupMap(candidateOccurrences, otherOccurrences);
|
||||
validateResourceConflicts(candidateOccurrences, otherOccurrences, subgroupToGroup);
|
||||
}
|
||||
|
||||
private boolean isCandidateOccurrence(RenderedLessonDto lesson, OverrideCommand command) {
|
||||
return Objects.equals(lesson.scheduleRuleSlotId(), command.baseRuleSlotId())
|
||||
&& (Objects.equals(lesson.originalLessonDate(), command.lessonDate())
|
||||
|| (Objects.equals(command.lessonDate(), command.effectiveDate())
|
||||
&& lesson.originalLessonDate() == null))
|
||||
&& Objects.equals(lesson.date(), command.effectiveDate());
|
||||
}
|
||||
|
||||
private void validateActionSpecificChange(String action,
|
||||
List<RenderedLessonDto> sourceOccurrences,
|
||||
List<RenderedLessonDto> candidateOccurrences) {
|
||||
@@ -222,7 +328,7 @@ public class ScheduleOverrideService {
|
||||
.collect(Collectors.toSet());
|
||||
if (source.equals(candidate)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Перенос должен фактически менять время пары или аудиторию"
|
||||
"Перенос должен фактически менять дату, время пары или аудиторию"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -331,7 +437,8 @@ public class ScheduleOverrideService {
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
private ResolvedReferences resolveReferences(OverrideCommand command) {
|
||||
private ResolvedReferences resolveReferences(OverrideCommand command,
|
||||
List<RenderedLessonDto> sourceOccurrences) {
|
||||
ScheduleRuleSlot baseRuleSlot = scheduleRuleSlotRepository.findById(command.baseRuleSlotId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Базовый слот расписания не найден"));
|
||||
TimeSlot newTimeSlot = command.newTimeSlotId() == null
|
||||
@@ -347,22 +454,73 @@ public class ScheduleOverrideService {
|
||||
: userRepository.findById(command.newTeacherId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Преподаватель не найден"));
|
||||
|
||||
if (newTeacher != null
|
||||
&& (newTeacher.getRole() != Role.TEACHER || !newTeacher.isActiveOn(command.lessonDate()))) {
|
||||
if ("CANCEL".equals(command.action())) {
|
||||
return new ResolvedReferences(baseRuleSlot, newTimeSlot, newClassroom, newTeacher);
|
||||
}
|
||||
|
||||
LocalDate effectiveDate = command.effectiveDate();
|
||||
User effectiveTeacher = newTeacher == null ? baseRuleSlot.getTeacher() : newTeacher;
|
||||
Classroom effectiveClassroom = newClassroom == null ? baseRuleSlot.getClassroom() : newClassroom;
|
||||
if (effectiveTeacher == null
|
||||
|| effectiveTeacher.getRole() != Role.TEACHER
|
||||
|| !effectiveTeacher.isActiveOn(effectiveDate)) {
|
||||
throw new IllegalArgumentException("Активный преподаватель с ролью TEACHER не найден");
|
||||
}
|
||||
if (newClassroom != null
|
||||
&& (!newClassroom.isActiveOn(command.lessonDate())
|
||||
|| !Boolean.TRUE.equals(newClassroom.getIsAvailable()))) {
|
||||
if (effectiveClassroom == null
|
||||
|| !effectiveClassroom.isActiveOn(effectiveDate)
|
||||
|| !Boolean.TRUE.equals(effectiveClassroom.getIsAvailable())) {
|
||||
throw new IllegalArgumentException("Активная и доступная аудитория не найдена");
|
||||
}
|
||||
if (newTimeSlot != null
|
||||
&& effectiveTimeSlotService != null
|
||||
&& !effectiveTimeSlotService.isEffectiveOnDate(newTimeSlot.getId(), effectiveDate)) {
|
||||
throw new IllegalArgumentException("Выбранный временной слот не действует на целевую дату");
|
||||
}
|
||||
return new ResolvedReferences(baseRuleSlot, newTimeSlot, newClassroom, newTeacher);
|
||||
}
|
||||
|
||||
private void validateTargetDate(OverrideCommand command,
|
||||
ScheduleRuleSlot baseRuleSlot,
|
||||
List<RenderedLessonDto> sourceOccurrences) {
|
||||
if (command.targetLessonDate() == null) {
|
||||
return;
|
||||
}
|
||||
if (academicDateService == null) {
|
||||
throw new IllegalStateException("Сервис учебных дат не настроен");
|
||||
}
|
||||
if (baseRuleSlot.getScheduleRule() == null
|
||||
|| baseRuleSlot.getScheduleRule().getSemester() == null) {
|
||||
throw new IllegalArgumentException("Для базового слота не найден семестр");
|
||||
}
|
||||
var rule = baseRuleSlot.getScheduleRule();
|
||||
var semester = rule.getSemester();
|
||||
LocalDate targetDate = command.targetLessonDate();
|
||||
if (targetDate.isBefore(semester.getStartDate()) || targetDate.isAfter(semester.getEndDate())) {
|
||||
throw new IllegalArgumentException("Перенести занятие можно только в пределах того же семестра");
|
||||
}
|
||||
if (!rule.isActiveOn(targetDate) || !rule.getSubject().isActiveOn(targetDate)) {
|
||||
throw new IllegalArgumentException("Правило или дисциплина не действуют на целевую дату");
|
||||
}
|
||||
Set<Long> affectedGroupIds = sourceOccurrences.stream()
|
||||
.flatMap(lesson -> safeIds(lesson.groupIds()).stream())
|
||||
.collect(Collectors.toSet());
|
||||
List<com.magistr.app.model.StudentGroup> affectedGroups = rule.getGroups().stream()
|
||||
.filter(group -> affectedGroupIds.contains(group.getId()))
|
||||
.toList();
|
||||
if (affectedGroups.size() != affectedGroupIds.size()
|
||||
|| affectedGroups.stream().anyMatch(group -> !group.isActiveOn(targetDate)
|
||||
|| !academicDateService.isTheoryDay(group, semester, targetDate))) {
|
||||
throw new IllegalArgumentException(
|
||||
"Целевая дата должна быть учебной для всех групп исходного занятия"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private ScheduleOverride buildCandidate(OverrideCommand command, ResolvedReferences references) {
|
||||
ScheduleOverride candidate = new ScheduleOverride();
|
||||
candidate.setBaseRuleSlot(references.baseRuleSlot());
|
||||
candidate.setLessonDate(command.lessonDate());
|
||||
candidate.setTargetLessonDate(command.targetLessonDate());
|
||||
candidate.setAction(command.action());
|
||||
candidate.setNewTimeSlot(references.newTimeSlot());
|
||||
candidate.setNewClassroom(references.newClassroom());
|
||||
@@ -374,6 +532,7 @@ public class ScheduleOverrideService {
|
||||
private void copyCandidate(ScheduleOverride source, ScheduleOverride target) {
|
||||
target.setBaseRuleSlot(source.getBaseRuleSlot());
|
||||
target.setLessonDate(source.getLessonDate());
|
||||
target.setTargetLessonDate(source.getTargetLessonDate());
|
||||
target.setAction(source.getAction());
|
||||
target.setNewTimeSlot(source.getNewTimeSlot());
|
||||
target.setNewClassroom(source.getNewClassroom());
|
||||
@@ -397,21 +556,34 @@ public class ScheduleOverrideService {
|
||||
throw new IllegalArgumentException("Недопустимое действие изменения расписания");
|
||||
}
|
||||
String lessonFormat = clean(request.newLessonFormat());
|
||||
LocalDate targetLessonDate = request.targetLessonDate();
|
||||
if (lessonFormat != null && !LESSON_FORMATS.contains(lessonFormat)) {
|
||||
throw new IllegalArgumentException("Формат пары должен быть «Очно» или «Онлайн»");
|
||||
}
|
||||
|
||||
if ("CANCEL".equals(action)
|
||||
&& (request.newTimeSlotId() != null
|
||||
&& (targetLessonDate != null
|
||||
|| request.newTimeSlotId() != null
|
||||
|| request.newClassroomId() != null
|
||||
|| request.newTeacherId() != null
|
||||
|| lessonFormat != null)) {
|
||||
throw new IllegalArgumentException("Для отмены пары новые параметры должны быть пустыми");
|
||||
}
|
||||
if (targetLessonDate != null && Objects.equals(targetLessonDate, request.lessonDate())) {
|
||||
throw new IllegalArgumentException("Целевая дата должна отличаться от исходной даты пары");
|
||||
}
|
||||
if (targetLessonDate != null && request.newTimeSlotId() == null) {
|
||||
throw new IllegalArgumentException("Для переноса даты выберите временной слот целевого дня");
|
||||
}
|
||||
if (targetLessonDate != null && !"MOVE".equals(action)) {
|
||||
throw new IllegalArgumentException("Перенос даты должен использовать действие MOVE");
|
||||
}
|
||||
if ("MOVE".equals(action)
|
||||
&& request.newTimeSlotId() == null
|
||||
&& request.newClassroomId() == null) {
|
||||
throw new IllegalArgumentException("Для переноса укажите новое время или аудиторию");
|
||||
&& request.newTimeSlotId() == null) {
|
||||
throw new IllegalArgumentException("Для переноса укажите новое время");
|
||||
}
|
||||
if ("REPLACE".equals(action) && request.newTimeSlotId() != null) {
|
||||
throw new IllegalArgumentException("Изменение времени должно использовать действие MOVE");
|
||||
}
|
||||
if ("REPLACE".equals(action)
|
||||
&& request.newTeacherId() == null
|
||||
@@ -424,6 +596,7 @@ public class ScheduleOverrideService {
|
||||
return new OverrideCommand(
|
||||
request.baseRuleSlotId(),
|
||||
request.lessonDate(),
|
||||
targetLessonDate,
|
||||
action,
|
||||
request.newTimeSlotId(),
|
||||
request.newClassroomId(),
|
||||
@@ -433,8 +606,9 @@ public class ScheduleOverrideService {
|
||||
);
|
||||
}
|
||||
|
||||
private void lockDatesInStableOrder(LocalDate first, LocalDate second) {
|
||||
List.of(first, second).stream()
|
||||
private void lockDatesInStableOrder(LocalDate... dates) {
|
||||
java.util.Arrays.stream(dates)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.sorted(Comparator.naturalOrder())
|
||||
.forEach(advisoryLock::lockScheduleOverrideDate);
|
||||
@@ -446,15 +620,55 @@ public class ScheduleOverrideService {
|
||||
&& Objects.equals(override.getLessonDate(), lessonDate);
|
||||
}
|
||||
|
||||
private LocalDate effectiveDate(ScheduleOverride override) {
|
||||
return override.getTargetLessonDate() == null
|
||||
? override.getLessonDate()
|
||||
: override.getTargetLessonDate();
|
||||
}
|
||||
|
||||
private void ensureSourceOccurrenceExists(Long baseRuleSlotId, LocalDate lessonDate) {
|
||||
boolean exists = scheduleQueryService.buildBaseDayForAllGroups(lessonDate).stream()
|
||||
.anyMatch(lesson -> Objects.equals(lesson.scheduleRuleSlotId(), baseRuleSlotId)
|
||||
&& Objects.equals(lesson.date(), lessonDate));
|
||||
if (!exists) {
|
||||
throw new IllegalArgumentException(
|
||||
"На выбранную дату базовая пара не формируется по правилам расписания"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void validateRange(LocalDate startDate, LocalDate endDate) {
|
||||
if (endDate.isBefore(startDate)) {
|
||||
throw new IllegalArgumentException("Дата окончания не может быть раньше даты начала");
|
||||
}
|
||||
if (ChronoUnit.DAYS.between(startDate, endDate) + 1 > 120) {
|
||||
throw new IllegalArgumentException("Диапазон точечных изменений не может превышать 120 дней");
|
||||
}
|
||||
}
|
||||
|
||||
private NoSuchElementException notFound(Long id) {
|
||||
return new NoSuchElementException("Точечное изменение расписания с идентификатором " + id + " не найдено");
|
||||
}
|
||||
|
||||
private ScheduleOverrideDto toDto(ScheduleOverride override) {
|
||||
ScheduleRuleSlot baseSlot = override.getBaseRuleSlot();
|
||||
var rule = baseSlot == null ? null : baseSlot.getScheduleRule();
|
||||
var semester = rule == null ? null : rule.getSemester();
|
||||
TimeSlot sourceTimeSlot = baseSlot == null ? null : baseSlot.getTimeSlot();
|
||||
User sourceTeacher = baseSlot == null ? null : baseSlot.getTeacher();
|
||||
Classroom sourceClassroom = baseSlot == null ? null : baseSlot.getClassroom();
|
||||
List<String> groupNames = rule == null || rule.getGroups() == null
|
||||
? List.of()
|
||||
: rule.getGroups().stream()
|
||||
.map(com.magistr.app.model.StudentGroup::getName)
|
||||
.filter(Objects::nonNull)
|
||||
.sorted()
|
||||
.toList();
|
||||
return new ScheduleOverrideDto(
|
||||
override.getId(),
|
||||
override.getBaseRuleSlot().getId(),
|
||||
baseSlot == null ? null : baseSlot.getId(),
|
||||
override.getLessonDate(),
|
||||
override.getTargetLessonDate(),
|
||||
override.getAction(),
|
||||
override.getNewTimeSlot() == null ? null : override.getNewTimeSlot().getId(),
|
||||
override.getNewClassroom() == null ? null : override.getNewClassroom().getId(),
|
||||
@@ -462,7 +676,24 @@ public class ScheduleOverrideService {
|
||||
override.getNewLessonFormat(),
|
||||
override.getComment(),
|
||||
override.getCreatedBy(),
|
||||
override.getCreatedAt()
|
||||
override.getCreatedAt(),
|
||||
semester == null ? null : semester.getId(),
|
||||
semester == null ? null : semester.getStartDate(),
|
||||
semester == null ? null : semester.getEndDate(),
|
||||
sourceTimeSlot == null ? null : sourceTimeSlot.getId(),
|
||||
sourceTimeSlot == null ? null : sourceTimeSlot.getOrderNumber(),
|
||||
sourceTimeSlot == null ? null : sourceTimeSlot.getStartTime(),
|
||||
sourceTimeSlot == null ? null : sourceTimeSlot.getEndTime(),
|
||||
sourceTeacher == null ? null : sourceTeacher.getId(),
|
||||
sourceTeacher == null ? null : ScheduleGeneratorService.displayUserName(sourceTeacher),
|
||||
sourceClassroom == null ? null : sourceClassroom.getId(),
|
||||
sourceClassroom == null ? null : sourceClassroom.getName(),
|
||||
baseSlot == null ? null : baseSlot.getLessonFormat(),
|
||||
rule == null || rule.getSubject() == null ? null : rule.getSubject().getName(),
|
||||
baseSlot == null || baseSlot.getLessonType() == null
|
||||
? null
|
||||
: baseSlot.getLessonType().getLessonType(),
|
||||
groupNames
|
||||
);
|
||||
}
|
||||
|
||||
@@ -473,6 +704,7 @@ public class ScheduleOverrideService {
|
||||
private record OverrideCommand(
|
||||
Long baseRuleSlotId,
|
||||
LocalDate lessonDate,
|
||||
LocalDate targetLessonDate,
|
||||
String action,
|
||||
Long newTimeSlotId,
|
||||
Long newClassroomId,
|
||||
@@ -480,6 +712,9 @@ public class ScheduleOverrideService {
|
||||
String newLessonFormat,
|
||||
String comment
|
||||
) {
|
||||
private LocalDate effectiveDate() {
|
||||
return targetLessonDate == null ? lessonDate : targetLessonDate;
|
||||
}
|
||||
}
|
||||
|
||||
private record ResolvedReferences(
|
||||
@@ -491,6 +726,7 @@ public class ScheduleOverrideService {
|
||||
}
|
||||
|
||||
private record EffectiveSourceTuple(
|
||||
LocalDate date,
|
||||
Long timeSlotId,
|
||||
LocalTime startTime,
|
||||
LocalTime endTime,
|
||||
@@ -500,6 +736,7 @@ public class ScheduleOverrideService {
|
||||
) {
|
||||
private static EffectiveSourceTuple from(RenderedLessonDto lesson) {
|
||||
return new EffectiveSourceTuple(
|
||||
lesson.date(),
|
||||
lesson.timeSlotId(),
|
||||
lesson.startTime(),
|
||||
lesson.endTime(),
|
||||
@@ -511,12 +748,13 @@ public class ScheduleOverrideService {
|
||||
}
|
||||
|
||||
private record MoveTuple(
|
||||
LocalDate date,
|
||||
LocalTime startTime,
|
||||
LocalTime endTime,
|
||||
Long classroomId
|
||||
) {
|
||||
private static MoveTuple from(RenderedLessonDto lesson) {
|
||||
return new MoveTuple(lesson.startTime(), lesson.endTime(), lesson.classroomId());
|
||||
return new MoveTuple(lesson.date(), lesson.startTime(), lesson.endTime(), lesson.classroomId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.magistr.app.model.ScheduleOverride;
|
||||
import com.magistr.app.model.StudentGroup;
|
||||
import com.magistr.app.repository.GroupRepository;
|
||||
import com.magistr.app.repository.ScheduleOverrideRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
@@ -22,15 +23,26 @@ public class ScheduleQueryService {
|
||||
private final GroupRepository groupRepository;
|
||||
private final ScheduleOverrideRepository scheduleOverrideRepository;
|
||||
private final StudentGroupLifecycleService groupLifecycleService;
|
||||
private final AcademicDateService academicDateService;
|
||||
|
||||
@Autowired
|
||||
public ScheduleQueryService(ScheduleGeneratorService scheduleGeneratorService,
|
||||
GroupRepository groupRepository,
|
||||
ScheduleOverrideRepository scheduleOverrideRepository,
|
||||
StudentGroupLifecycleService groupLifecycleService) {
|
||||
StudentGroupLifecycleService groupLifecycleService,
|
||||
AcademicDateService academicDateService) {
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
this.groupRepository = groupRepository;
|
||||
this.scheduleOverrideRepository = scheduleOverrideRepository;
|
||||
this.groupLifecycleService = groupLifecycleService;
|
||||
this.academicDateService = academicDateService;
|
||||
}
|
||||
|
||||
ScheduleQueryService(ScheduleGeneratorService scheduleGeneratorService,
|
||||
GroupRepository groupRepository,
|
||||
ScheduleOverrideRepository scheduleOverrideRepository,
|
||||
StudentGroupLifecycleService groupLifecycleService) {
|
||||
this(scheduleGeneratorService, groupRepository, scheduleOverrideRepository, groupLifecycleService, null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,6 +87,31 @@ public class ScheduleQueryService {
|
||||
return deduplicate(applyOverrides(safeBaseDay, date, overrides));
|
||||
}
|
||||
|
||||
/**
|
||||
* Строит несколько эффективных дней с учётом входящих и исходящих переносов.
|
||||
* Исходная пара достраивается даже тогда, когда её дата не входит в результирующий набор.
|
||||
*/
|
||||
public List<RenderedLessonDto> buildEffectiveDaysForAllGroups(
|
||||
Collection<LocalDate> dates,
|
||||
Collection<ScheduleOverride> overrides
|
||||
) {
|
||||
Set<LocalDate> requestedDates = Optional.ofNullable(dates)
|
||||
.orElseGet(List::of)
|
||||
.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toCollection(TreeSet::new));
|
||||
if (requestedDates.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
List<RenderedLessonDto> baseLessons = requestedDates.stream()
|
||||
.flatMap(date -> buildBaseDayForAllGroups(date).stream())
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
addOverrideSourceOccurrences(baseLessons, overrides, null);
|
||||
return deduplicate(applyOverrides(baseLessons, null, overrides).stream()
|
||||
.filter(lesson -> requestedDates.contains(lesson.date()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
public List<RenderedLessonDto> search(Long groupId,
|
||||
Long teacherId,
|
||||
Long classroomId,
|
||||
@@ -137,29 +174,26 @@ public class ScheduleQueryService {
|
||||
boolean enforceInteractiveGroupLimit) {
|
||||
validateRange(startDate, endDate);
|
||||
List<RenderedLessonDto> generatedLessons;
|
||||
List<StudentGroup> scopedGroups = null;
|
||||
boolean teacherOnlySearch = groupId == null && departmentId == null && teacherId != null;
|
||||
if (teacherOnlySearch) {
|
||||
generatedLessons = scheduleGeneratorService.buildScheduleForTeacher(teacherId, startDate, endDate);
|
||||
} else {
|
||||
List<StudentGroup> groups = resolveGroups(
|
||||
scopedGroups = resolveGroups(
|
||||
groupId,
|
||||
departmentId,
|
||||
startDate,
|
||||
endDate,
|
||||
enforceInteractiveGroupLimit
|
||||
);
|
||||
generatedLessons = scheduleGeneratorService.buildScheduleForGroups(groups, startDate, endDate);
|
||||
generatedLessons = scheduleGeneratorService.buildScheduleForGroups(scopedGroups, startDate, endDate);
|
||||
}
|
||||
List<ScheduleOverride> overrideSnapshot = scheduleOverrideRepository
|
||||
.findByLessonDateBetweenWithDetails(startDate, endDate);
|
||||
if (teacherOnlySearch) {
|
||||
generatedLessons = addOverrideSourceOccurrencesForTeacher(
|
||||
generatedLessons,
|
||||
teacherId,
|
||||
overrideSnapshot
|
||||
);
|
||||
}
|
||||
generatedLessons = new ArrayList<>(generatedLessons);
|
||||
addOverrideSourceOccurrences(generatedLessons, overrideSnapshot, scopedGroups);
|
||||
List<RenderedLessonDto> lessons = applyOverrides(generatedLessons, null, overrideSnapshot).stream()
|
||||
.filter(lesson -> !lesson.date().isBefore(startDate) && !lesson.date().isAfter(endDate))
|
||||
.filter(lesson -> teacherId == null || Objects.equals(lesson.teacherId(), teacherId))
|
||||
.filter(lesson -> classroomId == null || Objects.equals(lesson.classroomId(), classroomId))
|
||||
.filter(lesson -> subjectId == null || Objects.equals(lesson.subjectId(), subjectId))
|
||||
@@ -170,10 +204,10 @@ public class ScheduleQueryService {
|
||||
return deduplicate(lessons);
|
||||
}
|
||||
|
||||
private List<RenderedLessonDto> addOverrideSourceOccurrencesForTeacher(
|
||||
private void addOverrideSourceOccurrences(
|
||||
List<RenderedLessonDto> generatedLessons,
|
||||
Long teacherId,
|
||||
Collection<ScheduleOverride> overrideSnapshot
|
||||
Collection<ScheduleOverride> overrideSnapshot,
|
||||
List<StudentGroup> scopedGroups
|
||||
) {
|
||||
Map<LocalDate, Set<Long>> relevantSlotsByDate = Optional.ofNullable(overrideSnapshot)
|
||||
.orElseGet(List::of)
|
||||
@@ -182,8 +216,7 @@ public class ScheduleQueryService {
|
||||
.filter(override -> override.getLessonDate() != null)
|
||||
.filter(override -> override.getBaseRuleSlot() != null)
|
||||
.filter(override -> override.getBaseRuleSlot().getId() != null)
|
||||
.filter(override -> override.getNewTeacher() != null)
|
||||
.filter(override -> Objects.equals(override.getNewTeacher().getId(), teacherId))
|
||||
.filter(override -> !"CANCEL".equals(override.getAction()))
|
||||
.collect(Collectors.groupingBy(
|
||||
ScheduleOverride::getLessonDate,
|
||||
TreeMap::new,
|
||||
@@ -193,15 +226,27 @@ public class ScheduleQueryService {
|
||||
)
|
||||
));
|
||||
if (relevantSlotsByDate.isEmpty()) {
|
||||
return generatedLessons;
|
||||
return;
|
||||
}
|
||||
|
||||
List<RenderedLessonDto> supplementedLessons = new ArrayList<>(generatedLessons);
|
||||
relevantSlotsByDate.forEach((date, baseRuleSlotIds) -> buildBaseDayForAllGroups(date).stream()
|
||||
.filter(lesson -> date.equals(lesson.date()))
|
||||
.filter(lesson -> baseRuleSlotIds.contains(lesson.scheduleRuleSlotId()))
|
||||
.forEach(supplementedLessons::add));
|
||||
return supplementedLessons;
|
||||
relevantSlotsByDate.forEach((date, baseRuleSlotIds) -> {
|
||||
Set<Long> missingBaseRuleSlotIds = baseRuleSlotIds.stream()
|
||||
.filter(baseRuleSlotId -> generatedLessons.stream().noneMatch(lesson ->
|
||||
date.equals(lesson.date())
|
||||
&& Objects.equals(baseRuleSlotId, lesson.scheduleRuleSlotId())
|
||||
))
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
if (missingBaseRuleSlotIds.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<RenderedLessonDto> sourceDay = scopedGroups == null
|
||||
? buildBaseDayForAllGroups(date)
|
||||
: deduplicate(scheduleGeneratorService.buildScheduleForGroups(scopedGroups, date, date));
|
||||
sourceDay.stream()
|
||||
.filter(lesson -> date.equals(lesson.date()))
|
||||
.filter(lesson -> missingBaseRuleSlotIds.contains(lesson.scheduleRuleSlotId()))
|
||||
.forEach(generatedLessons::add);
|
||||
});
|
||||
}
|
||||
|
||||
private List<RenderedLessonDto> applyOverrides(List<RenderedLessonDto> lessons,
|
||||
@@ -235,6 +280,26 @@ public class ScheduleQueryService {
|
||||
if ("CANCEL".equals(override.getAction())) {
|
||||
return null;
|
||||
}
|
||||
LocalDate effectiveDate = override.getTargetLessonDate() == null
|
||||
? lesson.date()
|
||||
: override.getTargetLessonDate();
|
||||
Integer dayOfWeek = lesson.dayOfWeek();
|
||||
String dayName = lesson.dayName();
|
||||
Integer weekNumber = lesson.weekNumber();
|
||||
com.magistr.app.model.ScheduleParity parity = lesson.parity();
|
||||
if (!Objects.equals(effectiveDate, lesson.date())) {
|
||||
if (academicDateService == null
|
||||
|| override.getBaseRuleSlot() == null
|
||||
|| override.getBaseRuleSlot().getScheduleRule() == null
|
||||
|| override.getBaseRuleSlot().getScheduleRule().getSemester() == null) {
|
||||
throw new IllegalStateException("Невозможно пересчитать календарные параметры перенесённой пары");
|
||||
}
|
||||
var semester = override.getBaseRuleSlot().getScheduleRule().getSemester();
|
||||
dayOfWeek = effectiveDate.getDayOfWeek().getValue();
|
||||
dayName = dayName(effectiveDate.getDayOfWeek());
|
||||
weekNumber = academicDateService.getWeekNumber(semester, effectiveDate);
|
||||
parity = academicDateService.getParity(semester, effectiveDate);
|
||||
}
|
||||
Long timeSlotId = lesson.timeSlotId();
|
||||
Integer timeSlotOrder = lesson.timeSlotOrder();
|
||||
java.time.LocalTime startTime = lesson.startTime();
|
||||
@@ -255,11 +320,11 @@ public class ScheduleQueryService {
|
||||
return new RenderedLessonDto(
|
||||
lesson.scheduleRuleId(),
|
||||
lesson.scheduleRuleSlotId(),
|
||||
lesson.date(),
|
||||
lesson.dayOfWeek(),
|
||||
lesson.dayName(),
|
||||
lesson.weekNumber(),
|
||||
lesson.parity(),
|
||||
effectiveDate,
|
||||
dayOfWeek,
|
||||
dayName,
|
||||
weekNumber,
|
||||
parity,
|
||||
timeSlotId,
|
||||
timeSlotOrder,
|
||||
startTime,
|
||||
@@ -283,10 +348,25 @@ public class ScheduleQueryService {
|
||||
lesson.lessonTypeAcademicHours(),
|
||||
lesson.consumedLessonTypeAcademicHoursBeforeLesson(),
|
||||
lesson.remainingLessonTypeAcademicHoursAfterLesson(),
|
||||
lesson.ruleParity()
|
||||
lesson.ruleParity(),
|
||||
override.getId(),
|
||||
override.getAction(),
|
||||
lesson.date()
|
||||
);
|
||||
}
|
||||
|
||||
private String dayName(java.time.DayOfWeek dayOfWeek) {
|
||||
return switch (dayOfWeek) {
|
||||
case MONDAY -> "Понедельник";
|
||||
case TUESDAY -> "Вторник";
|
||||
case WEDNESDAY -> "Среда";
|
||||
case THURSDAY -> "Четверг";
|
||||
case FRIDAY -> "Пятница";
|
||||
case SATURDAY -> "Суббота";
|
||||
case SUNDAY -> "Воскресенье";
|
||||
};
|
||||
}
|
||||
|
||||
private String overrideKey(Long scheduleRuleSlotId, LocalDate date) {
|
||||
return scheduleRuleSlotId + ":" + date;
|
||||
}
|
||||
@@ -356,7 +436,9 @@ public class ScheduleQueryService {
|
||||
String.valueOf(lesson.timeSlotOrder()),
|
||||
String.valueOf(lesson.teacherId()),
|
||||
String.valueOf(lesson.subjectId()),
|
||||
sortedSignature(lesson.subgroupIds())
|
||||
sortedSignature(lesson.subgroupIds()),
|
||||
String.valueOf(lesson.scheduleOverrideId()),
|
||||
String.valueOf(lesson.originalLessonDate())
|
||||
);
|
||||
}
|
||||
|
||||
@@ -428,7 +510,10 @@ public class ScheduleQueryService {
|
||||
source.lessonTypeAcademicHours(),
|
||||
source.consumedLessonTypeAcademicHoursBeforeLesson(),
|
||||
source.remainingLessonTypeAcademicHoursAfterLesson(),
|
||||
source.ruleParity()
|
||||
source.ruleParity(),
|
||||
source.scheduleOverrideId(),
|
||||
source.overrideAction(),
|
||||
source.originalLessonDate()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user