ролевая моделю доступа + загруженность по кафедрам и преподавателям + просморт расписания
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
package com.magistr.app.config.auth;
|
||||
|
||||
public final class AuthContext {
|
||||
|
||||
private static final ThreadLocal<AuthenticatedUser> CURRENT_USER = new ThreadLocal<>();
|
||||
|
||||
private AuthContext() {
|
||||
}
|
||||
|
||||
public static void setCurrentUser(AuthenticatedUser user) {
|
||||
CURRENT_USER.set(user);
|
||||
}
|
||||
|
||||
public static AuthenticatedUser getCurrentUser() {
|
||||
return CURRENT_USER.get();
|
||||
}
|
||||
|
||||
public static Long currentUserId() {
|
||||
AuthenticatedUser user = getCurrentUser();
|
||||
return user == null ? null : user.id();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
CURRENT_USER.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.magistr.app.config.auth;
|
||||
|
||||
import com.magistr.app.model.User;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class AuthSessionService {
|
||||
|
||||
private final Map<String, AuthenticatedUser> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
public String createSession(User user) {
|
||||
String token = UUID.randomUUID().toString();
|
||||
sessions.put(token, new AuthenticatedUser(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getRole(),
|
||||
user.getDepartmentId()
|
||||
));
|
||||
return token;
|
||||
}
|
||||
|
||||
public Optional<AuthenticatedUser> findByToken(String token) {
|
||||
if (token == null || token.isBlank()) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return Optional.ofNullable(sessions.get(token));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.magistr.app.config.auth;
|
||||
|
||||
import com.magistr.app.model.Role;
|
||||
|
||||
public record AuthenticatedUser(Long id, String username, Role role, Long departmentId) {
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.magistr.app.config.auth;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.magistr.app.model.Role;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class AuthorizationInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final AuthSessionService sessionService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public AuthorizationInterceptor(AuthSessionService sessionService, ObjectMapper objectMapper) {
|
||||
this.sessionService = sessionService;
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
|
||||
if (!request.getRequestURI().startsWith("/api/") || "OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
||||
return true;
|
||||
}
|
||||
if (request.getRequestURI().equals("/api/auth/login")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
String token = bearerToken(request.getHeader("Authorization"));
|
||||
AuthenticatedUser user = sessionService.findByToken(token).orElse(null);
|
||||
if (user == null) {
|
||||
writeError(response, HttpServletResponse.SC_UNAUTHORIZED, "Требуется вход в систему");
|
||||
return false;
|
||||
}
|
||||
|
||||
AuthContext.setCurrentUser(user);
|
||||
RequireRoles roles = resolveRoles(handler);
|
||||
if (roles != null && Arrays.stream(roles.value()).noneMatch(role -> role == user.role())) {
|
||||
writeError(response, HttpServletResponse.SC_FORBIDDEN, "Недостаточно прав для выполнения операции");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
|
||||
AuthContext.clear();
|
||||
}
|
||||
|
||||
private RequireRoles resolveRoles(Object handler) {
|
||||
if (!(handler instanceof HandlerMethod handlerMethod)) {
|
||||
return null;
|
||||
}
|
||||
RequireRoles methodRoles = handlerMethod.getMethodAnnotation(RequireRoles.class);
|
||||
if (methodRoles != null) {
|
||||
return methodRoles;
|
||||
}
|
||||
return handlerMethod.getBeanType().getAnnotation(RequireRoles.class);
|
||||
}
|
||||
|
||||
private String bearerToken(String header) {
|
||||
if (header == null || !header.startsWith("Bearer ")) {
|
||||
return null;
|
||||
}
|
||||
return header.substring("Bearer ".length()).trim();
|
||||
}
|
||||
|
||||
private void writeError(HttpServletResponse response, int status, String message) throws IOException {
|
||||
response.setStatus(status);
|
||||
response.setContentType("application/json;charset=UTF-8");
|
||||
objectMapper.writeValue(response.getOutputStream(), Map.of("message", message));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.magistr.app.config.auth;
|
||||
|
||||
import com.magistr.app.model.Role;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
public @interface RequireRoles {
|
||||
Role[] value();
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.magistr.app.config.tenant;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.magistr.app.config.auth.AuthorizationInterceptor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@@ -123,6 +124,9 @@ public class TenantDataSourceConfig implements WebMvcConfigurer {
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private TenantRoutingDataSource tenantRoutingDataSource;
|
||||
|
||||
@org.springframework.beans.factory.annotation.Autowired
|
||||
private AuthorizationInterceptor authorizationInterceptor;
|
||||
|
||||
@Bean
|
||||
public TenantInterceptor tenantInterceptor(TenantRoutingDataSource routingDataSource) {
|
||||
TenantInterceptor interceptor = new TenantInterceptor();
|
||||
@@ -134,6 +138,7 @@ public class TenantDataSourceConfig implements WebMvcConfigurer {
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// Вызываем метод-бин с переданным параметром (будет перехвачен CGLIB)
|
||||
registry.addInterceptor(tenantInterceptor(tenantRoutingDataSource)).addPathPatterns("/**");
|
||||
registry.addInterceptor(authorizationInterceptor).addPathPatterns("/api/**");
|
||||
}
|
||||
|
||||
private List<TenantConfig> loadTenantsFromFile() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.*;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
@@ -13,6 +14,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/calendar")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class AcademicCalendarAdminController {
|
||||
|
||||
private final AcademicYearRepository academicYearRepository;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.*;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
@@ -13,6 +14,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/academic-calendars")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class AcademicCalendarController {
|
||||
|
||||
private final AcademicCalendarRepository calendarRepository;
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.dto.LoginRequest;
|
||||
import com.magistr.app.dto.LoginResponse;
|
||||
import com.magistr.app.config.auth.AuthSessionService;
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.model.User;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
@@ -10,7 +12,6 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@@ -18,16 +19,23 @@ public class AuthController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final BCryptPasswordEncoder passwordEncoder;
|
||||
private final AuthSessionService sessionService;
|
||||
|
||||
private static final Map<String, String> ROLE_REDIRECTS = Map.of(
|
||||
"ADMIN", "/admin/",
|
||||
"EDUCATION_OFFICE", "/admin/#schedule-view",
|
||||
"DEPARTMENT", "/admin/#department-workspace",
|
||||
"SCHEDULE_VIEWER", "/admin/#schedule-view",
|
||||
"TEACHER", "/teacher/",
|
||||
"STUDENT", "/student/"
|
||||
);
|
||||
|
||||
public AuthController(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
|
||||
public AuthController(UserRepository userRepository,
|
||||
BCryptPasswordEncoder passwordEncoder,
|
||||
AuthSessionService sessionService) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.sessionService = sessionService;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@@ -42,11 +50,30 @@ public class AuthController {
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
String token = UUID.randomUUID().toString();
|
||||
if (user.isArchivedRecord()) {
|
||||
return ResponseEntity
|
||||
.status(401)
|
||||
.body(new LoginResponse(false, "Пользователь архивирован", null, null, null, null));
|
||||
}
|
||||
String token = sessionService.createSession(user);
|
||||
String roleName = user.getRole().name();
|
||||
String redirect = ROLE_REDIRECTS.getOrDefault(roleName, "/");
|
||||
Long departmentId = user.getDepartmentId();
|
||||
|
||||
return ResponseEntity.ok(new LoginResponse(true, "OK", token, roleName, redirect, departmentId, user.getId()));
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<?> me() {
|
||||
var user = AuthContext.getCurrentUser();
|
||||
if (user == null) {
|
||||
return ResponseEntity.status(401).body(Map.of("message", "Требуется вход в систему"));
|
||||
}
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"userId", user.id(),
|
||||
"username", user.username(),
|
||||
"role", user.role().name(),
|
||||
"departmentId", user.departmentId()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.ClassroomRequest;
|
||||
import com.magistr.app.dto.ClassroomResponse;
|
||||
import com.magistr.app.model.Classroom;
|
||||
import com.magistr.app.model.Equipment;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.repository.ClassroomRepository;
|
||||
import com.magistr.app.repository.EquipmentRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -15,6 +18,7 @@ import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/classrooms")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class ClassroomController {
|
||||
|
||||
private final ClassroomRepository classroomRepository;
|
||||
@@ -26,8 +30,12 @@ public class ClassroomController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<ClassroomResponse> getAllClassrooms() {
|
||||
return classroomRepository.findAll().stream()
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public List<ClassroomResponse> getAllClassrooms(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
List<Classroom> classrooms = includeArchived
|
||||
? classroomRepository.findAll()
|
||||
: classroomRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
return classrooms.stream()
|
||||
.map(this::mapToResponse)
|
||||
.toList();
|
||||
}
|
||||
@@ -52,7 +60,9 @@ public class ClassroomController {
|
||||
classroom.setIsAvailable(request.getIsAvailable() != null ? request.getIsAvailable() : true);
|
||||
|
||||
if (request.getEquipmentIds() != null && !request.getEquipmentIds().isEmpty()) {
|
||||
List<Equipment> equipments = equipmentRepository.findAllById(request.getEquipmentIds());
|
||||
List<Equipment> equipments = equipmentRepository.findAllById(request.getEquipmentIds()).stream()
|
||||
.filter(Equipment::isActiveRecord)
|
||||
.toList();
|
||||
classroom.setEquipments(new java.util.HashSet<>(equipments));
|
||||
}
|
||||
|
||||
@@ -96,6 +106,9 @@ public class ClassroomController {
|
||||
|
||||
if (request.getEquipmentIds() != null) {
|
||||
List<Equipment> equipments = equipmentRepository.findAllById(request.getEquipmentIds());
|
||||
equipments = equipments.stream()
|
||||
.filter(Equipment::isActiveRecord)
|
||||
.toList();
|
||||
classroom.setEquipments(new java.util.HashSet<>(equipments));
|
||||
}
|
||||
|
||||
@@ -105,15 +118,33 @@ public class ClassroomController {
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteClassroom(@PathVariable Long id) {
|
||||
if (!classroomRepository.existsById(id)) {
|
||||
Optional<Classroom> classroomOpt = classroomRepository.findById(id);
|
||||
if (classroomOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
classroomRepository.deleteById(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Аудитория удалена"));
|
||||
Classroom classroom = classroomOpt.get();
|
||||
classroom.archive("Выведена из эксплуатации");
|
||||
classroom.setIsAvailable(false);
|
||||
classroomRepository.save(classroom);
|
||||
return ResponseEntity.ok(Map.of("message", "Аудитория выведена из эксплуатации"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
public ResponseEntity<?> restoreClassroom(@PathVariable Long id) {
|
||||
Optional<Classroom> classroomOpt = classroomRepository.findById(id);
|
||||
if (classroomOpt.isEmpty()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
Classroom classroom = classroomOpt.get();
|
||||
classroom.restore();
|
||||
classroom.setIsAvailable(true);
|
||||
classroomRepository.save(classroom);
|
||||
return ResponseEntity.ok(mapToResponse(classroom));
|
||||
}
|
||||
|
||||
private ClassroomResponse mapToResponse(Classroom c) {
|
||||
return new ClassroomResponse(c.getId(), c.getName(), c.getCapacity(), c.getBuilding(), c.getFloor(), c.getIsAvailable(),
|
||||
c.getStatus(), c.getArchivedAt(), c.getArchiveReason(),
|
||||
new java.util.ArrayList<>(c.getEquipments()));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.config.tenant.ConfigMapUpdater;
|
||||
import com.magistr.app.config.tenant.TenantConfig;
|
||||
import com.magistr.app.config.tenant.TenantConfigWatcher;
|
||||
import com.magistr.app.config.tenant.TenantContext;
|
||||
import com.magistr.app.config.tenant.TenantRoutingDataSource;
|
||||
import com.magistr.app.model.Role;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@@ -24,6 +26,7 @@ import java.util.Map;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/database")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class DatabaseController {
|
||||
|
||||
private final TenantRoutingDataSource routingDataSource;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateDepartmentRequest;
|
||||
import com.magistr.app.dto.DepartmentResponse;
|
||||
import com.magistr.app.model.Department;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.repository.DepartmentRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -15,6 +18,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/departments")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class DepartmentController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(DepartmentController.class);
|
||||
@@ -26,10 +30,13 @@ public class DepartmentController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Department> getAllDepartments() {
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public List<Department> getAllDepartments(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
logger.info("Получен запрос на получение списка кафедр");
|
||||
try {
|
||||
List<Department> departments = departmentRepository.findAll();
|
||||
List<Department> departments = includeArchived
|
||||
? departmentRepository.findAll()
|
||||
: departmentRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
List<Department> response = departments.stream()
|
||||
.map( d -> new Department(
|
||||
d.getId(),
|
||||
@@ -150,12 +157,29 @@ public class DepartmentController {
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteDepartment(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление кафедры с ID: {}", id);
|
||||
if (!departmentRepository.existsById(id)) {
|
||||
Department department = departmentRepository.findById(id).orElse(null);
|
||||
if (department == null) {
|
||||
logger.info("Кафедра с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
departmentRepository.deleteById(id);
|
||||
logger.info("Кафедра с ID - {} успешно удалена", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Кафедра удалена"));
|
||||
department.archive("Кафедра архивирована");
|
||||
departmentRepository.save(department);
|
||||
logger.info("Кафедра с ID - {} успешно архивирована", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Кафедра архивирована"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
public ResponseEntity<?> restoreDepartment(@PathVariable Long id) {
|
||||
Department department = departmentRepository.findById(id).orElse(null);
|
||||
if (department == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
department.restore();
|
||||
departmentRepository.save(department);
|
||||
return ResponseEntity.ok(new DepartmentResponse(
|
||||
department.getId(),
|
||||
department.getDepartmentName(),
|
||||
department.getDepartmentCode()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateSubjectRequest;
|
||||
import com.magistr.app.dto.SubjectCommentDto;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.SubjectCommentRepository;
|
||||
import com.magistr.app.repository.SubjectRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import com.magistr.app.service.ScheduleQueryService;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/department")
|
||||
@RequireRoles({Role.ADMIN, Role.DEPARTMENT})
|
||||
public class DepartmentWorkspaceController {
|
||||
|
||||
private final SubjectRepository subjectRepository;
|
||||
private final SubjectCommentRepository subjectCommentRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final ScheduleQueryService scheduleQueryService;
|
||||
|
||||
public DepartmentWorkspaceController(SubjectRepository subjectRepository,
|
||||
SubjectCommentRepository subjectCommentRepository,
|
||||
UserRepository userRepository,
|
||||
ScheduleQueryService scheduleQueryService) {
|
||||
this.subjectRepository = subjectRepository;
|
||||
this.subjectCommentRepository = subjectCommentRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.scheduleQueryService = scheduleQueryService;
|
||||
}
|
||||
|
||||
@GetMapping("/subjects")
|
||||
public List<Subject> getSubjects(@RequestParam(required = false) Long departmentId) {
|
||||
Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
|
||||
return subjectRepository.findByDepartmentIdAndStatusNot(effectiveDepartmentId, LifecycleEntity.STATUS_ARCHIVED);
|
||||
}
|
||||
|
||||
@PostMapping("/subjects/import")
|
||||
public ResponseEntity<?> importSubjects(@RequestBody List<CreateSubjectRequest> requests) {
|
||||
if (requests == null || requests.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Передайте список дисциплин для загрузки"));
|
||||
}
|
||||
Long requestedDepartmentId = requests.stream()
|
||||
.map(CreateSubjectRequest::getDepartmentId)
|
||||
.filter(id -> id != null && id > 0)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
Long departmentId = effectiveDepartmentId(requestedDepartmentId);
|
||||
if (departmentId == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Кафедра обязательна"));
|
||||
}
|
||||
List<Subject> saved = requests.stream()
|
||||
.filter(request -> request.getName() != null && !request.getName().isBlank())
|
||||
.map(request -> {
|
||||
Subject subject = subjectRepository.findByName(request.getName().trim()).orElseGet(Subject::new);
|
||||
subject.setName(request.getName().trim());
|
||||
subject.setCode(request.getCode() == null || request.getCode().isBlank() ? null : request.getCode().trim());
|
||||
subject.setDepartmentId(departmentId);
|
||||
if (subject.isArchivedRecord()) {
|
||||
subject.restore();
|
||||
}
|
||||
return subjectRepository.save(subject);
|
||||
})
|
||||
.toList();
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "Дисциплины загружены",
|
||||
"count", saved.size()
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/subjects/{subjectId}/comments")
|
||||
public ResponseEntity<?> getComments(@PathVariable Long subjectId) {
|
||||
Subject subject = subjectRepository.findById(subjectId).orElse(null);
|
||||
if (subject == null || !canAccessDepartment(subject.getDepartmentId())) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(subjectCommentRepository.findBySubjectIdOrderByCreatedAtDesc(subjectId).stream()
|
||||
.map(this::toCommentDto)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/subjects/{subjectId}/comments")
|
||||
public ResponseEntity<?> addComment(@PathVariable Long subjectId, @RequestBody Map<String, String> request) {
|
||||
Subject subject = subjectRepository.findById(subjectId).orElse(null);
|
||||
if (subject == null || !canAccessDepartment(subject.getDepartmentId())) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
String commentText = request == null ? null : request.get("comment");
|
||||
if (commentText == null || commentText.isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Комментарий обязателен"));
|
||||
}
|
||||
SubjectComment comment = new SubjectComment();
|
||||
comment.setSubject(subject);
|
||||
comment.setComment(commentText.trim());
|
||||
Long currentUserId = AuthContext.currentUserId();
|
||||
if (currentUserId != null) {
|
||||
userRepository.findById(currentUserId).ifPresent(comment::setAuthor);
|
||||
}
|
||||
return ResponseEntity.ok(toCommentDto(subjectCommentRepository.save(comment)));
|
||||
}
|
||||
|
||||
@GetMapping("/teachers")
|
||||
public List<?> getTeachers(@RequestParam(required = false) Long departmentId) {
|
||||
Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
|
||||
return userRepository.findByRoleAndDepartmentIdAndStatusNot(Role.TEACHER, effectiveDepartmentId, LifecycleEntity.STATUS_ARCHIVED);
|
||||
}
|
||||
|
||||
@GetMapping("/schedule")
|
||||
public ResponseEntity<?> getSchedule(
|
||||
@RequestParam(required = false) Long departmentId,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate
|
||||
) {
|
||||
Long effectiveDepartmentId = effectiveDepartmentId(departmentId);
|
||||
return ResponseEntity.ok(scheduleQueryService.search(null, null, null, effectiveDepartmentId,
|
||||
null, null, null, null, startDate, endDate));
|
||||
}
|
||||
|
||||
private SubjectCommentDto toCommentDto(SubjectComment comment) {
|
||||
User author = comment.getAuthor();
|
||||
return new SubjectCommentDto(
|
||||
comment.getId(),
|
||||
comment.getSubject().getId(),
|
||||
author == null ? null : author.getId(),
|
||||
author == null ? null : author.getFullName(),
|
||||
comment.getComment(),
|
||||
comment.getCreatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private Long effectiveDepartmentId(Long requestedDepartmentId) {
|
||||
var user = AuthContext.getCurrentUser();
|
||||
if (user != null && user.role() == Role.DEPARTMENT) {
|
||||
return user.departmentId();
|
||||
}
|
||||
return requestedDepartmentId == null && user != null ? user.departmentId() : requestedDepartmentId;
|
||||
}
|
||||
|
||||
private boolean canAccessDepartment(Long departmentId) {
|
||||
var user = AuthContext.getCurrentUser();
|
||||
return user == null
|
||||
|| user.role() == Role.ADMIN
|
||||
|| (user.role() == Role.DEPARTMENT && departmentId.equals(user.departmentId()));
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.model.EducationForm;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.StudentGroup;
|
||||
import com.magistr.app.repository.AcademicCalendarRepository;
|
||||
import com.magistr.app.repository.EducationFormRepository;
|
||||
@@ -13,6 +15,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/education-forms")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class EducationFormController {
|
||||
|
||||
private final EducationFormRepository educationFormRepository;
|
||||
@@ -28,6 +31,7 @@ public class EducationFormController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public List<Map<String, Object>> getAll() {
|
||||
return educationFormRepository.findAllByOrderByNameAsc().stream()
|
||||
.map(ef -> Map.<String, Object>of("id", ef.getId(), "name", ef.getName()))
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.Equipment;
|
||||
import com.magistr.app.repository.EquipmentRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -10,6 +13,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/equipments")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class EquipmentController {
|
||||
|
||||
private final EquipmentRepository equipmentRepository;
|
||||
@@ -19,8 +23,8 @@ public class EquipmentController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Equipment> getAllEquipments() {
|
||||
return equipmentRepository.findAll();
|
||||
public List<Equipment> getAllEquipments(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
return includeArchived ? equipmentRepository.findAll() : equipmentRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@@ -42,10 +46,23 @@ public class EquipmentController {
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteEquipment(@PathVariable Long id) {
|
||||
if (!equipmentRepository.existsById(id)) {
|
||||
Equipment equipment = equipmentRepository.findById(id).orElse(null);
|
||||
if (equipment == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
equipmentRepository.deleteById(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Оборудование удалено"));
|
||||
equipment.archive("Оборудование архивировано");
|
||||
equipmentRepository.save(equipment);
|
||||
return ResponseEntity.ok(Map.of("message", "Оборудование архивировано"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
public ResponseEntity<?> restoreEquipment(@PathVariable Long id) {
|
||||
Equipment equipment = equipmentRepository.findById(id).orElse(null);
|
||||
if (equipment == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
equipment.restore();
|
||||
equipmentRepository.save(equipment);
|
||||
return ResponseEntity.ok(equipment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateGroupRequest;
|
||||
import com.magistr.app.dto.GroupCalendarAssignmentDto;
|
||||
import com.magistr.app.dto.GroupResponse;
|
||||
import com.magistr.app.model.AcademicCalendar;
|
||||
import com.magistr.app.model.AcademicYear;
|
||||
import com.magistr.app.model.EducationForm;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.Speciality;
|
||||
import com.magistr.app.model.SpecialtyProfile;
|
||||
import com.magistr.app.model.StudentGroup;
|
||||
@@ -25,6 +28,7 @@ import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/groups")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER, Role.STUDENT})
|
||||
public class GroupController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GroupController.class);
|
||||
@@ -57,11 +61,13 @@ public class GroupController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<GroupResponse> getAllGroups() {
|
||||
public List<GroupResponse> getAllGroups(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
logger.info("Получен запрос на получение всех групп");
|
||||
|
||||
try {
|
||||
List<StudentGroup> groups = groupRepository.findAll();
|
||||
List<StudentGroup> groups = includeArchived
|
||||
? groupRepository.findAll()
|
||||
: groupRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
|
||||
List<GroupResponse> response = groups.stream()
|
||||
.map(this::mapToResponse)
|
||||
@@ -78,7 +84,7 @@ public class GroupController {
|
||||
public ResponseEntity<?> getGroupsByDepartmentId(@PathVariable Long departmentId) {
|
||||
logger.info("Получен запрос на получение списка групп для кафедры с ID - {}", departmentId);
|
||||
try {
|
||||
List<StudentGroup> groups = groupRepository.findByDepartmentId(departmentId);
|
||||
List<StudentGroup> groups = groupRepository.findByDepartmentIdAndStatusNot(departmentId, LifecycleEntity.STATUS_ARCHIVED);
|
||||
|
||||
if(groups.isEmpty()) {
|
||||
logger.info("Группы для кафедры с ID - {} не найдены", departmentId);
|
||||
@@ -101,6 +107,7 @@ public class GroupController {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public ResponseEntity<?> createGroup(@RequestBody CreateGroupRequest request) {
|
||||
logger.info("Получен запрос на создание новой группы: name = {}, groupSize = {}, educationFormId = {}, departmentId = {}, yearStartStudy = {}",
|
||||
request.getName(), request.getGroupSize(), request.getEducationFormId(), request.getDepartmentId(), request.getYearStartStudy());
|
||||
@@ -146,6 +153,7 @@ public class GroupController {
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public ResponseEntity<?> updateGroup(@PathVariable Long id, @RequestBody CreateGroupRequest request) {
|
||||
logger.info("Получен запрос на обновление группы с ID - {}", id);
|
||||
try {
|
||||
@@ -195,15 +203,30 @@ public class GroupController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public ResponseEntity<?> deleteGroup(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление группы с ID - {}", id);
|
||||
if (!groupRepository.existsById(id)) {
|
||||
StudentGroup group = groupRepository.findById(id).orElse(null);
|
||||
if (group == null) {
|
||||
logger.info("Группа с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
groupRepository.deleteById(id);
|
||||
logger.info("Группа с ID - {} успешно удалена", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Группа удалена"));
|
||||
group.archive("Группа архивирована");
|
||||
groupRepository.save(group);
|
||||
logger.info("Группа с ID - {} успешно архивирована", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Группа архивирована"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public ResponseEntity<?> restoreGroup(@PathVariable Long id) {
|
||||
StudentGroup group = groupRepository.findById(id).orElse(null);
|
||||
if (group == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
group.restore();
|
||||
groupRepository.save(group);
|
||||
return ResponseEntity.ok(mapToResponse(group));
|
||||
}
|
||||
|
||||
private ResponseEntity<?> validateGroupRequest(CreateGroupRequest request) {
|
||||
@@ -260,6 +283,7 @@ public class GroupController {
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/calendar-assignments")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> getCalendarAssignments(@PathVariable Long id) {
|
||||
if (!groupRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -270,6 +294,7 @@ public class GroupController {
|
||||
}
|
||||
|
||||
@PutMapping("/{id}/calendar-assignments")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> saveCalendarAssignment(@PathVariable Long id,
|
||||
@RequestBody GroupCalendarAssignmentDto request) {
|
||||
if (request == null || request.academicYearId() == null || request.calendarId() == null) {
|
||||
@@ -309,6 +334,7 @@ public class GroupController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}/calendar-assignments/{assignmentId}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> deleteCalendarAssignment(@PathVariable Long id, @PathVariable Long assignmentId) {
|
||||
StudentGroupCalendarAssignment assignment = assignmentRepository.findById(assignmentId).orElse(null);
|
||||
if (assignment == null || !assignment.getStudentGroup().getId().equals(id)) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.LessonTypeResponse;
|
||||
import com.magistr.app.model.LessonType;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.repository.LessonTypesRepository;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -12,6 +14,7 @@ import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/lesson-types")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public class LessonTypeController {
|
||||
|
||||
private final LessonTypesRepository lessonTypesRepository;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.RenderedLessonDto;
|
||||
import com.magistr.app.service.ScheduleGeneratorService;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.service.ScheduleQueryService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
@@ -14,14 +16,15 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/schedule")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER, Role.TEACHER, Role.STUDENT})
|
||||
public class ScheduleController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScheduleController.class);
|
||||
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
private final ScheduleQueryService scheduleQueryService;
|
||||
|
||||
public ScheduleController(ScheduleGeneratorService scheduleGeneratorService) {
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
public ScheduleController(ScheduleQueryService scheduleQueryService) {
|
||||
this.scheduleQueryService = scheduleQueryService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@@ -40,9 +43,18 @@ public class ScheduleController {
|
||||
}
|
||||
|
||||
try {
|
||||
List<RenderedLessonDto> lessons = groupId != null
|
||||
? scheduleGeneratorService.buildScheduleForGroup(groupId, startDate, endDate)
|
||||
: scheduleGeneratorService.buildScheduleForTeacher(teacherId, startDate, endDate);
|
||||
List<RenderedLessonDto> lessons = scheduleQueryService.search(
|
||||
groupId,
|
||||
teacherId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
startDate,
|
||||
endDate
|
||||
);
|
||||
return ResponseEntity.ok(lessons);
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.info("Ошибка запроса динамического расписания: {}", e.getMessage());
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.ScheduleOverrideDto;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
import com.magistr.app.service.ScheduleGeneratorService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/edu-office/schedule/overrides")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class ScheduleOverrideController {
|
||||
|
||||
private final ScheduleOverrideRepository scheduleOverrideRepository;
|
||||
private final ScheduleRuleSlotRepository scheduleRuleSlotRepository;
|
||||
private final TimeSlotRepository timeSlotRepository;
|
||||
private final ClassroomRepository classroomRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
|
||||
public ScheduleOverrideController(ScheduleOverrideRepository scheduleOverrideRepository,
|
||||
ScheduleRuleSlotRepository scheduleRuleSlotRepository,
|
||||
TimeSlotRepository timeSlotRepository,
|
||||
ClassroomRepository classroomRepository,
|
||||
UserRepository userRepository,
|
||||
ScheduleGeneratorService scheduleGeneratorService) {
|
||||
this.scheduleOverrideRepository = scheduleOverrideRepository;
|
||||
this.scheduleRuleSlotRepository = scheduleRuleSlotRepository;
|
||||
this.timeSlotRepository = timeSlotRepository;
|
||||
this.classroomRepository = classroomRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<ScheduleOverrideDto> getAll() {
|
||||
return scheduleOverrideRepository.findAll().stream()
|
||||
.map(this::toDto)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<?> create(@RequestBody ScheduleOverrideDto request) {
|
||||
return save(null, request);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<?> update(@PathVariable Long id, @RequestBody ScheduleOverrideDto request) {
|
||||
if (!scheduleOverrideRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return save(id, request);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> delete(@PathVariable Long id) {
|
||||
if (!scheduleOverrideRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
scheduleOverrideRepository.deleteById(id);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Точечное изменение расписания удалено"));
|
||||
}
|
||||
|
||||
private ResponseEntity<?> save(Long id, ScheduleOverrideDto request) {
|
||||
String validationError = validate(request);
|
||||
if (validationError != null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", validationError));
|
||||
}
|
||||
|
||||
ScheduleOverride override = id == null
|
||||
? scheduleOverrideRepository
|
||||
.findByBaseRuleSlotIdAndLessonDate(request.baseRuleSlotId(), request.lessonDate())
|
||||
.orElseGet(ScheduleOverride::new)
|
||||
: scheduleOverrideRepository.findById(id).orElseGet(ScheduleOverride::new);
|
||||
|
||||
override.setBaseRuleSlot(scheduleRuleSlotRepository.findById(request.baseRuleSlotId()).orElseThrow());
|
||||
override.setLessonDate(request.lessonDate());
|
||||
override.setAction(request.action());
|
||||
override.setNewTimeSlot(request.newTimeSlotId() == null ? null : timeSlotRepository.findById(request.newTimeSlotId()).orElse(null));
|
||||
override.setNewClassroom(request.newClassroomId() == null ? null : classroomRepository.findById(request.newClassroomId()).orElse(null));
|
||||
override.setNewTeacher(request.newTeacherId() == null ? null : userRepository.findById(request.newTeacherId()).orElse(null));
|
||||
override.setNewLessonFormat(clean(request.newLessonFormat()));
|
||||
override.setComment(clean(request.comment()));
|
||||
override.setCreatedBy(AuthContext.currentUserId());
|
||||
|
||||
ScheduleOverride saved = scheduleOverrideRepository.save(override);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toDto(saved));
|
||||
}
|
||||
|
||||
private String validate(ScheduleOverrideDto request) {
|
||||
if (request == null) {
|
||||
return "Передайте данные изменения расписания";
|
||||
}
|
||||
if (request.baseRuleSlotId() == null || scheduleRuleSlotRepository.findById(request.baseRuleSlotId()).isEmpty()) {
|
||||
return "Базовый слот расписания не найден";
|
||||
}
|
||||
if (request.lessonDate() == null) {
|
||||
return "Дата пары обязательна";
|
||||
}
|
||||
if (request.action() == null || !List.of("MOVE", "CANCEL", "REPLACE").contains(request.action())) {
|
||||
return "Недопустимое действие изменения расписания";
|
||||
}
|
||||
if (request.newClassroomId() != null) {
|
||||
Classroom classroom = classroomRepository.findById(request.newClassroomId()).orElse(null);
|
||||
if (classroom == null || classroom.isArchivedRecord() || Boolean.FALSE.equals(classroom.getIsAvailable())) {
|
||||
return "Активная аудитория не найдена";
|
||||
}
|
||||
}
|
||||
if (request.newTeacherId() != null) {
|
||||
User teacher = userRepository.findById(request.newTeacherId()).orElse(null);
|
||||
if (teacher == null || teacher.getRole() != Role.TEACHER || teacher.isArchivedRecord()) {
|
||||
return "Активный преподаватель не найден";
|
||||
}
|
||||
}
|
||||
if (request.newTimeSlotId() != null && timeSlotRepository.findById(request.newTimeSlotId()).isEmpty()) {
|
||||
return "Временной слот не найден";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private ScheduleOverrideDto toDto(ScheduleOverride override) {
|
||||
return new ScheduleOverrideDto(
|
||||
override.getId(),
|
||||
override.getBaseRuleSlot().getId(),
|
||||
override.getLessonDate(),
|
||||
override.getAction(),
|
||||
override.getNewTimeSlot() == null ? null : override.getNewTimeSlot().getId(),
|
||||
override.getNewClassroom() == null ? null : override.getNewClassroom().getId(),
|
||||
override.getNewTeacher() == null ? null : override.getNewTeacher().getId(),
|
||||
override.getNewLessonFormat(),
|
||||
override.getComment(),
|
||||
override.getCreatedBy(),
|
||||
override.getCreatedAt()
|
||||
);
|
||||
}
|
||||
|
||||
private String clean(String value) {
|
||||
return value == null || value.isBlank() ? null : value.trim();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.ScheduleRuleDto;
|
||||
import com.magistr.app.dto.ScheduleRuleSlotDto;
|
||||
import com.magistr.app.model.*;
|
||||
@@ -14,6 +15,7 @@ import java.util.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/schedule-rules")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class ScheduleRuleAdminController {
|
||||
|
||||
private static final String APPLY_MODE_DEFAULT = "DEFAULT";
|
||||
@@ -112,12 +114,14 @@ public class ScheduleRuleAdminController {
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> delete(@PathVariable Long id) {
|
||||
if (!scheduleRuleRepository.existsById(id)) {
|
||||
ScheduleRule rule = scheduleRuleRepository.findById(id).orElse(null);
|
||||
if (rule == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
scheduleRuleRepository.deleteById(id);
|
||||
rule.setStatus(LifecycleEntity.STATUS_ARCHIVED);
|
||||
scheduleRuleRepository.save(rule);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Правило расписания удалено"));
|
||||
return ResponseEntity.ok(Map.of("message", "Правило расписания архивировано"));
|
||||
}
|
||||
|
||||
private String validateRule(ScheduleRuleDto request) {
|
||||
@@ -143,12 +147,18 @@ public class ScheduleRuleAdminController {
|
||||
private void applyRule(ScheduleRule rule, ScheduleRuleDto request) {
|
||||
Subject subject = subjectRepository.findById(request.subjectId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Дисциплина не найдена"));
|
||||
if (subject.isArchivedRecord()) {
|
||||
throw new IllegalArgumentException("Архивную дисциплину нельзя использовать в новых правилах");
|
||||
}
|
||||
Semester semester = semesterRepository.findById(request.semesterId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Семестр не найден"));
|
||||
List<StudentGroup> groups = groupRepository.findAllById(request.groupIds());
|
||||
if (groups.size() != new HashSet<>(request.groupIds()).size()) {
|
||||
throw new IllegalArgumentException("Одна или несколько групп не найдены");
|
||||
}
|
||||
if (groups.stream().anyMatch(StudentGroup::isArchivedRecord)) {
|
||||
throw new IllegalArgumentException("Архивные группы нельзя использовать в новых правилах");
|
||||
}
|
||||
|
||||
rule.setSubject(subject);
|
||||
rule.setSemester(semester);
|
||||
@@ -186,8 +196,14 @@ public class ScheduleRuleAdminController {
|
||||
}
|
||||
User teacher = userRepository.findById(slotDto.teacherId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Преподаватель не найден"));
|
||||
if (teacher.isArchivedRecord()) {
|
||||
throw new IllegalArgumentException("Архивного преподавателя нельзя назначать в расписание");
|
||||
}
|
||||
Classroom classroom = classroomRepository.findById(slotDto.classroomId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Аудитория не найдена"));
|
||||
if (classroom.isArchivedRecord() || Boolean.FALSE.equals(classroom.getIsAvailable())) {
|
||||
throw new IllegalArgumentException("Аудитория недоступна для новых назначений");
|
||||
}
|
||||
LessonType lessonType = lessonTypesRepository.findById(slotDto.lessonTypeId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Тип занятия не найден"));
|
||||
ScheduleLessonCategory category = ScheduleLessonCategory.fromLessonType(lessonType);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.service.ScheduleQueryService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/schedule/search")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER, Role.TEACHER, Role.STUDENT})
|
||||
public class ScheduleSearchController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(ScheduleSearchController.class);
|
||||
|
||||
private final ScheduleQueryService scheduleQueryService;
|
||||
|
||||
public ScheduleSearchController(ScheduleQueryService scheduleQueryService) {
|
||||
this.scheduleQueryService = scheduleQueryService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<?> search(
|
||||
@RequestParam(required = false) Long groupId,
|
||||
@RequestParam(required = false) Long teacherId,
|
||||
@RequestParam(required = false) Long classroomId,
|
||||
@RequestParam(required = false) Long departmentId,
|
||||
@RequestParam(required = false) Long subjectId,
|
||||
@RequestParam(required = false) Long lessonTypeId,
|
||||
@RequestParam(required = false) Long timeSlotId,
|
||||
@RequestParam(required = false) String parity,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate
|
||||
) {
|
||||
logger.info("Расширенный поиск расписания: groupId={}, teacherId={}, classroomId={}, departmentId={}, startDate={}, endDate={}",
|
||||
groupId, teacherId, classroomId, departmentId, startDate, endDate);
|
||||
try {
|
||||
return ResponseEntity.ok(scheduleQueryService.search(
|
||||
groupId,
|
||||
teacherId,
|
||||
classroomId,
|
||||
departmentId,
|
||||
subjectId,
|
||||
lessonTypeId,
|
||||
timeSlotId,
|
||||
parity,
|
||||
startDate,
|
||||
endDate
|
||||
));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка расширенного поиска расписания: {}", e.getMessage(), e);
|
||||
return ResponseEntity.internalServerError().body(Map.of("message", "Произошла ошибка при поиске расписания"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateSpecialityRequest;
|
||||
import com.magistr.app.dto.SpecialityResponse;
|
||||
import com.magistr.app.dto.SpecialtyProfileDto;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.Speciality;
|
||||
import com.magistr.app.model.SpecialtyProfile;
|
||||
import com.magistr.app.repository.SpecialtiesRepository;
|
||||
@@ -18,6 +21,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/specialties")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class SpecialityController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SpecialityController.class);
|
||||
@@ -32,10 +36,12 @@ public class SpecialityController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Speciality> getAllSpecialties() {
|
||||
public List<Speciality> getAllSpecialties(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
logger.info("Получен запрос на получение списка специальностей");
|
||||
try {
|
||||
List<Speciality> specialities = specialtiesRepository.findAll();
|
||||
List<Speciality> specialities = includeArchived
|
||||
? specialtiesRepository.findAll()
|
||||
: specialtiesRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
List<Speciality> response = specialities.stream()
|
||||
.map( s -> new Speciality(
|
||||
s.getId(),
|
||||
@@ -161,13 +167,30 @@ public class SpecialityController {
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteSpeciality(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление специальности с ID: {}", id);
|
||||
if (!specialtiesRepository.existsById(id)) {
|
||||
Speciality speciality = specialtiesRepository.findById(id).orElse(null);
|
||||
if (speciality == null) {
|
||||
logger.info("Специальность с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
specialtiesRepository.deleteById(id);
|
||||
logger.info("Специальность с ID - {} успешно удалена", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Специальность удалена"));
|
||||
speciality.archive("Специальность архивирована");
|
||||
specialtiesRepository.save(speciality);
|
||||
logger.info("Специальность с ID - {} успешно архивирована", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Специальность архивирована"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
public ResponseEntity<?> restoreSpeciality(@PathVariable Long id) {
|
||||
Speciality speciality = specialtiesRepository.findById(id).orElse(null);
|
||||
if (speciality == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
speciality.restore();
|
||||
specialtiesRepository.save(speciality);
|
||||
return ResponseEntity.ok(new SpecialityResponse(
|
||||
speciality.getId(),
|
||||
speciality.getSpecialityName(),
|
||||
speciality.getSpecialityCode()
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/{specialtyId}/profiles")
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.SubgroupDto;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.StudentGroup;
|
||||
import com.magistr.app.model.Subgroup;
|
||||
import com.magistr.app.repository.GroupRepository;
|
||||
@@ -15,6 +17,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.STUDENT})
|
||||
public class SubgroupController {
|
||||
|
||||
private final SubgroupRepository subgroupRepository;
|
||||
@@ -50,6 +53,7 @@ public class SubgroupController {
|
||||
}
|
||||
|
||||
@PostMapping("/api/groups/{groupId}/subgroups")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> create(@PathVariable Long groupId, @RequestBody SubgroupDto request) {
|
||||
StudentGroup group = groupRepository.findById(groupId).orElse(null);
|
||||
if (group == null) {
|
||||
@@ -73,6 +77,7 @@ public class SubgroupController {
|
||||
}
|
||||
|
||||
@PutMapping("/api/groups/{groupId}/subgroups/{id}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> update(@PathVariable Long groupId,
|
||||
@PathVariable Long id,
|
||||
@RequestBody SubgroupDto request) {
|
||||
@@ -99,6 +104,7 @@ public class SubgroupController {
|
||||
}
|
||||
|
||||
@DeleteMapping("/api/groups/{groupId}/subgroups/{id}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public ResponseEntity<?> delete(@PathVariable Long groupId, @PathVariable Long id) {
|
||||
Subgroup subgroup = subgroupRepository.findByIdAndStudentGroupId(id, groupId).orElse(null);
|
||||
if (subgroup == null) {
|
||||
@@ -108,9 +114,10 @@ public class SubgroupController {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа используется в расписании"));
|
||||
}
|
||||
try {
|
||||
subgroupRepository.delete(subgroup);
|
||||
subgroup.archive("Подгруппа архивирована");
|
||||
subgroupRepository.save(subgroup);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(Map.of("message", "Подгруппа удалена"));
|
||||
return ResponseEntity.ok(Map.of("message", "Подгруппа архивирована"));
|
||||
} catch (DataIntegrityViolationException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа используется в расписании"));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateSubjectRequest;
|
||||
import com.magistr.app.dto.SubjectResponse;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.Subject;
|
||||
import com.magistr.app.repository.SubjectRepository;
|
||||
import org.slf4j.Logger;
|
||||
@@ -16,6 +19,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/subjects")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class SubjectController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(SubjectController.class);
|
||||
@@ -27,10 +31,13 @@ public class SubjectController {
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<Subject> getAllSubjects() {
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public List<Subject> getAllSubjects(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
logger.info("Получен запрос на получение всех дисциплин");
|
||||
try {
|
||||
List<Subject> subjects = subjectRepository.findAll();
|
||||
List<Subject> subjects = includeArchived
|
||||
? subjectRepository.findAll()
|
||||
: subjectRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
List<Subject> response = subjects.stream()
|
||||
.map(s -> new Subject(
|
||||
s.getId(),
|
||||
@@ -48,10 +55,11 @@ public class SubjectController {
|
||||
}
|
||||
|
||||
@GetMapping("/{departmentId}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public ResponseEntity<?> getSubjectsByDepartmentId(@PathVariable Long departmentId) {
|
||||
logger.info("Получен запрос на получение дисциплин для кафедры с ID - {}", departmentId);
|
||||
try{
|
||||
List<Subject> subjects = subjectRepository.findByDepartmentId(departmentId);
|
||||
List<Subject> subjects = subjectRepository.findByDepartmentIdAndStatusNot(departmentId, LifecycleEntity.STATUS_ARCHIVED);
|
||||
|
||||
if(subjects.isEmpty()){
|
||||
logger.info("Дисциплины для кафедры с ID - {} не найдены", departmentId);
|
||||
@@ -121,12 +129,31 @@ public class SubjectController {
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteSubject(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление дисциплины с ID: {}", id);
|
||||
if (!subjectRepository.existsById(id)) {
|
||||
Subject subject = subjectRepository.findById(id).orElse(null);
|
||||
if (subject == null) {
|
||||
logger.info("Дисциплина с ID - {} не найдена", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
subjectRepository.deleteById(id);
|
||||
logger.info("Дисциплина с ID - {} успешно удалена", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Дисциплина удалена"));
|
||||
subject.archive("Дисциплина архивирована");
|
||||
subjectRepository.save(subject);
|
||||
logger.info("Дисциплина с ID - {} успешно архивирована", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Дисциплина архивирована"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public ResponseEntity<?> restoreSubject(@PathVariable Long id) {
|
||||
Subject subject = subjectRepository.findById(id).orElse(null);
|
||||
if (subject == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
subject.restore();
|
||||
subjectRepository.save(subject);
|
||||
return ResponseEntity.ok(new SubjectResponse(
|
||||
subject.getId(),
|
||||
subject.getName(),
|
||||
subject.getCode(),
|
||||
subject.getDepartmentId()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.TeacherSubjectResponse;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.Subject;
|
||||
import com.magistr.app.model.TeacherSubject;
|
||||
import com.magistr.app.model.TeacherSubjectId;
|
||||
import com.magistr.app.model.User;
|
||||
import com.magistr.app.repository.SubjectRepository;
|
||||
import com.magistr.app.repository.TeacherSubjectRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
@@ -14,6 +19,7 @@ import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/teacher-subjects")
|
||||
@RequireRoles({Role.ADMIN, Role.DEPARTMENT})
|
||||
public class TeacherSubjectController {
|
||||
|
||||
private final TeacherSubjectRepository teacherSubjectRepository;
|
||||
@@ -31,6 +37,7 @@ public class TeacherSubjectController {
|
||||
@GetMapping
|
||||
public List<TeacherSubjectResponse> getAll() {
|
||||
return teacherSubjectRepository.findAll().stream()
|
||||
.filter(ts -> canManage(ts.getUser(), ts.getSubject()))
|
||||
.map(ts -> new TeacherSubjectResponse(
|
||||
ts.getUserId(),
|
||||
ts.getUser().getUsername(),
|
||||
@@ -48,12 +55,17 @@ public class TeacherSubjectController {
|
||||
if (userId == null || subjectId == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "userId и subjectId обязательны"));
|
||||
}
|
||||
if (!userRepository.existsById(userId)) {
|
||||
User user = userRepository.findById(userId).orElse(null);
|
||||
if (user == null || user.getRole() != Role.TEACHER || user.isArchivedRecord()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Преподаватель не найден"));
|
||||
}
|
||||
if (!subjectRepository.existsById(subjectId)) {
|
||||
Subject subject = subjectRepository.findById(subjectId).orElse(null);
|
||||
if (subject == null || subject.isArchivedRecord()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Дисциплина не найдена"));
|
||||
}
|
||||
if (!canManage(user, subject)) {
|
||||
return ResponseEntity.status(403).body(Map.of("message", "Кафедра может связывать только своих преподавателей и свои дисциплины"));
|
||||
}
|
||||
|
||||
TeacherSubjectId id = new TeacherSubjectId(userId, subjectId);
|
||||
if (teacherSubjectRepository.existsById(id)) {
|
||||
@@ -76,10 +88,30 @@ public class TeacherSubjectController {
|
||||
}
|
||||
|
||||
TeacherSubjectId id = new TeacherSubjectId(userId, subjectId);
|
||||
if (!teacherSubjectRepository.existsById(id)) {
|
||||
TeacherSubject teacherSubject = teacherSubjectRepository.findById(id).orElse(null);
|
||||
if (teacherSubject == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
if (!canManage(teacherSubject.getUser(), teacherSubject.getSubject())) {
|
||||
return ResponseEntity.status(403).body(Map.of("message", "Кафедра может удалять только свои привязки"));
|
||||
}
|
||||
teacherSubjectRepository.deleteById(id);
|
||||
return ResponseEntity.ok(Map.of("message", "Привязка удалена"));
|
||||
}
|
||||
|
||||
private boolean canManage(User teacher, Subject subject) {
|
||||
var currentUser = AuthContext.getCurrentUser();
|
||||
if (currentUser == null || currentUser.role() == Role.ADMIN) {
|
||||
return true;
|
||||
}
|
||||
if (currentUser.role() != Role.DEPARTMENT) {
|
||||
return false;
|
||||
}
|
||||
Long departmentId = currentUser.departmentId();
|
||||
return departmentId != null
|
||||
&& teacher != null
|
||||
&& subject != null
|
||||
&& departmentId.equals(teacher.getDepartmentId())
|
||||
&& departmentId.equals(subject.getDepartmentId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.TimeSlotDateAssignmentDto;
|
||||
import com.magistr.app.dto.TimeSlotDto;
|
||||
import com.magistr.app.dto.TimeSlotScopeDto;
|
||||
import com.magistr.app.model.TimeSlot;
|
||||
import com.magistr.app.model.TimeSlotDateAssignment;
|
||||
import com.magistr.app.model.TimeSlotScope;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.repository.TimeSlotDateAssignmentRepository;
|
||||
import com.magistr.app.repository.TimeSlotRepository;
|
||||
import com.magistr.app.repository.TimeSlotScopeRepository;
|
||||
@@ -23,6 +25,7 @@ import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/time-slots")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
|
||||
public class TimeSlotAdminController {
|
||||
|
||||
private static final String APPLY_MODE_DEFAULT = "DEFAULT";
|
||||
|
||||
@@ -1,43 +1,61 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.AuthContext;
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.CreateUserRequest;
|
||||
import com.magistr.app.dto.DepartmentTransferRequest;
|
||||
import com.magistr.app.dto.TeacherDepartmentAssignmentDto;
|
||||
import com.magistr.app.dto.UserResponse;
|
||||
import com.magistr.app.model.Department;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.TeacherDepartmentAssignment;
|
||||
import com.magistr.app.model.User;
|
||||
import com.magistr.app.repository.DepartmentRepository;
|
||||
import com.magistr.app.repository.TeacherDepartmentAssignmentRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@RequireRoles({Role.ADMIN})
|
||||
public class UserController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(UserController.class);
|
||||
private final UserRepository userRepository;
|
||||
private final DepartmentRepository departmentRepository;
|
||||
private final TeacherDepartmentAssignmentRepository teacherDepartmentAssignmentRepository;
|
||||
private final BCryptPasswordEncoder passwordEncoder;
|
||||
|
||||
public UserController(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder, DepartmentRepository departmentRepository) {
|
||||
public UserController(UserRepository userRepository,
|
||||
BCryptPasswordEncoder passwordEncoder,
|
||||
DepartmentRepository departmentRepository,
|
||||
TeacherDepartmentAssignmentRepository teacherDepartmentAssignmentRepository) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.departmentRepository = departmentRepository;
|
||||
this.teacherDepartmentAssignmentRepository = teacherDepartmentAssignmentRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<UserResponse> getAllUsers() {
|
||||
public List<UserResponse> getAllUsers(@RequestParam(defaultValue = "false") boolean includeArchived) {
|
||||
logger.info("Получен запрос на получение всех пользователей");
|
||||
try {
|
||||
List<User> users = userRepository.findAll();
|
||||
List<User> users = includeArchived
|
||||
? userRepository.findAll()
|
||||
: userRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
|
||||
List<UserResponse> response = users.stream()
|
||||
.map(u -> {
|
||||
@@ -45,13 +63,15 @@ public class UserController {
|
||||
.map(Department::getDepartmentName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
return new UserResponse(
|
||||
UserResponse userResponse = new UserResponse(
|
||||
u.getId(),
|
||||
u.getUsername(),
|
||||
u.getRole().name(),
|
||||
u.getFullName(),
|
||||
u.getJobTitle(),
|
||||
departmentName);
|
||||
userResponse.setStatus(u.getStatus());
|
||||
return userResponse;
|
||||
})
|
||||
.toList();
|
||||
logger.info("Получено {} пользователей", response.size());
|
||||
@@ -64,11 +84,12 @@ public class UserController {
|
||||
}
|
||||
|
||||
@GetMapping("/teachers")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public List<UserResponse> getTeachers() {
|
||||
logger.info("Запрос на получение пользователей с ролью 'Преподаватель'");
|
||||
|
||||
try {
|
||||
List<User> users = userRepository.findByRole(Role.TEACHER);
|
||||
List<User> users = userRepository.findByRoleAndStatusNot(Role.TEACHER, LifecycleEntity.STATUS_ARCHIVED);
|
||||
|
||||
List<UserResponse> response = users.stream()
|
||||
.map(u -> {
|
||||
@@ -76,13 +97,15 @@ public class UserController {
|
||||
.map(Department::getDepartmentName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
return new UserResponse(
|
||||
UserResponse userResponse = new UserResponse(
|
||||
u.getId(),
|
||||
u.getUsername(),
|
||||
u.getRole().name(),
|
||||
u.getFullName(),
|
||||
u.getJobTitle(),
|
||||
departmentName);
|
||||
userResponse.setStatus(u.getStatus());
|
||||
return userResponse;
|
||||
})
|
||||
.toList();
|
||||
logger.info("Получено {} преподавателей", response.size());
|
||||
@@ -94,10 +117,15 @@ public class UserController {
|
||||
}
|
||||
|
||||
@GetMapping("/teachers/{departmentId}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public ResponseEntity<?> getTeachersByDepartmentId(@PathVariable Long departmentId){
|
||||
logger.info("Получен запрос на получение преподавателей для кафедры с ID - {}", departmentId);
|
||||
try {
|
||||
List<User> users = userRepository.findByRoleAndDepartmentId(Role.TEACHER, departmentId);
|
||||
List<User> users = userRepository.findByRoleAndDepartmentIdAndStatusNot(
|
||||
Role.TEACHER,
|
||||
departmentId,
|
||||
LifecycleEntity.STATUS_ARCHIVED
|
||||
);
|
||||
|
||||
if (users.isEmpty()) {
|
||||
logger.info("Преподаватели для кафедры с ID - {} не найдены", departmentId);
|
||||
@@ -178,6 +206,19 @@ public class UserController {
|
||||
user.setJobTitle(request.getJobTitle());
|
||||
user.setDepartmentId(request.getDepartmentId());
|
||||
userRepository.save(user);
|
||||
if (role == Role.TEACHER) {
|
||||
Department department = departmentRepository.findById(user.getDepartmentId()).orElse(null);
|
||||
if (department != null) {
|
||||
TeacherDepartmentAssignment assignment = new TeacherDepartmentAssignment();
|
||||
assignment.setTeacher(user);
|
||||
assignment.setDepartment(department);
|
||||
assignment.setValidFrom(LocalDate.now());
|
||||
assignment.setPrimaryAssignment(true);
|
||||
assignment.setComment("Начальная кафедра при создании пользователя");
|
||||
assignment.setCreatedBy(AuthContext.currentUserId());
|
||||
teacherDepartmentAssignmentRepository.save(assignment);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Пользователь успешно создан с ID: {}", user.getId());
|
||||
|
||||
@@ -192,12 +233,111 @@ public class UserController {
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
|
||||
logger.info("Получен запрос на удаление пользователя с ID: {}", id);
|
||||
if (!userRepository.existsById(id)) {
|
||||
User user = userRepository.findById(id).orElse(null);
|
||||
if (user == null) {
|
||||
logger.info("Пользователь с ID - {} не найден", id);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
userRepository.deleteById(id);
|
||||
logger.info("Пользователь с ID - {} успешно удалён", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Пользователь удалён"));
|
||||
user.archive("Пользователь архивирован");
|
||||
userRepository.save(user);
|
||||
logger.info("Пользователь с ID - {} успешно архивирован", id);
|
||||
return ResponseEntity.ok(Map.of("message", "Пользователь архивирован"));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/restore")
|
||||
public ResponseEntity<?> restoreUser(@PathVariable Long id) {
|
||||
User user = userRepository.findById(id).orElse(null);
|
||||
if (user == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
user.restore();
|
||||
userRepository.save(user);
|
||||
return ResponseEntity.ok(new UserResponse(user.getId(), user.getUsername(), user.getRole().name(),
|
||||
user.getFullName(), user.getJobTitle(), user.getDepartmentId()));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/department-history")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT})
|
||||
public ResponseEntity<?> getDepartmentHistory(@PathVariable Long id) {
|
||||
if (!userRepository.existsById(id)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(teacherDepartmentAssignmentRepository.findHistoryByTeacherId(id).stream()
|
||||
.map(this::toAssignmentDto)
|
||||
.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/department-transfer")
|
||||
@Transactional
|
||||
public ResponseEntity<?> transferTeacher(@PathVariable Long id, @RequestBody DepartmentTransferRequest request) {
|
||||
User teacher = userRepository.findById(id).orElse(null);
|
||||
if (teacher == null || teacher.getRole() != Role.TEACHER) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Преподаватель не найден"));
|
||||
}
|
||||
if (teacher.isArchivedRecord()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Архивного преподавателя нельзя перевести"));
|
||||
}
|
||||
if (request.departmentId() == null) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Кафедра обязательна"));
|
||||
}
|
||||
Department department = departmentRepository.findById(request.departmentId()).orElse(null);
|
||||
if (department == null || department.isArchivedRecord()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Активная кафедра не найдена"));
|
||||
}
|
||||
LocalDate validFrom = request.validFrom() == null ? LocalDate.now() : request.validFrom();
|
||||
|
||||
teacherDepartmentAssignmentRepository.findOpenPrimaryByTeacherId(id)
|
||||
.ifPresent(current -> {
|
||||
if (current.getValidFrom().isBefore(validFrom)) {
|
||||
current.setValidTo(validFrom.minusDays(1));
|
||||
teacherDepartmentAssignmentRepository.save(current);
|
||||
} else {
|
||||
teacherDepartmentAssignmentRepository.delete(current);
|
||||
}
|
||||
});
|
||||
|
||||
TeacherDepartmentAssignment assignment = new TeacherDepartmentAssignment();
|
||||
assignment.setTeacher(teacher);
|
||||
assignment.setDepartment(department);
|
||||
assignment.setValidFrom(validFrom);
|
||||
assignment.setPrimaryAssignment(true);
|
||||
assignment.setComment(request.comment());
|
||||
assignment.setCreatedBy(AuthContext.currentUserId());
|
||||
teacherDepartmentAssignmentRepository.save(assignment);
|
||||
|
||||
teacher.setDepartmentId(department.getId());
|
||||
userRepository.save(teacher);
|
||||
|
||||
return ResponseEntity.ok(toAssignmentDto(assignment));
|
||||
}
|
||||
|
||||
@GetMapping("/teachers/by-department/{departmentId}")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT})
|
||||
public ResponseEntity<?> getTeachersByDepartmentAtDate(@PathVariable Long departmentId,
|
||||
@RequestParam(required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
|
||||
LocalDate effectiveDate = date == null ? LocalDate.now() : date;
|
||||
return ResponseEntity.ok(teacherDepartmentAssignmentRepository
|
||||
.findDepartmentTeachersAtDate(departmentId, effectiveDate)
|
||||
.stream()
|
||||
.map(TeacherDepartmentAssignment::getTeacher)
|
||||
.filter(User::isActiveRecord)
|
||||
.map(user -> new UserResponse(user.getId(), user.getUsername(), user.getRole().name(),
|
||||
user.getFullName(), user.getJobTitle(), user.getDepartmentId()))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private TeacherDepartmentAssignmentDto toAssignmentDto(TeacherDepartmentAssignment assignment) {
|
||||
return new TeacherDepartmentAssignmentDto(
|
||||
assignment.getId(),
|
||||
assignment.getTeacher().getId(),
|
||||
assignment.getTeacher().getFullName(),
|
||||
assignment.getDepartment().getId(),
|
||||
assignment.getDepartment().getDepartmentName(),
|
||||
assignment.getValidFrom(),
|
||||
assignment.getValidTo(),
|
||||
assignment.getPrimaryAssignment(),
|
||||
assignment.getComment()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.config.auth.RequireRoles;
|
||||
import com.magistr.app.dto.ClassroomResponse;
|
||||
import com.magistr.app.dto.RenderedLessonDto;
|
||||
import com.magistr.app.dto.WorkloadSummaryDto;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.ClassroomRepository;
|
||||
import com.magistr.app.repository.DepartmentRepository;
|
||||
import com.magistr.app.repository.UserRepository;
|
||||
import com.magistr.app.service.ScheduleQueryService;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/workload")
|
||||
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE, Role.DEPARTMENT, Role.SCHEDULE_VIEWER})
|
||||
public class WorkloadController {
|
||||
|
||||
private static final int ACADEMIC_HOURS_PER_LESSON = 2;
|
||||
|
||||
private final ScheduleQueryService scheduleQueryService;
|
||||
private final ClassroomRepository classroomRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final DepartmentRepository departmentRepository;
|
||||
|
||||
public WorkloadController(ScheduleQueryService scheduleQueryService,
|
||||
ClassroomRepository classroomRepository,
|
||||
UserRepository userRepository,
|
||||
DepartmentRepository departmentRepository) {
|
||||
this.scheduleQueryService = scheduleQueryService;
|
||||
this.classroomRepository = classroomRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.departmentRepository = departmentRepository;
|
||||
}
|
||||
|
||||
@GetMapping("/teachers")
|
||||
public List<WorkloadSummaryDto> teacherWorkload(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
|
||||
@RequestParam(required = false) Long departmentId
|
||||
) {
|
||||
List<RenderedLessonDto> lessons = scheduleQueryService.search(null, null, null, departmentId,
|
||||
null, null, null, null, startDate, endDate);
|
||||
Map<Long, User> teachersById = userRepository.findAllById(lessons.stream()
|
||||
.map(RenderedLessonDto::teacherId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet()))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(User::getId, Function.identity()));
|
||||
return summarize(lessons, RenderedLessonDto::teacherId, RenderedLessonDto::teacherName,
|
||||
teacherId -> {
|
||||
User teacher = teachersById.get(teacherId);
|
||||
return teacher == null ? null : teacher.getDepartmentId();
|
||||
});
|
||||
}
|
||||
|
||||
@GetMapping("/classrooms")
|
||||
public List<WorkloadSummaryDto> classroomWorkload(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
|
||||
@RequestParam(required = false) Long departmentId
|
||||
) {
|
||||
List<RenderedLessonDto> lessons = scheduleQueryService.search(null, null, null, departmentId,
|
||||
null, null, null, null, startDate, endDate);
|
||||
return summarize(lessons, RenderedLessonDto::classroomId, RenderedLessonDto::classroomName, ignored -> null);
|
||||
}
|
||||
|
||||
@GetMapping("/departments")
|
||||
public List<WorkloadSummaryDto> departmentWorkload(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
|
||||
@RequestParam(required = false) Long departmentId
|
||||
) {
|
||||
List<RenderedLessonDto> lessons = scheduleQueryService.search(null, null, null, departmentId,
|
||||
null, null, null, null, startDate, endDate);
|
||||
Map<Long, String> departments = departmentRepository.findAll().stream()
|
||||
.collect(Collectors.toMap(Department::getId, Department::getDepartmentName));
|
||||
Map<Long, User> teachersById = userRepository.findAllById(lessons.stream()
|
||||
.map(RenderedLessonDto::teacherId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet()))
|
||||
.stream()
|
||||
.collect(Collectors.toMap(User::getId, Function.identity()));
|
||||
|
||||
Map<Long, List<RenderedLessonDto>> byDepartment = lessons.stream()
|
||||
.collect(Collectors.groupingBy(lesson -> {
|
||||
User teacher = teachersById.get(lesson.teacherId());
|
||||
return teacher == null ? 0L : teacher.getDepartmentId();
|
||||
}));
|
||||
|
||||
return byDepartment.entrySet().stream()
|
||||
.map(entry -> summary(entry.getKey(), departments.getOrDefault(entry.getKey(), "Кафедра не указана"), entry.getKey(), entry.getValue()))
|
||||
.sorted(Comparator.comparing(WorkloadSummaryDto::name, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping("/time-slots")
|
||||
public List<WorkloadSummaryDto> timeSlotWorkload(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
|
||||
@RequestParam(required = false) Long departmentId
|
||||
) {
|
||||
List<RenderedLessonDto> lessons = scheduleQueryService.search(null, null, null, departmentId,
|
||||
null, null, null, null, startDate, endDate);
|
||||
return summarize(lessons, RenderedLessonDto::timeSlotId,
|
||||
lesson -> lesson.timeSlotOrder() == null
|
||||
? "Пара"
|
||||
: lesson.timeSlotOrder() + " пара (" + lesson.startTime() + " - " + lesson.endTime() + ")",
|
||||
ignored -> null);
|
||||
}
|
||||
|
||||
@GetMapping("/free-classrooms")
|
||||
public List<ClassroomResponse> freeClassrooms(
|
||||
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date,
|
||||
@RequestParam Long timeSlotId
|
||||
) {
|
||||
Set<Long> busyClassroomIds = scheduleQueryService.search(null, null, null, null,
|
||||
null, null, timeSlotId, null, date, date)
|
||||
.stream()
|
||||
.map(RenderedLessonDto::classroomId)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
return classroomRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED).stream()
|
||||
.filter(Classroom::getIsAvailable)
|
||||
.filter(classroom -> !busyClassroomIds.contains(classroom.getId()))
|
||||
.map(this::toClassroomResponse)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private List<WorkloadSummaryDto> summarize(List<RenderedLessonDto> lessons,
|
||||
Function<RenderedLessonDto, Long> idFn,
|
||||
Function<RenderedLessonDto, String> nameFn,
|
||||
Function<Long, Long> departmentFn) {
|
||||
Map<Long, List<RenderedLessonDto>> grouped = lessons.stream()
|
||||
.filter(lesson -> idFn.apply(lesson) != null)
|
||||
.collect(Collectors.groupingBy(idFn, LinkedHashMap::new, Collectors.toList()));
|
||||
return grouped.entrySet().stream()
|
||||
.map(entry -> summary(entry.getKey(), nameFn.apply(entry.getValue().get(0)), departmentFn.apply(entry.getKey()), entry.getValue()))
|
||||
.sorted(Comparator.comparing(WorkloadSummaryDto::name, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private WorkloadSummaryDto summary(Long id, String name, Long departmentId, List<RenderedLessonDto> lessons) {
|
||||
return new WorkloadSummaryDto(
|
||||
id,
|
||||
name,
|
||||
departmentId,
|
||||
departmentName(departmentId),
|
||||
lessons.size(),
|
||||
lessons.size() * ACADEMIC_HOURS_PER_LESSON,
|
||||
lessons.stream()
|
||||
.map(lesson -> lesson.date() + ":" + lesson.timeSlotId())
|
||||
.distinct()
|
||||
.count()
|
||||
);
|
||||
}
|
||||
|
||||
private String departmentName(Long departmentId) {
|
||||
if (departmentId == null) {
|
||||
return null;
|
||||
}
|
||||
return departmentRepository.findById(departmentId)
|
||||
.map(Department::getDepartmentName)
|
||||
.orElse("Кафедра не найдена");
|
||||
}
|
||||
|
||||
private ClassroomResponse toClassroomResponse(Classroom classroom) {
|
||||
return new ClassroomResponse(
|
||||
classroom.getId(),
|
||||
classroom.getName(),
|
||||
classroom.getCapacity(),
|
||||
classroom.getBuilding(),
|
||||
classroom.getFloor(),
|
||||
classroom.getIsAvailable(),
|
||||
classroom.getStatus(),
|
||||
classroom.getArchivedAt(),
|
||||
classroom.getArchiveReason(),
|
||||
new ArrayList<>(classroom.getEquipments())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import com.magistr.app.model.Equipment;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public class ClassroomResponse {
|
||||
@@ -10,6 +11,9 @@ public class ClassroomResponse {
|
||||
private String building;
|
||||
private Integer floor;
|
||||
private Boolean isAvailable;
|
||||
private String status;
|
||||
private LocalDateTime archivedAt;
|
||||
private String archiveReason;
|
||||
private List<Equipment> equipments;
|
||||
|
||||
public ClassroomResponse() {
|
||||
@@ -17,12 +21,21 @@ public class ClassroomResponse {
|
||||
|
||||
public ClassroomResponse(Long id, String name, Integer capacity, String building, Integer floor,
|
||||
Boolean isAvailable, List<Equipment> equipments) {
|
||||
this(id, name, capacity, building, floor, isAvailable, null, null, null, equipments);
|
||||
}
|
||||
|
||||
public ClassroomResponse(Long id, String name, Integer capacity, String building, Integer floor,
|
||||
Boolean isAvailable, String status, LocalDateTime archivedAt,
|
||||
String archiveReason, List<Equipment> equipments) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.capacity = capacity;
|
||||
this.building = building;
|
||||
this.floor = floor;
|
||||
this.isAvailable = isAvailable;
|
||||
this.status = status;
|
||||
this.archivedAt = archivedAt;
|
||||
this.archiveReason = archiveReason;
|
||||
this.equipments = equipments;
|
||||
}
|
||||
|
||||
@@ -74,6 +87,30 @@ public class ClassroomResponse {
|
||||
this.isAvailable = isAvailable;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getArchivedAt() {
|
||||
return archivedAt;
|
||||
}
|
||||
|
||||
public void setArchivedAt(LocalDateTime archivedAt) {
|
||||
this.archivedAt = archivedAt;
|
||||
}
|
||||
|
||||
public String getArchiveReason() {
|
||||
return archiveReason;
|
||||
}
|
||||
|
||||
public void setArchiveReason(String archiveReason) {
|
||||
this.archiveReason = archiveReason;
|
||||
}
|
||||
|
||||
public List<Equipment> getEquipments() {
|
||||
return equipments;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record DepartmentTransferRequest(
|
||||
Long departmentId,
|
||||
LocalDate validFrom,
|
||||
String comment
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record ScheduleOverrideDto(
|
||||
Long id,
|
||||
Long baseRuleSlotId,
|
||||
LocalDate lessonDate,
|
||||
String action,
|
||||
Long newTimeSlotId,
|
||||
Long newClassroomId,
|
||||
Long newTeacherId,
|
||||
String newLessonFormat,
|
||||
String comment,
|
||||
Long createdBy,
|
||||
LocalDateTime createdAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public record SubjectCommentDto(
|
||||
Long id,
|
||||
Long subjectId,
|
||||
Long authorId,
|
||||
String authorName,
|
||||
String comment,
|
||||
LocalDateTime createdAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
public record TeacherDepartmentAssignmentDto(
|
||||
Long id,
|
||||
Long teacherId,
|
||||
String teacherName,
|
||||
Long departmentId,
|
||||
String departmentName,
|
||||
LocalDate validFrom,
|
||||
LocalDate validTo,
|
||||
Boolean primaryAssignment,
|
||||
String comment
|
||||
) {
|
||||
}
|
||||
@@ -12,6 +12,7 @@ public class UserResponse {
|
||||
private String jobTitle;
|
||||
private String departmentName;
|
||||
private Long departmentId;
|
||||
private String status;
|
||||
|
||||
public UserResponse() {
|
||||
}
|
||||
@@ -69,4 +70,12 @@ public class UserResponse {
|
||||
public Long getDepartmentId() {
|
||||
return departmentId;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
public record WorkloadSummaryDto(
|
||||
Long id,
|
||||
String name,
|
||||
Long departmentId,
|
||||
String departmentName,
|
||||
long lessonCount,
|
||||
int academicHours,
|
||||
long occupiedSlotCount
|
||||
) {
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import java.util.Set;
|
||||
|
||||
@Entity
|
||||
@Table(name = "classrooms")
|
||||
public class Classroom {
|
||||
public class Classroom extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name="departments")
|
||||
public class Department {
|
||||
public class Department extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "education_forms")
|
||||
public class EducationForm {
|
||||
public class EducationForm extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "equipments")
|
||||
public class Equipment {
|
||||
public class Equipment extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
104
backend/src/main/java/com/magistr/app/model/LifecycleEntity.java
Normal file
104
backend/src/main/java/com/magistr/app/model/LifecycleEntity.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@MappedSuperclass
|
||||
public abstract class LifecycleEntity {
|
||||
|
||||
public static final String STATUS_ACTIVE = "ACTIVE";
|
||||
public static final String STATUS_ARCHIVED = "ARCHIVED";
|
||||
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status = STATUS_ACTIVE;
|
||||
|
||||
@Column(name = "active_from")
|
||||
private LocalDate activeFrom;
|
||||
|
||||
@Column(name = "active_to")
|
||||
private LocalDate activeTo;
|
||||
|
||||
@Column(name = "archived_at")
|
||||
private LocalDateTime archivedAt;
|
||||
|
||||
@Column(name = "archive_reason")
|
||||
private String archiveReason;
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status == null || status.isBlank() ? STATUS_ACTIVE : status;
|
||||
}
|
||||
|
||||
public LocalDate getActiveFrom() {
|
||||
return activeFrom;
|
||||
}
|
||||
|
||||
public void setActiveFrom(LocalDate activeFrom) {
|
||||
this.activeFrom = activeFrom;
|
||||
}
|
||||
|
||||
public LocalDate getActiveTo() {
|
||||
return activeTo;
|
||||
}
|
||||
|
||||
public void setActiveTo(LocalDate activeTo) {
|
||||
this.activeTo = activeTo;
|
||||
}
|
||||
|
||||
public LocalDateTime getArchivedAt() {
|
||||
return archivedAt;
|
||||
}
|
||||
|
||||
public void setArchivedAt(LocalDateTime archivedAt) {
|
||||
this.archivedAt = archivedAt;
|
||||
}
|
||||
|
||||
public String getArchiveReason() {
|
||||
return archiveReason;
|
||||
}
|
||||
|
||||
public void setArchiveReason(String archiveReason) {
|
||||
this.archiveReason = archiveReason;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isActiveRecord() {
|
||||
return !STATUS_ARCHIVED.equals(status);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isArchivedRecord() {
|
||||
return STATUS_ARCHIVED.equals(status);
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public boolean isActiveOn(LocalDate date) {
|
||||
if (date == null) {
|
||||
return isActiveRecord();
|
||||
}
|
||||
boolean afterStart = activeFrom == null || !date.isBefore(activeFrom);
|
||||
boolean beforeEnd = activeTo == null || !date.isAfter(activeTo);
|
||||
return afterStart && beforeEnd;
|
||||
}
|
||||
|
||||
public void archive(String reason) {
|
||||
status = STATUS_ARCHIVED;
|
||||
archivedAt = LocalDateTime.now();
|
||||
activeTo = LocalDate.now();
|
||||
archiveReason = reason == null || reason.isBlank() ? "Архивировано пользователем" : reason.trim();
|
||||
}
|
||||
|
||||
public void restore() {
|
||||
status = STATUS_ACTIVE;
|
||||
archivedAt = null;
|
||||
archiveReason = null;
|
||||
activeTo = null;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package com.magistr.app.model;
|
||||
|
||||
public enum Role {
|
||||
ADMIN,
|
||||
EDUCATION_OFFICE,
|
||||
DEPARTMENT,
|
||||
SCHEDULE_VIEWER,
|
||||
TEACHER,
|
||||
STUDENT
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "schedule_overrides")
|
||||
public class ScheduleOverride {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "base_rule_slot_id", nullable = false)
|
||||
private ScheduleRuleSlot baseRuleSlot;
|
||||
|
||||
@Column(name = "lesson_date", nullable = false)
|
||||
private LocalDate lessonDate;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String action;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "new_time_slot_id")
|
||||
private TimeSlot newTimeSlot;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "new_classroom_id")
|
||||
private Classroom newClassroom;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "new_teacher_id")
|
||||
private User newTeacher;
|
||||
|
||||
@Column(name = "new_lesson_format", length = 30)
|
||||
private String newLessonFormat;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String comment;
|
||||
|
||||
@Column(name = "created_by")
|
||||
private Long createdBy;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public ScheduleRuleSlot getBaseRuleSlot() {
|
||||
return baseRuleSlot;
|
||||
}
|
||||
|
||||
public void setBaseRuleSlot(ScheduleRuleSlot baseRuleSlot) {
|
||||
this.baseRuleSlot = baseRuleSlot;
|
||||
}
|
||||
|
||||
public LocalDate getLessonDate() {
|
||||
return lessonDate;
|
||||
}
|
||||
|
||||
public void setLessonDate(LocalDate lessonDate) {
|
||||
this.lessonDate = lessonDate;
|
||||
}
|
||||
|
||||
public String getAction() {
|
||||
return action;
|
||||
}
|
||||
|
||||
public void setAction(String action) {
|
||||
this.action = action;
|
||||
}
|
||||
|
||||
public TimeSlot getNewTimeSlot() {
|
||||
return newTimeSlot;
|
||||
}
|
||||
|
||||
public void setNewTimeSlot(TimeSlot newTimeSlot) {
|
||||
this.newTimeSlot = newTimeSlot;
|
||||
}
|
||||
|
||||
public Classroom getNewClassroom() {
|
||||
return newClassroom;
|
||||
}
|
||||
|
||||
public void setNewClassroom(Classroom newClassroom) {
|
||||
this.newClassroom = newClassroom;
|
||||
}
|
||||
|
||||
public User getNewTeacher() {
|
||||
return newTeacher;
|
||||
}
|
||||
|
||||
public void setNewTeacher(User newTeacher) {
|
||||
this.newTeacher = newTeacher;
|
||||
}
|
||||
|
||||
public String getNewLessonFormat() {
|
||||
return newLessonFormat;
|
||||
}
|
||||
|
||||
public void setNewLessonFormat(String newLessonFormat) {
|
||||
this.newLessonFormat = newLessonFormat;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
public void setComment(String comment) {
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public Long getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(Long createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -21,6 +22,15 @@ public class ScheduleRule {
|
||||
@JoinColumn(name = "semester_id", nullable = false)
|
||||
private Semester semester;
|
||||
|
||||
@Column(nullable = false, length = 20)
|
||||
private String status = LifecycleEntity.STATUS_ACTIVE;
|
||||
|
||||
@Column(name = "valid_from")
|
||||
private LocalDate validFrom;
|
||||
|
||||
@Column(name = "valid_to")
|
||||
private LocalDate validTo;
|
||||
|
||||
@Column(name = "lecture_academic_hours", nullable = false)
|
||||
private Integer lectureAcademicHours = 0;
|
||||
|
||||
@@ -74,6 +84,30 @@ public class ScheduleRule {
|
||||
this.semester = semester;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDate getValidFrom() {
|
||||
return validFrom;
|
||||
}
|
||||
|
||||
public void setValidFrom(LocalDate validFrom) {
|
||||
this.validFrom = validFrom;
|
||||
}
|
||||
|
||||
public LocalDate getValidTo() {
|
||||
return validTo;
|
||||
}
|
||||
|
||||
public void setValidTo(LocalDate validTo) {
|
||||
this.validTo = validTo;
|
||||
}
|
||||
|
||||
public Integer getLectureAcademicHours() {
|
||||
return lectureAcademicHours;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name="specialties")
|
||||
public class Speciality {
|
||||
public class Speciality extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "specialty_profiles")
|
||||
public class SpecialtyProfile {
|
||||
public class SpecialtyProfile extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "student_groups")
|
||||
public class StudentGroup {
|
||||
public class StudentGroup extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "subgroups")
|
||||
public class Subgroup {
|
||||
public class Subgroup extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "subjects")
|
||||
public class Subject {
|
||||
public class Subject extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "subject_comments")
|
||||
public class SubjectComment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "subject_id", nullable = false)
|
||||
private Subject subject;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "author_id")
|
||||
private User author;
|
||||
|
||||
@Column(nullable = false, columnDefinition = "TEXT")
|
||||
private String comment;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public Subject getSubject() {
|
||||
return subject;
|
||||
}
|
||||
|
||||
public void setSubject(Subject subject) {
|
||||
this.subject = subject;
|
||||
}
|
||||
|
||||
public User getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(User author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
public void setComment(String comment) {
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.magistr.app.model;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "teacher_department_assignments")
|
||||
public class TeacherDepartmentAssignment {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "teacher_id", nullable = false)
|
||||
private User teacher;
|
||||
|
||||
@ManyToOne(optional = false)
|
||||
@JoinColumn(name = "department_id", nullable = false)
|
||||
private Department department;
|
||||
|
||||
@Column(name = "valid_from", nullable = false)
|
||||
private LocalDate validFrom;
|
||||
|
||||
@Column(name = "valid_to")
|
||||
private LocalDate validTo;
|
||||
|
||||
@Column(name = "is_primary", nullable = false)
|
||||
private Boolean primaryAssignment = true;
|
||||
|
||||
@Column(columnDefinition = "TEXT")
|
||||
private String comment;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
private LocalDateTime createdAt = LocalDateTime.now();
|
||||
|
||||
@Column(name = "created_by")
|
||||
private Long createdBy;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public User getTeacher() {
|
||||
return teacher;
|
||||
}
|
||||
|
||||
public void setTeacher(User teacher) {
|
||||
this.teacher = teacher;
|
||||
}
|
||||
|
||||
public Department getDepartment() {
|
||||
return department;
|
||||
}
|
||||
|
||||
public void setDepartment(Department department) {
|
||||
this.department = department;
|
||||
}
|
||||
|
||||
public LocalDate getValidFrom() {
|
||||
return validFrom;
|
||||
}
|
||||
|
||||
public void setValidFrom(LocalDate validFrom) {
|
||||
this.validFrom = validFrom;
|
||||
}
|
||||
|
||||
public LocalDate getValidTo() {
|
||||
return validTo;
|
||||
}
|
||||
|
||||
public void setValidTo(LocalDate validTo) {
|
||||
this.validTo = validTo;
|
||||
}
|
||||
|
||||
public Boolean getPrimaryAssignment() {
|
||||
return primaryAssignment;
|
||||
}
|
||||
|
||||
public void setPrimaryAssignment(Boolean primaryAssignment) {
|
||||
this.primaryAssignment = primaryAssignment;
|
||||
}
|
||||
|
||||
public String getComment() {
|
||||
return comment;
|
||||
}
|
||||
|
||||
public void setComment(String comment) {
|
||||
this.comment = comment;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Long getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(Long createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
public class User extends LifecycleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -4,7 +4,10 @@ import com.magistr.app.model.Classroom;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.List;
|
||||
|
||||
public interface ClassroomRepository extends JpaRepository<Classroom, Long> {
|
||||
Optional<Classroom> findByName(String name);
|
||||
|
||||
List<Classroom> findByStatusNot(String status);
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ import com.magistr.app.model.Department;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.List;
|
||||
|
||||
public interface DepartmentRepository extends JpaRepository<Department, Long> {
|
||||
|
||||
Optional<Department> findByDepartmentName(String departmentName);
|
||||
|
||||
Optional<Department> findByDepartmentCode(Long departmentCode);
|
||||
|
||||
List<Department> findByStatusNot(String status);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import com.magistr.app.model.Equipment;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.List;
|
||||
|
||||
public interface EquipmentRepository extends JpaRepository<Equipment, Long> {
|
||||
Optional<Equipment> findByName(String name);
|
||||
|
||||
List<Equipment> findByStatusNot(String status);
|
||||
}
|
||||
|
||||
@@ -10,4 +10,8 @@ public interface GroupRepository extends JpaRepository<StudentGroup, Long> {
|
||||
List<StudentGroup> findByEducationFormId(Long educationFormId);
|
||||
|
||||
List<StudentGroup> findByDepartmentId(Long departmentId);
|
||||
|
||||
List<StudentGroup> findByStatusNot(String status);
|
||||
|
||||
List<StudentGroup> findByDepartmentIdAndStatusNot(Long departmentId, String status);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.ScheduleOverride;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface ScheduleOverrideRepository extends JpaRepository<ScheduleOverride, Long> {
|
||||
|
||||
@Query("""
|
||||
select o
|
||||
from ScheduleOverride o
|
||||
left join fetch o.baseRuleSlot baseSlot
|
||||
left join fetch o.newTimeSlot
|
||||
left join fetch o.newClassroom
|
||||
left join fetch o.newTeacher
|
||||
where o.lessonDate between :startDate and :endDate
|
||||
""")
|
||||
List<ScheduleOverride> findByLessonDateBetweenWithDetails(
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate
|
||||
);
|
||||
|
||||
Optional<ScheduleOverride> findByBaseRuleSlotIdAndLessonDate(Long baseRuleSlotId, LocalDate lessonDate);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ public interface ScheduleRuleRepository extends JpaRepository<ScheduleRule, Long
|
||||
left join fetch slots.lessonType
|
||||
where g.id = :groupId
|
||||
and sem.id = :semesterId
|
||||
and r.status <> 'ARCHIVED'
|
||||
""")
|
||||
List<ScheduleRule> findByGroupIdAndSemesterId(
|
||||
@Param("groupId") Long groupId,
|
||||
@@ -49,6 +50,7 @@ public interface ScheduleRuleRepository extends JpaRepository<ScheduleRule, Long
|
||||
left join fetch slots.lessonType
|
||||
where teacherSlot.teacher.id = :teacherId
|
||||
and sem.id = :semesterId
|
||||
and r.status <> 'ARCHIVED'
|
||||
""")
|
||||
List<ScheduleRule> findByTeacherIdAndSemesterId(
|
||||
@Param("teacherId") Long teacherId,
|
||||
@@ -69,6 +71,7 @@ public interface ScheduleRuleRepository extends JpaRepository<ScheduleRule, Long
|
||||
left join fetch slots.teacher
|
||||
left join fetch slots.classroom
|
||||
left join fetch slots.lessonType
|
||||
where r.status <> 'ARCHIVED'
|
||||
order by r.id desc
|
||||
""")
|
||||
List<ScheduleRule> findAllWithDetails();
|
||||
|
||||
@@ -4,10 +4,13 @@ import com.magistr.app.model.Speciality;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.List;
|
||||
|
||||
public interface SpecialtiesRepository extends JpaRepository<Speciality, Long> {
|
||||
|
||||
Optional<Speciality> findBySpecialityName(String specialityName);
|
||||
|
||||
Optional<Speciality> findBySpecialityCode(String specialityCode);
|
||||
|
||||
List<Speciality> findByStatusNot(String status);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ public interface SubgroupRepository extends JpaRepository<Subgroup, Long> {
|
||||
select subgroup
|
||||
from Subgroup subgroup
|
||||
join fetch subgroup.studentGroup studentGroup
|
||||
where subgroup.status <> 'ARCHIVED'
|
||||
order by studentGroup.name, subgroup.name
|
||||
""")
|
||||
List<Subgroup> findAllWithGroupsOrderByGroupNameAndName();
|
||||
@@ -23,6 +24,7 @@ public interface SubgroupRepository extends JpaRepository<Subgroup, Long> {
|
||||
from Subgroup subgroup
|
||||
join fetch subgroup.studentGroup
|
||||
where subgroup.studentGroup.id = :groupId
|
||||
and subgroup.status <> 'ARCHIVED'
|
||||
order by subgroup.name
|
||||
""")
|
||||
List<Subgroup> findByStudentGroupIdOrderByNameAsc(@Param("groupId") Long groupId);
|
||||
@@ -41,6 +43,7 @@ public interface SubgroupRepository extends JpaRepository<Subgroup, Long> {
|
||||
from Subgroup subgroup
|
||||
where subgroup.studentGroup.id = :groupId
|
||||
and lower(subgroup.name) = lower(:name)
|
||||
and subgroup.status <> 'ARCHIVED'
|
||||
""")
|
||||
boolean existsByStudentGroupIdAndNameIgnoreCase(@Param("groupId") Long groupId, @Param("name") String name);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.SubjectComment;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface SubjectCommentRepository extends JpaRepository<SubjectComment, Long> {
|
||||
List<SubjectComment> findBySubjectIdOrderByCreatedAtDesc(Long subjectId);
|
||||
}
|
||||
@@ -10,4 +10,8 @@ public interface SubjectRepository extends JpaRepository<Subject, Long> {
|
||||
Optional<Subject> findByName(String name);
|
||||
|
||||
List<Subject> findByDepartmentId(Long departmentId);
|
||||
|
||||
List<Subject> findByStatusNot(String status);
|
||||
|
||||
List<Subject> findByDepartmentIdAndStatusNot(Long departmentId, String status);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.magistr.app.repository;
|
||||
|
||||
import com.magistr.app.model.TeacherDepartmentAssignment;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface TeacherDepartmentAssignmentRepository extends JpaRepository<TeacherDepartmentAssignment, Long> {
|
||||
|
||||
@Query("""
|
||||
select a
|
||||
from TeacherDepartmentAssignment a
|
||||
join fetch a.teacher
|
||||
join fetch a.department
|
||||
where a.teacher.id = :teacherId
|
||||
order by a.validFrom desc, a.id desc
|
||||
""")
|
||||
List<TeacherDepartmentAssignment> findHistoryByTeacherId(@Param("teacherId") Long teacherId);
|
||||
|
||||
@Query("""
|
||||
select a
|
||||
from TeacherDepartmentAssignment a
|
||||
join fetch a.teacher
|
||||
join fetch a.department
|
||||
where a.teacher.id = :teacherId
|
||||
and a.primaryAssignment = true
|
||||
and a.validTo is null
|
||||
""")
|
||||
Optional<TeacherDepartmentAssignment> findOpenPrimaryByTeacherId(@Param("teacherId") Long teacherId);
|
||||
|
||||
@Query("""
|
||||
select a
|
||||
from TeacherDepartmentAssignment a
|
||||
join fetch a.teacher
|
||||
join fetch a.department
|
||||
where a.department.id = :departmentId
|
||||
and a.primaryAssignment = true
|
||||
and a.validFrom <= :date
|
||||
and (a.validTo is null or a.validTo >= :date)
|
||||
order by a.teacher.fullName
|
||||
""")
|
||||
List<TeacherDepartmentAssignment> findDepartmentTeachersAtDate(
|
||||
@Param("departmentId") Long departmentId,
|
||||
@Param("date") LocalDate date
|
||||
);
|
||||
}
|
||||
@@ -14,4 +14,10 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
List<User> findByRole(Role role);
|
||||
|
||||
List<User> findByRoleAndDepartmentId(Role role, Long departmentId);
|
||||
|
||||
List<User> findByStatusNot(String status);
|
||||
|
||||
List<User> findByRoleAndStatusNot(Role role, String status);
|
||||
|
||||
List<User> findByRoleAndDepartmentIdAndStatusNot(Role role, Long departmentId, String status);
|
||||
}
|
||||
|
||||
@@ -167,6 +167,9 @@ public class ScheduleGeneratorService {
|
||||
if (semesterOpt.isEmpty() || !Objects.equals(semesterOpt.get().getId(), rule.getSemester().getId())) {
|
||||
return;
|
||||
}
|
||||
if (!rule.getSubject().isActiveOn(date)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Semester semester = semesterOpt.get();
|
||||
List<StudentGroup> eligibleGroups = eligibleGroups(rule, targetGroup, semester, date);
|
||||
@@ -193,6 +196,12 @@ public class ScheduleGeneratorService {
|
||||
Map<String, Integer> consumedHoursByBucket = new HashMap<>();
|
||||
|
||||
for (ScheduleRuleSlot slot : slots) {
|
||||
if (!slot.getTeacher().isActiveOn(date) || !slot.getClassroom().isActiveOn(date)) {
|
||||
continue;
|
||||
}
|
||||
if (date.isAfter(LocalDate.now()) && Boolean.FALSE.equals(slot.getClassroom().getIsAvailable())) {
|
||||
continue;
|
||||
}
|
||||
ScheduleLessonCategory category = ScheduleLessonCategory.fromLessonType(slot.getLessonType());
|
||||
if (weekNumber < rule.startWeekFor(category)) {
|
||||
continue;
|
||||
@@ -254,10 +263,13 @@ public class ScheduleGeneratorService {
|
||||
|
||||
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 targetGroup.isActiveOn(date) && academicDateService.isTheoryDay(targetGroup, semester, date)
|
||||
? List.of(targetGroup)
|
||||
: List.of();
|
||||
}
|
||||
|
||||
return rule.getGroups().stream()
|
||||
.filter(group -> group.isActiveOn(date))
|
||||
.filter(group -> academicDateService.isTheoryDay(group, semester, date))
|
||||
.sorted(Comparator.comparing(StudentGroup::getName))
|
||||
.toList();
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.magistr.app.service;
|
||||
|
||||
import com.magistr.app.dto.RenderedLessonDto;
|
||||
import com.magistr.app.model.LifecycleEntity;
|
||||
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.stereotype.Service;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class ScheduleQueryService {
|
||||
|
||||
private final ScheduleGeneratorService scheduleGeneratorService;
|
||||
private final GroupRepository groupRepository;
|
||||
private final ScheduleOverrideRepository scheduleOverrideRepository;
|
||||
|
||||
public ScheduleQueryService(ScheduleGeneratorService scheduleGeneratorService,
|
||||
GroupRepository groupRepository,
|
||||
ScheduleOverrideRepository scheduleOverrideRepository) {
|
||||
this.scheduleGeneratorService = scheduleGeneratorService;
|
||||
this.groupRepository = groupRepository;
|
||||
this.scheduleOverrideRepository = scheduleOverrideRepository;
|
||||
}
|
||||
|
||||
public List<RenderedLessonDto> search(Long groupId,
|
||||
Long teacherId,
|
||||
Long classroomId,
|
||||
Long departmentId,
|
||||
Long subjectId,
|
||||
Long lessonTypeId,
|
||||
Long timeSlotId,
|
||||
String parity,
|
||||
LocalDate startDate,
|
||||
LocalDate endDate) {
|
||||
List<StudentGroup> groups = resolveGroups(groupId, departmentId);
|
||||
List<RenderedLessonDto> generatedLessons = groups.stream()
|
||||
.flatMap(group -> scheduleGeneratorService.buildScheduleForGroup(group.getId(), startDate, endDate).stream())
|
||||
.toList();
|
||||
List<RenderedLessonDto> lessons = applyOverrides(generatedLessons, startDate, endDate).stream()
|
||||
.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))
|
||||
.filter(lesson -> lessonTypeId == null || Objects.equals(lesson.lessonTypeId(), lessonTypeId))
|
||||
.filter(lesson -> timeSlotId == null || Objects.equals(lesson.timeSlotId(), timeSlotId))
|
||||
.filter(lesson -> parity == null || parity.isBlank() || lesson.parity().name().equalsIgnoreCase(parity))
|
||||
.toList();
|
||||
return deduplicate(lessons);
|
||||
}
|
||||
|
||||
private List<RenderedLessonDto> applyOverrides(List<RenderedLessonDto> lessons, LocalDate startDate, LocalDate endDate) {
|
||||
Map<String, ScheduleOverride> overrides = scheduleOverrideRepository
|
||||
.findByLessonDateBetweenWithDetails(startDate, endDate)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
override -> overrideKey(override.getBaseRuleSlot().getId(), override.getLessonDate()),
|
||||
override -> override,
|
||||
(first, second) -> second
|
||||
));
|
||||
if (overrides.isEmpty()) {
|
||||
return lessons;
|
||||
}
|
||||
return lessons.stream()
|
||||
.map(lesson -> {
|
||||
ScheduleOverride override = overrides.get(overrideKey(lesson.scheduleRuleSlotId(), lesson.date()));
|
||||
return override == null ? lesson : applyOverride(lesson, override);
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private RenderedLessonDto applyOverride(RenderedLessonDto lesson, ScheduleOverride override) {
|
||||
if ("CANCEL".equals(override.getAction())) {
|
||||
return null;
|
||||
}
|
||||
Long timeSlotId = lesson.timeSlotId();
|
||||
Integer timeSlotOrder = lesson.timeSlotOrder();
|
||||
java.time.LocalTime startTime = lesson.startTime();
|
||||
java.time.LocalTime endTime = lesson.endTime();
|
||||
if (override.getNewTimeSlot() != null) {
|
||||
timeSlotId = override.getNewTimeSlot().getId();
|
||||
timeSlotOrder = override.getNewTimeSlot().getOrderNumber();
|
||||
startTime = override.getNewTimeSlot().getStartTime();
|
||||
endTime = override.getNewTimeSlot().getEndTime();
|
||||
}
|
||||
|
||||
Long classroomId = override.getNewClassroom() == null ? lesson.classroomId() : override.getNewClassroom().getId();
|
||||
String classroomName = override.getNewClassroom() == null ? lesson.classroomName() : override.getNewClassroom().getName();
|
||||
Long teacherId = override.getNewTeacher() == null ? lesson.teacherId() : override.getNewTeacher().getId();
|
||||
String teacherName = override.getNewTeacher() == null ? lesson.teacherName() : ScheduleGeneratorService.displayUserName(override.getNewTeacher());
|
||||
String lessonFormat = override.getNewLessonFormat() == null ? lesson.lessonFormat() : override.getNewLessonFormat();
|
||||
|
||||
return new RenderedLessonDto(
|
||||
lesson.scheduleRuleId(),
|
||||
lesson.scheduleRuleSlotId(),
|
||||
lesson.date(),
|
||||
lesson.dayOfWeek(),
|
||||
lesson.dayName(),
|
||||
lesson.weekNumber(),
|
||||
lesson.parity(),
|
||||
timeSlotId,
|
||||
timeSlotOrder,
|
||||
startTime,
|
||||
endTime,
|
||||
lesson.subjectId(),
|
||||
lesson.subjectName(),
|
||||
teacherId,
|
||||
teacherName,
|
||||
classroomId,
|
||||
classroomName,
|
||||
lesson.lessonTypeId(),
|
||||
lesson.lessonTypeName(),
|
||||
lessonFormat,
|
||||
lesson.subgroupId(),
|
||||
lesson.subgroupName(),
|
||||
lesson.subgroupIds(),
|
||||
lesson.subgroupNames(),
|
||||
lesson.groupIds(),
|
||||
lesson.groupNames(),
|
||||
lesson.activityType(),
|
||||
lesson.lessonTypeAcademicHours(),
|
||||
lesson.consumedLessonTypeAcademicHoursBeforeLesson(),
|
||||
lesson.remainingLessonTypeAcademicHoursAfterLesson()
|
||||
);
|
||||
}
|
||||
|
||||
private String overrideKey(Long scheduleRuleSlotId, LocalDate date) {
|
||||
return scheduleRuleSlotId + ":" + date;
|
||||
}
|
||||
|
||||
private List<StudentGroup> resolveGroups(Long groupId, Long departmentId) {
|
||||
if (groupId != null) {
|
||||
return groupRepository.findById(groupId)
|
||||
.filter(StudentGroup::isActiveRecord)
|
||||
.map(List::of)
|
||||
.orElse(List.of());
|
||||
}
|
||||
if (departmentId != null) {
|
||||
return groupRepository.findByDepartmentIdAndStatusNot(departmentId, LifecycleEntity.STATUS_ARCHIVED);
|
||||
}
|
||||
return groupRepository.findByStatusNot(LifecycleEntity.STATUS_ARCHIVED);
|
||||
}
|
||||
|
||||
private List<RenderedLessonDto> deduplicate(List<RenderedLessonDto> lessons) {
|
||||
Map<String, MutableLesson> unique = new LinkedHashMap<>();
|
||||
for (RenderedLessonDto lesson : lessons) {
|
||||
String key = lessonKey(lesson);
|
||||
unique.computeIfAbsent(key, ignored -> new MutableLesson(lesson)).merge(lesson);
|
||||
}
|
||||
return unique.values().stream()
|
||||
.map(MutableLesson::toDto)
|
||||
.sorted(Comparator
|
||||
.comparing(RenderedLessonDto::date)
|
||||
.thenComparing(RenderedLessonDto::timeSlotOrder, Comparator.nullsLast(Integer::compareTo))
|
||||
.thenComparing(RenderedLessonDto::subjectName, Comparator.nullsLast(String::compareTo)))
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String lessonKey(RenderedLessonDto lesson) {
|
||||
return String.join(":",
|
||||
String.valueOf(lesson.date()),
|
||||
String.valueOf(lesson.scheduleRuleSlotId()),
|
||||
String.valueOf(lesson.classroomId()),
|
||||
String.valueOf(lesson.timeSlotOrder()),
|
||||
String.valueOf(lesson.teacherId()),
|
||||
String.valueOf(lesson.subjectId()),
|
||||
sortedSignature(lesson.subgroupIds())
|
||||
);
|
||||
}
|
||||
|
||||
private String sortedSignature(List<Long> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return values.stream()
|
||||
.map(String::valueOf)
|
||||
.sorted()
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
private static class MutableLesson {
|
||||
private final RenderedLessonDto source;
|
||||
private final LinkedHashSet<Long> groupIds = new LinkedHashSet<>();
|
||||
private final LinkedHashSet<String> groupNames = new LinkedHashSet<>();
|
||||
private final LinkedHashSet<Long> subgroupIds = new LinkedHashSet<>();
|
||||
private final LinkedHashSet<String> subgroupNames = new LinkedHashSet<>();
|
||||
|
||||
MutableLesson(RenderedLessonDto source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
void merge(RenderedLessonDto lesson) {
|
||||
if (lesson.groupIds() != null) {
|
||||
groupIds.addAll(lesson.groupIds());
|
||||
}
|
||||
if (lesson.groupNames() != null) {
|
||||
groupNames.addAll(lesson.groupNames());
|
||||
}
|
||||
if (lesson.subgroupIds() != null) {
|
||||
subgroupIds.addAll(lesson.subgroupIds());
|
||||
}
|
||||
if (lesson.subgroupNames() != null) {
|
||||
subgroupNames.addAll(lesson.subgroupNames());
|
||||
}
|
||||
}
|
||||
|
||||
RenderedLessonDto toDto() {
|
||||
return new RenderedLessonDto(
|
||||
source.scheduleRuleId(),
|
||||
source.scheduleRuleSlotId(),
|
||||
source.date(),
|
||||
source.dayOfWeek(),
|
||||
source.dayName(),
|
||||
source.weekNumber(),
|
||||
source.parity(),
|
||||
source.timeSlotId(),
|
||||
source.timeSlotOrder(),
|
||||
source.startTime(),
|
||||
source.endTime(),
|
||||
source.subjectId(),
|
||||
source.subjectName(),
|
||||
source.teacherId(),
|
||||
source.teacherName(),
|
||||
source.classroomId(),
|
||||
source.classroomName(),
|
||||
source.lessonTypeId(),
|
||||
source.lessonTypeName(),
|
||||
source.lessonFormat(),
|
||||
subgroupIds.size() == 1 ? subgroupIds.iterator().next() : null,
|
||||
subgroupNames.size() == 1 ? subgroupNames.iterator().next() : null,
|
||||
List.copyOf(subgroupIds),
|
||||
List.copyOf(subgroupNames),
|
||||
List.copyOf(groupIds),
|
||||
List.copyOf(groupNames),
|
||||
source.activityType(),
|
||||
source.lessonTypeAcademicHours(),
|
||||
source.consumedLessonTypeAcademicHoursBeforeLesson(),
|
||||
source.remainingLessonTypeAcademicHoursAfterLesson()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,13 @@ CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
CREATE TABLE IF NOT EXISTS departments (
|
||||
id BIGSERIAL UNIQUE PRIMARY KEY NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
code BIGINT UNIQUE NOT NULL
|
||||
code BIGINT UNIQUE NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
active_from DATE,
|
||||
active_to DATE,
|
||||
archived_at TIMESTAMP,
|
||||
archived_by BIGINT,
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
INSERT INTO departments (name, code) VALUES
|
||||
@@ -20,7 +26,13 @@ INSERT INTO departments (name, code) VALUES
|
||||
CREATE TABLE IF NOT EXISTS specialties (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
specialty_code VARCHAR(255) UNIQUE NOT NULL
|
||||
specialty_code VARCHAR(255) UNIQUE NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
active_from DATE,
|
||||
active_to DATE,
|
||||
archived_at TIMESTAMP,
|
||||
archived_by BIGINT,
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
INSERT INTO specialties (name, specialty_code) VALUES
|
||||
@@ -35,6 +47,12 @@ CREATE TABLE IF NOT EXISTS specialty_profiles (
|
||||
name VARCHAR(500) NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
active_from DATE,
|
||||
active_to DATE,
|
||||
archived_at TIMESTAMP,
|
||||
archived_by BIGINT,
|
||||
archive_reason TEXT,
|
||||
CONSTRAINT uq_specialty_profiles_name UNIQUE (specialty_id, name)
|
||||
);
|
||||
|
||||
@@ -50,20 +68,62 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'STUDENT',
|
||||
role VARCHAR(30) NOT NULL DEFAULT 'STUDENT',
|
||||
full_name VARCHAR(255) NOT NULL,
|
||||
job_title VARCHAR(255) NOT NULL,
|
||||
department_id BIGINT NOT NULL REFERENCES departments(id),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
active_from DATE,
|
||||
active_to DATE,
|
||||
archived_at TIMESTAMP,
|
||||
archived_by BIGINT,
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
-- Админ по умолчанию: admin / admin (bcrypt через pgcrypto)
|
||||
INSERT INTO users (username, password, role, full_name, job_title, department_id)
|
||||
VALUES ('admin', crypt('admin', gen_salt('bf', 10)), 'ADMIN', 'Иванов Админ Иванович', 'Доцент', 1),
|
||||
('Тестовый преподаватель', crypt('1234567890', gen_salt('bf', 10)), 'TEACHER', 'Петров Препод Петрович', 'Профессор', 2)
|
||||
('Тестовый преподаватель', crypt('1234567890', gen_salt('bf', 10)), 'TEACHER', 'Петров Препод Петрович', 'Профессор', 2),
|
||||
('учебный_отдел', crypt('1234567890', gen_salt('bf', 10)), 'EDUCATION_OFFICE', 'Оператор учебного отдела', 'Учебный отдел', 1),
|
||||
('кафедра_иб', crypt('1234567890', gen_salt('bf', 10)), 'DEPARTMENT', 'Оператор кафедры ИБ', 'Кафедра', 1),
|
||||
('просмотр_расписаний', crypt('1234567890', gen_salt('bf', 10)), 'SCHEDULE_VIEWER', 'Оператор просмотра расписаний', 'Просмотр расписаний', 1)
|
||||
ON CONFLICT (username) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS teacher_department_assignments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
teacher_id BIGINT NOT NULL REFERENCES users(id),
|
||||
department_id BIGINT NOT NULL REFERENCES departments(id),
|
||||
valid_from DATE NOT NULL,
|
||||
valid_to DATE,
|
||||
is_primary BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
comment TEXT,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by BIGINT REFERENCES users(id),
|
||||
CONSTRAINT chk_teacher_department_dates CHECK (valid_to IS NULL OR valid_to >= valid_from)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_teacher_department_teacher_dates
|
||||
ON teacher_department_assignments(teacher_id, valid_from, valid_to);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_teacher_department_department_dates
|
||||
ON teacher_department_assignments(department_id, valid_from, valid_to);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_teacher_department_open_primary
|
||||
ON teacher_department_assignments(teacher_id)
|
||||
WHERE is_primary = TRUE AND valid_to IS NULL;
|
||||
|
||||
INSERT INTO teacher_department_assignments (teacher_id, department_id, valid_from, is_primary, comment)
|
||||
SELECT u.id, u.department_id, COALESCE(u.created_at::DATE, CURRENT_DATE), TRUE, 'Начальная кафедра из users.department_id'
|
||||
FROM users u
|
||||
WHERE u.role = 'TEACHER'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM teacher_department_assignments existing
|
||||
WHERE existing.teacher_id = u.id
|
||||
);
|
||||
|
||||
-- ==========================================
|
||||
-- Образовательные формы
|
||||
-- ==========================================
|
||||
@@ -71,7 +131,13 @@ CREATE TABLE IF NOT EXISTS education_forms (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
active_from DATE,
|
||||
active_to DATE,
|
||||
archived_at TIMESTAMP,
|
||||
archived_by BIGINT,
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
INSERT INTO education_forms (name) VALUES
|
||||
@@ -92,7 +158,13 @@ CREATE TABLE IF NOT EXISTS student_groups (
|
||||
specialty_id BIGINT NOT NULL REFERENCES specialties(id),
|
||||
specialty_profile_id BIGINT NOT NULL REFERENCES specialty_profiles(id),
|
||||
year_start_study BIGINT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
active_from DATE,
|
||||
active_to DATE,
|
||||
archived_at TIMESTAMP,
|
||||
archived_by BIGINT,
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
-- Тестовая базовая группа для работы
|
||||
@@ -116,6 +188,12 @@ CREATE TABLE IF NOT EXISTS subgroups (
|
||||
group_id BIGINT NOT NULL REFERENCES student_groups(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
student_capacity INT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
active_from DATE,
|
||||
active_to DATE,
|
||||
archived_at TIMESTAMP,
|
||||
archived_by BIGINT,
|
||||
archive_reason TEXT,
|
||||
UNIQUE(group_id, name)
|
||||
);
|
||||
|
||||
@@ -146,7 +224,13 @@ CREATE TABLE IF NOT EXISTS subjects (
|
||||
code VARCHAR(20),
|
||||
department_id BIGINT NOT NULL REFERENCES departments(id),
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
active_from DATE,
|
||||
active_to DATE,
|
||||
archived_at TIMESTAMP,
|
||||
archived_by BIGINT,
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
INSERT INTO subjects (name, department_id) VALUES
|
||||
@@ -157,6 +241,17 @@ INSERT INTO subjects (name, department_id) VALUES
|
||||
('Английский язык', 1)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS subject_comments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
subject_id BIGINT NOT NULL REFERENCES subjects(id),
|
||||
author_id BIGINT REFERENCES users(id),
|
||||
comment TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_subject_comments_subject
|
||||
ON subject_comments(subject_id, created_at DESC);
|
||||
|
||||
-- Типы занятий
|
||||
CREATE TABLE IF NOT EXISTS lesson_types (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
@@ -176,7 +271,13 @@ CREATE TABLE IF NOT EXISTS equipments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
inventory_number VARCHAR(50)
|
||||
inventory_number VARCHAR(50),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
active_from DATE,
|
||||
active_to DATE,
|
||||
archived_at TIMESTAMP,
|
||||
archived_by BIGINT,
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
INSERT INTO equipments (name) VALUES
|
||||
@@ -197,7 +298,13 @@ CREATE TABLE IF NOT EXISTS classrooms (
|
||||
floor INT,
|
||||
is_available BOOLEAN DEFAULT TRUE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
active_from DATE,
|
||||
active_to DATE,
|
||||
archived_at TIMESTAMP,
|
||||
archived_by BIGINT,
|
||||
archive_reason TEXT
|
||||
);
|
||||
|
||||
INSERT INTO classrooms (name, capacity, building, floor) VALUES
|
||||
@@ -229,6 +336,17 @@ WHERE
|
||||
OR (c.name = '303 Обычная' AND e.name IN ('Проектор'))
|
||||
ON CONFLICT (classroom_id, equipment_id) DO NOTHING;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_departments_status ON departments(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_specialties_status ON specialties(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_specialty_profiles_status ON specialty_profiles(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_status ON users(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_education_forms_status ON education_forms(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_student_groups_status ON student_groups(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_subgroups_status ON subgroups(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_subjects_status ON subjects(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_equipments_status ON equipments(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_classrooms_status ON classrooms(status);
|
||||
|
||||
-- ==========================================
|
||||
-- Связи для преподавателей
|
||||
-- ==========================================
|
||||
@@ -483,6 +601,13 @@ CREATE TABLE IF NOT EXISTS schedule_rules (
|
||||
lecture_start_week INT NOT NULL,
|
||||
laboratory_start_week INT NOT NULL,
|
||||
practice_start_week INT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
valid_from DATE,
|
||||
valid_to DATE,
|
||||
version_group_id BIGINT,
|
||||
change_reason TEXT,
|
||||
created_by BIGINT REFERENCES users(id),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT chk_schedule_rules_type_hours_non_negative CHECK (
|
||||
lecture_academic_hours >= 0
|
||||
AND laboratory_academic_hours >= 0
|
||||
@@ -495,11 +620,14 @@ CREATE TABLE IF NOT EXISTS schedule_rules (
|
||||
lecture_start_week > 0
|
||||
AND laboratory_start_week > 0
|
||||
AND practice_start_week > 0
|
||||
)
|
||||
),
|
||||
CONSTRAINT chk_schedule_rules_dates CHECK (valid_to IS NULL OR valid_from IS NULL OR valid_to >= valid_from)
|
||||
);
|
||||
|
||||
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 INDEX IF NOT EXISTS idx_schedule_rules_status_dates
|
||||
ON schedule_rules(status, valid_from, valid_to);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schedule_rule_groups (
|
||||
schedule_rule_id BIGINT NOT NULL REFERENCES schedule_rules(id) ON DELETE CASCADE,
|
||||
@@ -519,6 +647,12 @@ CREATE TABLE IF NOT EXISTS schedule_rule_slots (
|
||||
classroom_id BIGINT NOT NULL REFERENCES classrooms(id),
|
||||
lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id),
|
||||
lesson_format VARCHAR(30) NOT NULL,
|
||||
time_locked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
classroom_locked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
teacher_locked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
locked_by BIGINT REFERENCES users(id),
|
||||
locked_at TIMESTAMP,
|
||||
lock_comment TEXT,
|
||||
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 ('Очно', 'Онлайн'))
|
||||
@@ -537,6 +671,28 @@ CREATE TABLE IF NOT EXISTS schedule_rule_slot_subgroups (
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slot_subgroups_subgroup ON schedule_rule_slot_subgroups(subgroup_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS schedule_overrides (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
base_rule_slot_id BIGINT NOT NULL REFERENCES schedule_rule_slots(id),
|
||||
lesson_date DATE NOT NULL,
|
||||
action VARCHAR(20) NOT NULL,
|
||||
new_time_slot_id BIGINT REFERENCES time_slots(id),
|
||||
new_classroom_id BIGINT REFERENCES classrooms(id),
|
||||
new_teacher_id BIGINT REFERENCES users(id),
|
||||
new_lesson_format VARCHAR(30),
|
||||
comment TEXT,
|
||||
created_by BIGINT REFERENCES users(id),
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT chk_schedule_overrides_action CHECK (action IN ('MOVE', 'CANCEL', 'REPLACE')),
|
||||
CONSTRAINT uq_schedule_overrides_slot_date UNIQUE (base_rule_slot_id, lesson_date)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_overrides_date
|
||||
ON schedule_overrides(lesson_date);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_schedule_overrides_new_teacher
|
||||
ON schedule_overrides(new_teacher_id);
|
||||
|
||||
CREATE OR REPLACE FUNCTION validate_schedule_rule_slot_subgroups()
|
||||
RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
@@ -762,12 +918,17 @@ COMMENT ON TABLE schedule_rules IS 'Правила динамической ге
|
||||
COMMENT ON TABLE schedule_rule_groups IS 'Привязка правил расписания к учебным группам';
|
||||
COMMENT ON TABLE schedule_rule_slots IS 'Слоты проведения занятий внутри правила расписания';
|
||||
COMMENT ON TABLE schedule_rule_slot_subgroups IS 'Подгруппы лабораторных слотов правила расписания';
|
||||
COMMENT ON TABLE teacher_department_assignments IS 'История принадлежности преподавателей к кафедрам';
|
||||
COMMENT ON TABLE subject_comments IS 'Комментарии кафедр и администраторов к дисциплинам';
|
||||
COMMENT ON TABLE schedule_overrides IS 'Точечные изменения конкретных сгенерированных пар';
|
||||
COMMENT ON COLUMN schedule_rules.lecture_academic_hours IS 'Лимит академических часов лекций';
|
||||
COMMENT ON COLUMN schedule_rules.laboratory_academic_hours IS 'Лимит академических часов лабораторных работ';
|
||||
COMMENT ON COLUMN schedule_rules.practice_academic_hours IS 'Лимит академических часов практик';
|
||||
COMMENT ON COLUMN schedule_rules.lecture_start_week IS 'Неделя начала лекций в семестре';
|
||||
COMMENT ON COLUMN schedule_rules.laboratory_start_week IS 'Неделя начала лабораторных работ в семестре';
|
||||
COMMENT ON COLUMN schedule_rules.practice_start_week IS 'Неделя начала практик в семестре';
|
||||
COMMENT ON COLUMN schedule_rules.valid_from IS 'Дата начала действия версии правила расписания';
|
||||
COMMENT ON COLUMN schedule_rules.valid_to IS 'Дата окончания действия версии правила расписания';
|
||||
|
||||
COMMENT ON TABLE education_forms IS 'Формы обучения';
|
||||
COMMENT ON TABLE subgroups IS 'Подгруппы';
|
||||
@@ -782,6 +943,7 @@ COMMENT ON COLUMN users.id IS 'ID пользователя';
|
||||
COMMENT ON COLUMN users.username IS 'Логин пользователя';
|
||||
COMMENT ON COLUMN users.password IS 'Хэш пароля пользователя';
|
||||
COMMENT ON COLUMN users.role IS 'Роль пользователя';
|
||||
COMMENT ON COLUMN users.status IS 'Жизненный цикл пользователя: ACTIVE или ARCHIVED';
|
||||
COMMENT ON COLUMN users.created_at IS 'Дата и время создания';
|
||||
COMMENT ON COLUMN users.updated_at IS 'Дата и время последнего обновления';
|
||||
COMMENT ON COLUMN users.full_name IS 'ФИО пользователя';
|
||||
@@ -833,6 +995,7 @@ COMMENT ON COLUMN classrooms.floor IS 'Этаж';
|
||||
COMMENT ON COLUMN classrooms.is_available IS 'Доступность аудитории';
|
||||
COMMENT ON COLUMN classrooms.description IS 'Описание аудитории';
|
||||
COMMENT ON COLUMN classrooms.created_at IS 'Дата и время создания';
|
||||
COMMENT ON COLUMN classrooms.status IS 'Жизненный цикл аудитории: ACTIVE или ARCHIVED';
|
||||
|
||||
COMMENT ON COLUMN classroom_equipments.classroom_id IS 'ID аудитории';
|
||||
COMMENT ON COLUMN classroom_equipments.equipment_id IS 'ID оборудования';
|
||||
|
||||
Reference in New Issue
Block a user