Compare commits
4 Commits
9d06c99d06
...
course-sem
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e82ed69639 | ||
|
|
cd6cc6f5f7 | ||
| 2be2534a1e | |||
| b14d937062 |
@@ -38,14 +38,15 @@ public class AuthController {
|
|||||||
!passwordEncoder.matches(request.getPassword(), userOpt.get().getPassword())) {
|
!passwordEncoder.matches(request.getPassword(), userOpt.get().getPassword())) {
|
||||||
return ResponseEntity
|
return ResponseEntity
|
||||||
.status(401)
|
.status(401)
|
||||||
.body(new LoginResponse(false, "Неверное имя пользователя или пароль", null, null, null));
|
.body(new LoginResponse(false, "Неверное имя пользователя или пароль", null, null, null, null));
|
||||||
}
|
}
|
||||||
|
|
||||||
User user = userOpt.get();
|
User user = userOpt.get();
|
||||||
String token = UUID.randomUUID().toString();
|
String token = UUID.randomUUID().toString();
|
||||||
String roleName = user.getRole().name();
|
String roleName = user.getRole().name();
|
||||||
String redirect = ROLE_REDIRECTS.getOrDefault(roleName, "/");
|
String redirect = ROLE_REDIRECTS.getOrDefault(roleName, "/");
|
||||||
|
Long departmentId = user.getDepartmentId();
|
||||||
|
|
||||||
return ResponseEntity.ok(new LoginResponse(true, "OK", token, roleName, redirect));
|
return ResponseEntity.ok(new LoginResponse(true, "OK", token, roleName, redirect, departmentId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ public class GroupController {
|
|||||||
g.getEducationForm().getId(),
|
g.getEducationForm().getId(),
|
||||||
g.getEducationForm().getName(),
|
g.getEducationForm().getName(),
|
||||||
g.getDepartmentId(),
|
g.getDepartmentId(),
|
||||||
|
g.getEnrollmentYear(),
|
||||||
g.getCourse(),
|
g.getCourse(),
|
||||||
|
g.getSemester(),
|
||||||
g.getSpecialityCode()
|
g.getSpecialityCode()
|
||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -82,8 +84,8 @@ public class GroupController {
|
|||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<?> createGroup(@RequestBody CreateGroupRequest request) {
|
public ResponseEntity<?> createGroup(@RequestBody CreateGroupRequest request) {
|
||||||
logger.info("Получен запрос на создание новой группы: name = {}, groupSize = {}, educationFormId = {}, departmentId = {}, course = {}",
|
logger.info("Получен запрос на создание новой группы: name = {}, groupSize = {}, educationFormId = {}, departmentId = {}, enrollmentYear = {}",
|
||||||
request.getName(), request.getGroupSize(), request.getEducationFormId(), request.getDepartmentId(), request.getCourse());
|
request.getName(), request.getGroupSize(), request.getEducationFormId(), request.getDepartmentId(), request.getEnrollmentYear());
|
||||||
try {
|
try {
|
||||||
if (request.getName() == null || request.getName().isBlank()) {
|
if (request.getName() == null || request.getName().isBlank()) {
|
||||||
String errorMessage = "Название группы обязательно";
|
String errorMessage = "Название группы обязательно";
|
||||||
@@ -110,8 +112,8 @@ public class GroupController {
|
|||||||
logger.error("Ошибка валидации: {}", errorMessage);
|
logger.error("Ошибка валидации: {}", errorMessage);
|
||||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
}
|
}
|
||||||
if (request.getCourse() == null || request.getCourse() == 0) {
|
if (request.getEnrollmentYear() == null || request.getEnrollmentYear() == 0) {
|
||||||
String errorMessage = "Курс обязателен";
|
String errorMessage = "Год начала обучения обязателен";
|
||||||
logger.error("Ошибка валидации: {}", errorMessage);
|
logger.error("Ошибка валидации: {}", errorMessage);
|
||||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
}
|
}
|
||||||
@@ -131,7 +133,7 @@ public class GroupController {
|
|||||||
group.setGroupSize(request.getGroupSize());
|
group.setGroupSize(request.getGroupSize());
|
||||||
group.setEducationForm(efOpt.get());
|
group.setEducationForm(efOpt.get());
|
||||||
group.setDepartmentId(request.getDepartmentId());
|
group.setDepartmentId(request.getDepartmentId());
|
||||||
group.setCourse(request.getCourse());
|
group.setEnrollmentYear(request.getEnrollmentYear());
|
||||||
group.setSpecialityCode(request.getSpecialityCode());
|
group.setSpecialityCode(request.getSpecialityCode());
|
||||||
groupRepository.save(group);
|
groupRepository.save(group);
|
||||||
|
|
||||||
@@ -144,7 +146,9 @@ public class GroupController {
|
|||||||
group.getEducationForm().getId(),
|
group.getEducationForm().getId(),
|
||||||
group.getEducationForm().getName(),
|
group.getEducationForm().getName(),
|
||||||
group.getDepartmentId(),
|
group.getDepartmentId(),
|
||||||
|
group.getEnrollmentYear(),
|
||||||
group.getCourse(),
|
group.getCourse(),
|
||||||
|
group.getSemester(),
|
||||||
group.getSpecialityCode()));
|
group.getSpecialityCode()));
|
||||||
} catch (Exception e ) {
|
} catch (Exception e ) {
|
||||||
logger.error("Ошибка при создании группы: {}", e.getMessage(), e);
|
logger.error("Ошибка при создании группы: {}", e.getMessage(), e);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.magistr.app.dto.CreateScheduleDataRequest;
|
|||||||
import com.magistr.app.dto.ScheduleResponse;
|
import com.magistr.app.dto.ScheduleResponse;
|
||||||
import com.magistr.app.model.*;
|
import com.magistr.app.model.*;
|
||||||
import com.magistr.app.repository.*;
|
import com.magistr.app.repository.*;
|
||||||
|
import com.magistr.app.utils.SemesterTypeValidator;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@@ -147,8 +148,91 @@ public class ScheduleDataController {
|
|||||||
// Доделать проверки получаемых полей!!!
|
// Доделать проверки получаемых полей!!!
|
||||||
@PostMapping("/create")
|
@PostMapping("/create")
|
||||||
public ResponseEntity<?> createScheduleData(@RequestBody CreateScheduleDataRequest request) {
|
public ResponseEntity<?> createScheduleData(@RequestBody CreateScheduleDataRequest request) {
|
||||||
logger.info("Получен запрос на создание записи данных для расписаний");
|
logger.info("Получен запрос на создание записи данных для расписаний: departmentId={}, semester={}, groupId={}, subjectsId={}, lessonTypeId={}, numberOfHours={}, division={}, teacherId={}, semesterType={}, period={}",
|
||||||
|
request.getDepartmentId(), request.getSemester(), request.getGroupId(), request.getSubjectsId(), request.getLessonTypeId(), request.getNumberOfHours(), request.getDivision(), request.getTeacherId(), request.getSemesterType(), request.getPeriod());
|
||||||
try {
|
try {
|
||||||
|
if (request.getDepartmentId() == null || request.getDepartmentId() == 0) {
|
||||||
|
String errorMessage = "ID кафедры обязателен";
|
||||||
|
logger.info("Ошибка валидации: {}", errorMessage);
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
} else if(!scheduleDataRepository.existsById(request.getDepartmentId())) {
|
||||||
|
String errorMessage = "Кафедра не найдена";
|
||||||
|
logger.info("Кафедра не найдена");
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getSemester() == null || request.getSemester() == 0) {
|
||||||
|
String errorMessage = "Семестр обязателен";
|
||||||
|
logger.info("Ошибка валидации: {}", errorMessage);
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
} else if(request.getSemester() > 12) {
|
||||||
|
String errorMessage = "Семестр должен быть меньше или равен 12";
|
||||||
|
logger.info("Семестр должен быть меньше или равен 12");
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getGroupId() == null || request.getGroupId() == 0) {
|
||||||
|
String errorMessage = "ID группы обязателен";
|
||||||
|
logger.info("Ошибка валидации: {}", errorMessage);
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getSubjectsId() == null || request.getSubjectsId() == 0) {
|
||||||
|
String errorMessage = "ID дисциплины обязателен";
|
||||||
|
logger.info("Ошибка валидации: {}", errorMessage);
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getLessonTypeId() == null || request.getLessonTypeId() == 0) {
|
||||||
|
String errorMessage = "ID типа занятия обязателен";
|
||||||
|
logger.info("Ошибка валидации: {}", errorMessage);
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getNumberOfHours() == null) {
|
||||||
|
request.setNumberOfHours(0L);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getTeacherId() == null || request.getTeacherId() == 0) {
|
||||||
|
String errorMessage = "ID преподавателя обязателен";
|
||||||
|
logger.info("Ошибка валидации: {}", errorMessage);
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getSemesterType() == null) {
|
||||||
|
String errorMessage = "Семестр обязателен";
|
||||||
|
logger.info("Ошибка валидации: {}", errorMessage);
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
} else if (!SemesterTypeValidator.isValidTypeSemester(request.getSemesterType().toString())) {
|
||||||
|
String errorMessage = "Некорректный формат семестра. Допустимые форматы: " + SemesterTypeValidator.getValidTypes();
|
||||||
|
logger.info("Ошибка валидации: {}", errorMessage);
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.getPeriod() == null || request.getPeriod().isBlank()) {
|
||||||
|
String errorMessage = "Период обязателен";
|
||||||
|
logger.info("Ошибка валидации: {}", errorMessage);
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean existsRecord = scheduleDataRepository.existsByDepartmentIdAndSemesterAndGroupIdAndSubjectsIdAndLessonTypeIdAndNumberOfHoursAndDivisionAndTeacherIdAndSemesterTypeAndPeriod(
|
||||||
|
request.getDepartmentId(),
|
||||||
|
request.getSemester(),
|
||||||
|
request.getGroupId(),
|
||||||
|
request.getSubjectsId(),
|
||||||
|
request.getLessonTypeId(),
|
||||||
|
request.getNumberOfHours(),
|
||||||
|
request.getDivision(),
|
||||||
|
request.getTeacherId(),
|
||||||
|
request.getSemesterType(),
|
||||||
|
request.getPeriod()
|
||||||
|
);
|
||||||
|
|
||||||
|
if(existsRecord) {
|
||||||
|
return ResponseEntity.status(HttpStatus.CONFLICT)
|
||||||
|
.body(Map.of("message", "Такая запись уже существует"));
|
||||||
|
}
|
||||||
|
|
||||||
ScheduleData scheduleData = new ScheduleData();
|
ScheduleData scheduleData = new ScheduleData();
|
||||||
scheduleData.setDepartmentId(request.getDepartmentId());
|
scheduleData.setDepartmentId(request.getDepartmentId());
|
||||||
scheduleData.setSemester(request.getSemester());
|
scheduleData.setSemester(request.getSemester());
|
||||||
@@ -179,6 +263,9 @@ public class ScheduleDataController {
|
|||||||
logger.info("Запись успешно создана с ID: {}", savedSchedule.getId());
|
logger.info("Запись успешно создана с ID: {}", savedSchedule.getId());
|
||||||
|
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (org.springframework.dao.DataIntegrityViolationException e) {
|
||||||
|
return ResponseEntity.status(HttpStatus.CONFLICT)
|
||||||
|
.body(Map.of("message", "Такая запись уже существует"));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
logger.error("Ошибка при создании записи: {}", e.getMessage(), e);
|
logger.error("Ошибка при создании записи: {}", e.getMessage(), e);
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ public class CreateGroupRequest {
|
|||||||
private Long groupSize;
|
private Long groupSize;
|
||||||
private Long educationFormId;
|
private Long educationFormId;
|
||||||
private Long departmentId;
|
private Long departmentId;
|
||||||
private Integer course;
|
private Integer enrollmentYear;
|
||||||
private Long specialityCode;
|
private Long specialityCode;
|
||||||
|
|
||||||
public String getName() {
|
public String getName() {
|
||||||
@@ -41,12 +41,12 @@ public class CreateGroupRequest {
|
|||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getCourse() {
|
public Integer getEnrollmentYear() {
|
||||||
return course;
|
return enrollmentYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCourse(Integer course) {
|
public void setEnrollmentYear(Integer enrollmentYear) {
|
||||||
this.course = course;
|
this.enrollmentYear = enrollmentYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getSpecialityCode() {
|
public Long getSpecialityCode() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ public class CreateScheduleDataRequest {
|
|||||||
private Long subjectsId;
|
private Long subjectsId;
|
||||||
private Long lessonTypeId;
|
private Long lessonTypeId;
|
||||||
private Long numberOfHours;
|
private Long numberOfHours;
|
||||||
private Boolean isDivision;
|
private Boolean division;
|
||||||
private Long teacherId;
|
private Long teacherId;
|
||||||
private SemesterType semesterType;
|
private SemesterType semesterType;
|
||||||
private String period;
|
private String period;
|
||||||
@@ -72,11 +72,11 @@ public class CreateScheduleDataRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getDivision() {
|
public Boolean getDivision() {
|
||||||
return isDivision;
|
return division;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDivision(Boolean division) {
|
public void setDivision(Boolean division) {
|
||||||
isDivision = division;
|
this.division = division;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getTeacherId() {
|
public Long getTeacherId() {
|
||||||
|
|||||||
@@ -8,17 +8,24 @@ public class GroupResponse {
|
|||||||
private Long educationFormId;
|
private Long educationFormId;
|
||||||
private String educationFormName;
|
private String educationFormName;
|
||||||
private Long departmentId;
|
private Long departmentId;
|
||||||
|
private Integer enrollmentYear;
|
||||||
private Integer course;
|
private Integer course;
|
||||||
|
private Integer semester;
|
||||||
private Long specialityCode;
|
private Long specialityCode;
|
||||||
|
|
||||||
public GroupResponse(Long id, String name, Long groupSize, Long educationFormId, String educationFormName, Long departmentId, Integer course, Long specialityCode) {
|
public GroupResponse(Long id, String name, Long groupSize, Long educationFormId,
|
||||||
|
String educationFormName, Long departmentId,
|
||||||
|
Integer enrollmentYear, Integer course, Integer semester,
|
||||||
|
Long specialityCode) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.groupSize = groupSize;
|
this.groupSize = groupSize;
|
||||||
this.educationFormId = educationFormId;
|
this.educationFormId = educationFormId;
|
||||||
this.educationFormName = educationFormName;
|
this.educationFormName = educationFormName;
|
||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
|
this.enrollmentYear = enrollmentYear;
|
||||||
this.course = course;
|
this.course = course;
|
||||||
|
this.semester = semester;
|
||||||
this.specialityCode = specialityCode;
|
this.specialityCode = specialityCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +53,18 @@ public class GroupResponse {
|
|||||||
return departmentId;
|
return departmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getEnrollmentYear() {
|
||||||
|
return enrollmentYear;
|
||||||
|
}
|
||||||
|
|
||||||
public Integer getCourse() {
|
public Integer getCourse() {
|
||||||
return course;
|
return course;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getSemester() {
|
||||||
|
return semester;
|
||||||
|
}
|
||||||
|
|
||||||
public Long getSpecialityCode() {
|
public Long getSpecialityCode() {
|
||||||
return specialityCode;
|
return specialityCode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ public class LoginResponse {
|
|||||||
private String token;
|
private String token;
|
||||||
private String role;
|
private String role;
|
||||||
private String redirect;
|
private String redirect;
|
||||||
|
private Long departmentId;
|
||||||
|
|
||||||
public LoginResponse() {
|
public LoginResponse() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public LoginResponse(boolean success, String message, String token, String role, String redirect) {
|
public LoginResponse(boolean success, String message, String token, String role, String redirect, Long departmentId) {
|
||||||
this.success = success;
|
this.success = success;
|
||||||
this.message = message;
|
this.message = message;
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.role = role;
|
this.role = role;
|
||||||
this.redirect = redirect;
|
this.redirect = redirect;
|
||||||
|
this.departmentId = departmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSuccess() {
|
public boolean isSuccess() {
|
||||||
@@ -58,4 +60,12 @@ public class LoginResponse {
|
|||||||
public void setRedirect(String redirect) {
|
public void setRedirect(String redirect) {
|
||||||
this.redirect = redirect;
|
this.redirect = redirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getDepartmentId() {
|
||||||
|
return departmentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDepartmentId(Long departmentId) {
|
||||||
|
this.departmentId = departmentId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ public class ScheduleResponse {
|
|||||||
private Long lessonTypeId;
|
private Long lessonTypeId;
|
||||||
private String lessonType;
|
private String lessonType;
|
||||||
private Long numberOfHours;
|
private Long numberOfHours;
|
||||||
private Boolean isDivision;
|
private Boolean division;
|
||||||
private Long teacherId;
|
private Long teacherId;
|
||||||
private String teacherName;
|
private String teacherName;
|
||||||
private String teacherJobTitle;
|
private String teacherJobTitle;
|
||||||
private SemesterType semesterType;
|
private SemesterType semesterType;
|
||||||
private String period;
|
private String period;
|
||||||
|
|
||||||
public ScheduleResponse(Long id, Long departmentId, Long semester, Long groupId, Long subjectsId, Long lessonTypeId, String lessonType, Long numberOfHours, Boolean isDivision, Long teacherId, SemesterType semesterType, String period) {
|
public ScheduleResponse(Long id, Long departmentId, Long semester, Long groupId, Long subjectsId, Long lessonTypeId, String lessonType, Long numberOfHours, Boolean division, Long teacherId, SemesterType semesterType, String period) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
this.semester = semester;
|
this.semester = semester;
|
||||||
@@ -32,13 +32,13 @@ public class ScheduleResponse {
|
|||||||
this.subjectsId = subjectsId;
|
this.subjectsId = subjectsId;
|
||||||
this.lessonTypeId = lessonTypeId;
|
this.lessonTypeId = lessonTypeId;
|
||||||
this.numberOfHours = numberOfHours;
|
this.numberOfHours = numberOfHours;
|
||||||
this.isDivision = isDivision;
|
this.division = division;
|
||||||
this.teacherId = teacherId;
|
this.teacherId = teacherId;
|
||||||
this.semesterType = semesterType;
|
this.semesterType = semesterType;
|
||||||
this.period = period;
|
this.period = period;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ScheduleResponse(Long id, Long departmentId, String specialityCode, Long semester, String groupName, Integer groupCourse, String subjectName, String lessonType, Long numberOfHours, Boolean isDivision, String teacherName, String teacherJobTitle, SemesterType semesterType, String period) {
|
public ScheduleResponse(Long id, Long departmentId, String specialityCode, Long semester, String groupName, Integer groupCourse, String subjectName, String lessonType, Long numberOfHours, Boolean division, String teacherName, String teacherJobTitle, SemesterType semesterType, String period) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
this.specialityCode = specialityCode;
|
this.specialityCode = specialityCode;
|
||||||
@@ -48,7 +48,7 @@ public class ScheduleResponse {
|
|||||||
this.subjectName = subjectName;
|
this.subjectName = subjectName;
|
||||||
this.lessonType = lessonType;
|
this.lessonType = lessonType;
|
||||||
this.numberOfHours = numberOfHours;
|
this.numberOfHours = numberOfHours;
|
||||||
this.isDivision = isDivision;
|
this.division = division;
|
||||||
this.teacherName = teacherName;
|
this.teacherName = teacherName;
|
||||||
this.teacherJobTitle = teacherJobTitle;
|
this.teacherJobTitle = teacherJobTitle;
|
||||||
this.semesterType = semesterType;
|
this.semesterType = semesterType;
|
||||||
@@ -104,7 +104,7 @@ public class ScheduleResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getDivision() {
|
public Boolean getDivision() {
|
||||||
return isDivision;
|
return division;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getTeacherId() {
|
public Long getTeacherId() {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class ScheduleData {
|
|||||||
private Long numberOfHours;
|
private Long numberOfHours;
|
||||||
|
|
||||||
@Column(name="is_division", nullable = false)
|
@Column(name="is_division", nullable = false)
|
||||||
private Boolean isDivision;
|
private Boolean division;
|
||||||
|
|
||||||
@Column(name="teacher_id", nullable = false)
|
@Column(name="teacher_id", nullable = false)
|
||||||
private Long teacherId;
|
private Long teacherId;
|
||||||
@@ -43,7 +43,7 @@ public class ScheduleData {
|
|||||||
|
|
||||||
public ScheduleData() {}
|
public ScheduleData() {}
|
||||||
|
|
||||||
public ScheduleData(Long id, Long departmentId, Long semester, Long groupId, Long subjectsId, Long lessonTypeId, Long numberOfHours, Boolean isDivision, Long teacherId, SemesterType semesterType, String period) {
|
public ScheduleData(Long id, Long departmentId, Long semester, Long groupId, Long subjectsId, Long lessonTypeId, Long numberOfHours, Boolean division, Long teacherId, SemesterType semesterType, String period) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
this.semester = semester;
|
this.semester = semester;
|
||||||
@@ -51,7 +51,7 @@ public class ScheduleData {
|
|||||||
this.subjectsId = subjectsId;
|
this.subjectsId = subjectsId;
|
||||||
this.lessonTypeId = lessonTypeId;
|
this.lessonTypeId = lessonTypeId;
|
||||||
this.numberOfHours = numberOfHours;
|
this.numberOfHours = numberOfHours;
|
||||||
this.isDivision = isDivision;
|
this.division = division;
|
||||||
this.teacherId = teacherId;
|
this.teacherId = teacherId;
|
||||||
this.semesterType = semesterType;
|
this.semesterType = semesterType;
|
||||||
this.period = period;
|
this.period = period;
|
||||||
@@ -114,11 +114,11 @@ public class ScheduleData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Boolean getDivision() {
|
public Boolean getDivision() {
|
||||||
return isDivision;
|
return division;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDivision(Boolean division) {
|
public void setDivision(Boolean division) {
|
||||||
isDivision = division;
|
this.division = division;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getTeacherId() {
|
public Long getTeacherId() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.magistr.app.model;
|
package com.magistr.app.model;
|
||||||
|
|
||||||
|
import com.magistr.app.utils.CourseCalculator;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@@ -23,8 +24,8 @@ public class StudentGroup {
|
|||||||
@Column(name = "department_id", nullable = false)
|
@Column(name = "department_id", nullable = false)
|
||||||
private Long departmentId;
|
private Long departmentId;
|
||||||
|
|
||||||
@Column(name = "course", nullable = false)
|
@Column(name = "enrollment_year", nullable = false)
|
||||||
private Integer course;
|
private Integer enrollmentYear;
|
||||||
|
|
||||||
@Column(name="specialty_code", nullable = false)
|
@Column(name="specialty_code", nullable = false)
|
||||||
private Long specialityCode;
|
private Long specialityCode;
|
||||||
@@ -72,12 +73,30 @@ public class StudentGroup {
|
|||||||
this.departmentId = departmentId;
|
this.departmentId = departmentId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Integer getCourse() {
|
public Integer getEnrollmentYear() {
|
||||||
return course;
|
return enrollmentYear;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCourse(Integer course) {
|
public void setEnrollmentYear(Integer enrollmentYear) {
|
||||||
this.course = course;
|
this.enrollmentYear = enrollmentYear;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вычисляемый курс на основе года начала обучения.
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
public Integer getCourse() {
|
||||||
|
if (enrollmentYear == null) return null;
|
||||||
|
return CourseCalculator.calculateCourse(enrollmentYear);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вычисляемый семестр на основе года начала обучения.
|
||||||
|
*/
|
||||||
|
@Transient
|
||||||
|
public Integer getSemester() {
|
||||||
|
if (enrollmentYear == null) return null;
|
||||||
|
return CourseCalculator.calculateSemester(enrollmentYear);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getSpecialityCode() {
|
public Long getSpecialityCode() {
|
||||||
|
|||||||
@@ -9,4 +9,17 @@ import java.util.List;
|
|||||||
public interface ScheduleDataRepository extends JpaRepository<ScheduleData, Long> {
|
public interface ScheduleDataRepository extends JpaRepository<ScheduleData, Long> {
|
||||||
|
|
||||||
List<ScheduleData> findByDepartmentIdAndSemesterTypeAndPeriod(Long departmentId, SemesterType semesterType, String period);
|
List<ScheduleData> findByDepartmentIdAndSemesterTypeAndPeriod(Long departmentId, SemesterType semesterType, String period);
|
||||||
|
|
||||||
|
boolean existsByDepartmentIdAndSemesterAndGroupIdAndSubjectsIdAndLessonTypeIdAndNumberOfHoursAndDivisionAndTeacherIdAndSemesterTypeAndPeriod(
|
||||||
|
Long departmentId,
|
||||||
|
Long semester,
|
||||||
|
Long groupId,
|
||||||
|
Long subjectsId,
|
||||||
|
Long lessonTypeId,
|
||||||
|
Long numberOfHours,
|
||||||
|
Boolean division,
|
||||||
|
Long teacherId,
|
||||||
|
SemesterType semesterType,
|
||||||
|
String period
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.magistr.app.utils;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Утилитный класс для вычисления курса и семестра группы
|
||||||
|
* на основе года начала обучения.
|
||||||
|
*/
|
||||||
|
public final class CourseCalculator {
|
||||||
|
|
||||||
|
private CourseCalculator() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вычисляет текущий курс группы.
|
||||||
|
* До сентября студент ещё на старом курсе, с сентября — на следующем.
|
||||||
|
*
|
||||||
|
* @param enrollmentYear год начала обучения (напр. 2023)
|
||||||
|
* @return номер курса (1, 2, 3, ...)
|
||||||
|
*/
|
||||||
|
public static int calculateCourse(int enrollmentYear) {
|
||||||
|
LocalDate now = LocalDate.now();
|
||||||
|
int currentYear = now.getYear();
|
||||||
|
int currentMonth = now.getMonthValue();
|
||||||
|
|
||||||
|
// С сентября начинается новый учебный год
|
||||||
|
if (currentMonth >= 9) {
|
||||||
|
return currentYear - enrollmentYear + 1;
|
||||||
|
} else {
|
||||||
|
return currentYear - enrollmentYear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Вычисляет текущий семестр группы.
|
||||||
|
* Сентябрь–январь → нечётный (осенний) семестр, февраль–август → чётный (весенний).
|
||||||
|
*
|
||||||
|
* @param enrollmentYear год начала обучения (напр. 2023)
|
||||||
|
* @return номер семестра (1, 2, 3, ...)
|
||||||
|
*/
|
||||||
|
public static int calculateSemester(int enrollmentYear) {
|
||||||
|
int course = calculateCourse(enrollmentYear);
|
||||||
|
int currentMonth = LocalDate.now().getMonthValue();
|
||||||
|
|
||||||
|
// Сентябрь–январь: осенний (нечётный) семестр
|
||||||
|
// Февраль–август: весенний (чётный) семестр
|
||||||
|
if (currentMonth >= 9 || currentMonth <= 1) {
|
||||||
|
return (course - 1) * 2 + 1;
|
||||||
|
} else {
|
||||||
|
return (course - 1) * 2 + 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.magistr.app.utils;
|
||||||
|
|
||||||
|
import com.magistr.app.model.SemesterType;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class SemesterTypeValidator {
|
||||||
|
|
||||||
|
public static boolean isValidTypeSemester(String semesterType) {
|
||||||
|
if (semesterType == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
SemesterType.valueOf(semesterType);
|
||||||
|
return true;
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getValidTypes() {
|
||||||
|
return String.join(", ", Arrays.stream(SemesterType.values())
|
||||||
|
.map(Enum::name)
|
||||||
|
.toArray(String[]::new));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,3 +27,24 @@ VALUES (1, 1, 1, 1, 3, 2, true, 1, 'autumn', '2024-2025'),
|
|||||||
(1, 2, 2, 2, 3, 4, false, 2, 'autumn', '2024-2025'),
|
(1, 2, 2, 2, 3, 4, false, 2, 'autumn', '2024-2025'),
|
||||||
(1, 3, 1, 4, 2, 1, false, 1, 'autumn', '2024-2025'),
|
(1, 3, 1, 4, 2, 1, false, 1, 'autumn', '2024-2025'),
|
||||||
(1, 4, 2, 5, 1, 7, true, 1, 'autumn', '2024-2025');
|
(1, 4, 2, 5, 1, 7, true, 1, 'autumn', '2024-2025');
|
||||||
|
|
||||||
|
-- ==========================================
|
||||||
|
-- Год начала обучения вместо статического курса
|
||||||
|
-- ==========================================
|
||||||
|
|
||||||
|
ALTER TABLE student_groups
|
||||||
|
ADD COLUMN IF NOT EXISTS enrollment_year INT;
|
||||||
|
|
||||||
|
-- Обратный расчёт: enrollment_year = текущий_год - course + 1
|
||||||
|
-- (для месяцев до сентября курс ещё не увеличился)
|
||||||
|
UPDATE student_groups
|
||||||
|
SET enrollment_year = EXTRACT(YEAR FROM NOW())::INT - course
|
||||||
|
+ CASE WHEN EXTRACT(MONTH FROM NOW()) >= 9 THEN 1 ELSE 0 END;
|
||||||
|
|
||||||
|
ALTER TABLE student_groups
|
||||||
|
ALTER COLUMN enrollment_year SET NOT NULL;
|
||||||
|
|
||||||
|
ALTER TABLE student_groups
|
||||||
|
DROP COLUMN IF EXISTS course;
|
||||||
|
|
||||||
|
COMMENT ON COLUMN student_groups.enrollment_year IS 'Год начала обучения группы';
|
||||||
@@ -51,7 +51,8 @@ erDiagram
|
|||||||
BIGINT group_size
|
BIGINT group_size
|
||||||
BIGINT education_form_id FK
|
BIGINT education_form_id FK
|
||||||
BIGINT department_id FK
|
BIGINT department_id FK
|
||||||
INT course
|
INT enrollment_year
|
||||||
|
INT specialty_code FK
|
||||||
TIMESTAMP created_at
|
TIMESTAMP created_at
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +221,10 @@ erDiagram
|
|||||||
| `group_size` | BIGINT | Количество студентов |
|
| `group_size` | BIGINT | Количество студентов |
|
||||||
| `education_form_id` | BIGINT FK → education_forms | Форма обучения |
|
| `education_form_id` | BIGINT FK → education_forms | Форма обучения |
|
||||||
| `department_id` | BIGINT FK → departments | Кафедра |
|
| `department_id` | BIGINT FK → departments | Кафедра |
|
||||||
| `course` | INT CHECK(1–6) | Курс |
|
| `enrollment_year` | INT NOT NULL | Год начала обучения (напр. 2023) |
|
||||||
|
| `specialty_code` | INT FK → specialties | Код специальности |
|
||||||
|
|
||||||
|
> **Примечание:** Курс и семестр **вычисляются динамически** на основе `enrollment_year` и текущей даты (утилита `CourseCalculator.java`). В БД не хранятся.
|
||||||
|
|
||||||
#### `subgroups` — Подгруппы
|
#### `subgroups` — Подгруппы
|
||||||
| Колонка | Тип | Описание |
|
| Колонка | Тип | Описание |
|
||||||
@@ -341,6 +345,7 @@ erDiagram
|
|||||||
| Файл | Описание |
|
| Файл | Описание |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| `V1__init.sql` | Инициализация: все таблицы, тестовые данные, триггеры, комментарии |
|
| `V1__init.sql` | Инициализация: все таблицы, тестовые данные, триггеры, комментарии |
|
||||||
|
| `V2__editScheduleData.sql` | Добавление `specialty_code`, тестовые данные расписания, замена `course` → `enrollment_year` |
|
||||||
|
|
||||||
### Накатывание на существующих тенантов
|
### Накатывание на существующих тенантов
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,82 @@
|
|||||||
|
/* ===== Оверлей для модалок создания записей (к/ф) ===== */
|
||||||
|
.cs-overlay {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-overlay.open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-overlay-scroll {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Общие стили для обеих модалок */
|
||||||
|
.cs-modal {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1100px;
|
||||||
|
position: relative;
|
||||||
|
animation: csModalAppear 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Модалка 1 (форма) всегда поверх модалки 2 (таблицы),
|
||||||
|
чтобы выпадающие списки не уходили под таблицу */
|
||||||
|
.cs-modal-form {
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-modal-table {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes csModalAppear {
|
||||||
|
from { opacity: 0; transform: translateY(-12px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cs-modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кнопка закрытия */
|
||||||
|
.btn-close-panel {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--bg-card-border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 1.3rem;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0.25rem 0.6rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color var(--transition), background var(--transition), border-color var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-close-panel:hover {
|
||||||
|
color: var(--error);
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
border-color: var(--error);
|
||||||
|
}
|
||||||
|
|
||||||
.wrap{
|
.wrap{
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -33,22 +33,14 @@ export async function initDepartment() {
|
|||||||
const deptName = departmentSelect.options[departmentSelect.selectedIndex].text;
|
const deptName = departmentSelect.options[departmentSelect.selectedIndex].text;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({ departmentId, semesterType, period });
|
||||||
departmentId,
|
|
||||||
semesterType,
|
|
||||||
period
|
|
||||||
});
|
|
||||||
|
|
||||||
// Запрос на бэк
|
|
||||||
const data = await api.get(`/api/department/schedule?${params.toString()}`);
|
const data = await api.get(`/api/department/schedule?${params.toString()}`);
|
||||||
|
|
||||||
const semesterName = semesterType === 'spring' ? 'весенний' : (semesterType === 'autumn' ? 'осенний' : semesterType);
|
const semesterName = semesterType === 'spring' ? 'весенний' : (semesterType === 'autumn' ? 'осенний' : semesterType);
|
||||||
const periodName = period.replace('-', '/'); // Display 2024-2025 as 2024/2025
|
const periodName = period.replace('-', '/');
|
||||||
|
|
||||||
renderScheduleBlock(deptName, semesterName, periodName, data);
|
renderScheduleBlock(deptName, semesterName, periodName, data);
|
||||||
|
|
||||||
form.reset();
|
form.reset();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showAlert('schedule-form-alert', err.message || 'Ошибка загрузки данных', 'error');
|
showAlert('schedule-form-alert', err.message || 'Ошибка загрузки данных', 'error');
|
||||||
}
|
}
|
||||||
@@ -57,8 +49,7 @@ export async function initDepartment() {
|
|||||||
function renderScheduleBlock(deptName, semester, period, schedule) {
|
function renderScheduleBlock(deptName, semester, period, schedule) {
|
||||||
const details = document.createElement('details');
|
const details = document.createElement('details');
|
||||||
details.className = 'table-item';
|
details.className = 'table-item';
|
||||||
details.open = true; // Сразу открываем новый блок
|
details.open = true;
|
||||||
|
|
||||||
details.innerHTML = `
|
details.innerHTML = `
|
||||||
<summary>
|
<summary>
|
||||||
<div class="chev" aria-hidden="true">
|
<div class="chev" aria-hidden="true">
|
||||||
@@ -75,7 +66,6 @@ export async function initDepartment() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="meta">${schedule ? schedule.length : 0} записей</div>
|
<div class="meta">${schedule ? schedule.length : 0} записей</div>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
@@ -96,7 +86,6 @@ export async function initDepartment() {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
container.prepend(details);
|
container.prepend(details);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,34 +93,306 @@ export async function initDepartment() {
|
|||||||
if (!schedule || schedule.length === 0) {
|
if (!schedule || schedule.length === 0) {
|
||||||
return '<tr><td colspan="8" class="loading-row">Нет данных</td></tr>';
|
return '<tr><td colspan="8" class="loading-row">Нет данных</td></tr>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return schedule.map(r => `
|
return schedule.map(r => `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${escapeHtml(r.specialityCode || '-')}</td>
|
<td>${escapeHtml(r.specialityCode || '-')}</td>
|
||||||
<td>
|
<td>${(() => {
|
||||||
${(() => {
|
|
||||||
const course = r.groupCourse || '-';
|
const course = r.groupCourse || '-';
|
||||||
const semester = r.semester || '-';
|
const semester = r.semester || '-';
|
||||||
if (course === '-' && semester === '-') return '-';
|
if (course === '-' && semester === '-') return '-';
|
||||||
return `${course} | ${semester}`;
|
return `${course} | ${semester}`;
|
||||||
})()}
|
})()}</td>
|
||||||
</td>
|
|
||||||
<td>${escapeHtml(r.groupName || '-')}</td>
|
<td>${escapeHtml(r.groupName || '-')}</td>
|
||||||
<td>${escapeHtml(r.subjectName || '-')}</td>
|
<td>${escapeHtml(r.subjectName || '-')}</td>
|
||||||
<td>${escapeHtml(r.lessonType || '-')}</td>
|
<td>${escapeHtml(r.lessonType || '-')}</td>
|
||||||
<td>${escapeHtml(r.numberOfHours || '-')}</td>
|
<td>${escapeHtml(r.numberOfHours || '-')}</td>
|
||||||
<td>
|
<td>${r.division === true ? '✓' : ''}</td>
|
||||||
${r.division === true ? '✓' : (r.division === false ? '' : escapeHtml(''))}
|
<td>${(() => {
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
${(() => {
|
|
||||||
const jobTitle = r.teacherJobTitle || '-';
|
const jobTitle = r.teacherJobTitle || '-';
|
||||||
const teacherName = r.teacherName || '-';
|
const teacherName = r.teacherName || '-';
|
||||||
if (jobTitle === '-' && teacherName === '-') return '-';
|
if (jobTitle === '-' && teacherName === '-') return '-';
|
||||||
return `${jobTitle}, ${teacherName}`;
|
return `${jobTitle}, ${teacherName}`;
|
||||||
})()}
|
})()}</td>
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================
|
||||||
|
// ЛОГИКА ДЛЯ ФУНКЦИОНАЛА "СОЗДАТЬ ЗАПИСЬ (К/Ф)"
|
||||||
|
// Два модальных окна поверх всего контента в одном оверлее
|
||||||
|
// =========================================================
|
||||||
|
const btnCreateSchedule = document.getElementById('btn-create-schedule');
|
||||||
|
const csOverlay = document.getElementById('cs-overlay');
|
||||||
|
|
||||||
|
const modalCreateSchedule = document.getElementById('modal-create-schedule');
|
||||||
|
const modalCreateScheduleClose = document.getElementById('modal-create-schedule-close');
|
||||||
|
const formCreateSchedule = document.getElementById('create-schedule-form');
|
||||||
|
|
||||||
|
const modalViewSchedules = document.getElementById('modal-view-schedules');
|
||||||
|
const btnSaveSchedules = document.getElementById('btn-save-schedules');
|
||||||
|
const preparedSchedulesTbody = document.getElementById('prepared-schedules-tbody');
|
||||||
|
|
||||||
|
const csGroupSelect = document.getElementById('cs-group');
|
||||||
|
const csSubjectSelect = document.getElementById('cs-subject');
|
||||||
|
const csTeacherSelect = document.getElementById('cs-teacher');
|
||||||
|
const csDepartmentIdInput = document.getElementById('cs-department-id');
|
||||||
|
|
||||||
|
let preparedSchedules = [];
|
||||||
|
let csGroups = [];
|
||||||
|
let csSubjects = [];
|
||||||
|
let csTeachers = [];
|
||||||
|
|
||||||
|
const SEMESTER_LABELS = { autumn: 'Осенний', spring: 'Весенний' };
|
||||||
|
const LESSON_TYPE_LABELS = { 1: 'Лекция', 2: 'Практическая работа', 3: 'Лабораторная работа' };
|
||||||
|
|
||||||
|
const localDepartmentId = localStorage.getItem('departmentId');
|
||||||
|
|
||||||
|
// ===== Загрузка справочников =====
|
||||||
|
async function loadDictionariesForSchedule() {
|
||||||
|
try {
|
||||||
|
csGroups = await api.get('/api/groups');
|
||||||
|
csGroupSelect.innerHTML = '<option value="">Выберите группу</option>' +
|
||||||
|
csGroups.map(g => `<option value="${g.id}">${escapeHtml(g.name)}</option>`).join('');
|
||||||
|
|
||||||
|
csSubjects = await api.get('/api/subjects');
|
||||||
|
csSubjectSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
|
||||||
|
csSubjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
|
||||||
|
|
||||||
|
if (localDepartmentId) {
|
||||||
|
csTeachers = await api.get(`/api/users/teachers/${localDepartmentId}`);
|
||||||
|
csTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' +
|
||||||
|
csTeachers.map(t => `<option value="${t.id}">${escapeHtml(t.fullName || t.username)}</option>`).join('');
|
||||||
|
} else {
|
||||||
|
csTeacherSelect.innerHTML = '<option value="">Ошибка: Не найден ID кафедры</option>';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка загрузки справочников:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDictionariesForSchedule();
|
||||||
|
|
||||||
|
// ===== Открытие / Закрытие оверлея =====
|
||||||
|
function openOverlay() {
|
||||||
|
csOverlay.classList.add('open');
|
||||||
|
document.body.style.overflow = 'hidden'; // Предотвращаем скролл страницы
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeOverlay() {
|
||||||
|
csOverlay.classList.remove('open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
hideAlert('create-schedule-alert');
|
||||||
|
hideAlert('save-schedules-alert');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTableVisibility() {
|
||||||
|
modalViewSchedules.style.display = preparedSchedules.length > 0 ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Кнопка «Создать запись» =====
|
||||||
|
btnCreateSchedule.addEventListener('click', () => {
|
||||||
|
if (localDepartmentId) {
|
||||||
|
csDepartmentIdInput.value = localDepartmentId;
|
||||||
|
} else {
|
||||||
|
showAlert('schedule-form-alert', 'Требуется перезайти (отсутствует ID кафедры)', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openOverlay();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Закрытие =====
|
||||||
|
modalCreateScheduleClose.addEventListener('click', closeOverlay);
|
||||||
|
|
||||||
|
csOverlay.addEventListener('click', (e) => {
|
||||||
|
// Закрыть по клику на затемнённый фон (но не по клику на содержимое модалок)
|
||||||
|
if (e.target === csOverlay || e.target.classList.contains('cs-overlay-scroll')) {
|
||||||
|
closeOverlay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && csOverlay.classList.contains('open')) {
|
||||||
|
closeOverlay();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Рендер таблицы =====
|
||||||
|
function renderPreparedSchedules() {
|
||||||
|
if (preparedSchedules.length === 0) {
|
||||||
|
preparedSchedulesTbody.innerHTML = '<tr><td colspan="10" class="loading-row">Нет записей</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
preparedSchedulesTbody.innerHTML = preparedSchedules.map((s, index) => {
|
||||||
|
const groupName = csGroups.find(g => g.id == s.groupId)?.name || s.groupId;
|
||||||
|
const subjectName = csSubjects.find(sub => sub.id == s.subjectsId)?.name || s.subjectsId;
|
||||||
|
const teacherName = csTeachers.find(t => t.id == s.teacherId)?.fullName
|
||||||
|
|| csTeachers.find(t => t.id == s.teacherId)?.username || s.teacherId;
|
||||||
|
const lessonTypeName = LESSON_TYPE_LABELS[s.lessonTypeId] || 'Неизвестно';
|
||||||
|
const semLabel = SEMESTER_LABELS[s.semesterType] || s.semesterType;
|
||||||
|
const periodDisplay = s.period.replace('-', '/');
|
||||||
|
const divText = s.division ? '✓' : '';
|
||||||
|
const hasError = !!s._errorMsg;
|
||||||
|
const rowStyle = hasError ? ' style="background: rgba(239, 68, 68, 0.08);"' : '';
|
||||||
|
let row = `
|
||||||
|
<tr${rowStyle}>
|
||||||
|
<td>${escapeHtml(periodDisplay)}</td>
|
||||||
|
<td>${escapeHtml(semLabel)}</td>
|
||||||
|
<td>${s.semester}</td>
|
||||||
|
<td>${escapeHtml(String(groupName))}</td>
|
||||||
|
<td>${escapeHtml(String(subjectName))}</td>
|
||||||
|
<td>${escapeHtml(lessonTypeName)}</td>
|
||||||
|
<td>${s.numberOfHours}</td>
|
||||||
|
<td>${divText}</td>
|
||||||
|
<td>${escapeHtml(String(teacherName))}</td>
|
||||||
|
<td><button type="button" class="btn-delete" data-index="${index}">Удалить</button></td>
|
||||||
|
</tr>`;
|
||||||
|
if (hasError) {
|
||||||
|
row += `<tr style="background: rgba(239, 68, 68, 0.05);">
|
||||||
|
<td colspan="10" style="color: var(--error); font-size: 0.85rem; padding: 0.4rem 0.85rem;">
|
||||||
|
⚠ ${escapeHtml(s._errorMsg)}
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Удаление строки из таблицы =====
|
||||||
|
preparedSchedulesTbody.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('btn-delete')) {
|
||||||
|
const idx = parseInt(e.target.getAttribute('data-index'), 10);
|
||||||
|
preparedSchedules.splice(idx, 1);
|
||||||
|
renderPreparedSchedules();
|
||||||
|
updateTableVisibility();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Очистка полей формы (частичная) =====
|
||||||
|
// НЕ очищаем select'ы — они остаются заполненными для удобства.
|
||||||
|
// Пользователь сам изменит нужные поля для следующей записи.
|
||||||
|
function clearFormFields() {
|
||||||
|
document.getElementById('cs-hours').value = '';
|
||||||
|
document.getElementById('cs-division').checked = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== Добавление записи в список =====
|
||||||
|
formCreateSchedule.addEventListener('submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
hideAlert('create-schedule-alert');
|
||||||
|
|
||||||
|
const depId = csDepartmentIdInput.value;
|
||||||
|
const period = document.getElementById('cs-period').value;
|
||||||
|
const semesterType = document.querySelector('input[name="csSemesterType"]:checked')?.value;
|
||||||
|
const semester = document.getElementById('cs-semester').value;
|
||||||
|
const groupId = csGroupSelect.value;
|
||||||
|
const subjectId = csSubjectSelect.value;
|
||||||
|
const lessonTypeId = document.getElementById('cs-lesson-type').value;
|
||||||
|
const hours = document.getElementById('cs-hours').value;
|
||||||
|
const division = document.getElementById('cs-division').checked;
|
||||||
|
const teacherId = csTeacherSelect.value;
|
||||||
|
|
||||||
|
if (!period || !semesterType || !semester || !groupId || !subjectId || !lessonTypeId || !hours || !teacherId) {
|
||||||
|
showAlert('create-schedule-alert', 'Заполните все обязательные поля', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRecord = {
|
||||||
|
departmentId: Number(depId),
|
||||||
|
semester: Number(semester),
|
||||||
|
groupId: Number(groupId),
|
||||||
|
subjectsId: Number(subjectId),
|
||||||
|
lessonTypeId: Number(lessonTypeId),
|
||||||
|
numberOfHours: Number(hours),
|
||||||
|
division: division,
|
||||||
|
teacherId: Number(teacherId),
|
||||||
|
semesterType: semesterType,
|
||||||
|
period: period
|
||||||
|
};
|
||||||
|
|
||||||
|
// Проверка на дубликат в уже добавленных записях
|
||||||
|
const isDuplicate = preparedSchedules.some(s =>
|
||||||
|
s.period === newRecord.period &&
|
||||||
|
s.semesterType === newRecord.semesterType &&
|
||||||
|
s.semester === newRecord.semester &&
|
||||||
|
s.groupId === newRecord.groupId &&
|
||||||
|
s.subjectsId === newRecord.subjectsId &&
|
||||||
|
s.lessonTypeId === newRecord.lessonTypeId &&
|
||||||
|
s.numberOfHours === newRecord.numberOfHours &&
|
||||||
|
s.division === newRecord.division &&
|
||||||
|
s.teacherId === newRecord.teacherId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
showAlert('create-schedule-alert', 'Такая запись уже есть в списке', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
preparedSchedules.push(newRecord);
|
||||||
|
|
||||||
|
clearFormFields();
|
||||||
|
|
||||||
|
showAlert('create-schedule-alert', 'Запись добавлена ✓', 'success');
|
||||||
|
setTimeout(() => hideAlert('create-schedule-alert'), 2000);
|
||||||
|
|
||||||
|
renderPreparedSchedules();
|
||||||
|
updateTableVisibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Сохранение в БД =====
|
||||||
|
btnSaveSchedules.addEventListener('click', async () => {
|
||||||
|
if (preparedSchedules.length === 0) {
|
||||||
|
showAlert('save-schedules-alert', 'Нет записей для сохранения', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btnSaveSchedules.disabled = true;
|
||||||
|
btnSaveSchedules.textContent = 'Сохранение...';
|
||||||
|
hideAlert('save-schedules-alert');
|
||||||
|
|
||||||
|
let errors = 0;
|
||||||
|
let saved = 0;
|
||||||
|
const failedRecords = [];
|
||||||
|
|
||||||
|
for (const record of preparedSchedules) {
|
||||||
|
try {
|
||||||
|
await api.post('/api/department/schedule/create', record);
|
||||||
|
saved++;
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Ошибка сохранения записи:', err);
|
||||||
|
errors++;
|
||||||
|
// Помечаем запись как дубликат, если бэк вернул соответствующую ошибку
|
||||||
|
const isDuplicate = err.status === 409 ||
|
||||||
|
(err.message && err.message.toLowerCase().includes('уже существует'));
|
||||||
|
failedRecords.push({
|
||||||
|
...record,
|
||||||
|
_errorMsg: isDuplicate
|
||||||
|
? 'Такая запись уже есть в базе данных'
|
||||||
|
: (err.message || 'Ошибка сохранения')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btnSaveSchedules.disabled = false;
|
||||||
|
btnSaveSchedules.textContent = 'Сохранить в БД';
|
||||||
|
|
||||||
|
if (errors === 0) {
|
||||||
|
showAlert('save-schedules-alert', `Все записи (${saved}) успешно сохранены!`, 'success');
|
||||||
|
preparedSchedules = [];
|
||||||
|
renderPreparedSchedules();
|
||||||
|
updateTableVisibility();
|
||||||
|
setTimeout(closeOverlay, 2000);
|
||||||
|
} else {
|
||||||
|
// Оставляем неудачные записи для повторной попытки / удаления
|
||||||
|
preparedSchedules = failedRecords;
|
||||||
|
renderPreparedSchedules();
|
||||||
|
if (saved > 0) {
|
||||||
|
showAlert('save-schedules-alert',
|
||||||
|
`Сохранено: ${saved}. Ошибок: ${errors}. Проблемные записи отмечены в таблице.`, 'error');
|
||||||
|
} else {
|
||||||
|
showAlert('save-schedules-alert',
|
||||||
|
`Не удалось сохранить. Ошибок: ${errors}. Проблемные записи отмечены в таблице.`, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ export async function initGroups() {
|
|||||||
populateEfSelects(educationForms);
|
populateEfSelects(educationForms);
|
||||||
await loadGroups();
|
await loadGroups();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
groupsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Ошибка загрузки данных</td></tr>';
|
groupsTbody.innerHTML = '<tr><td colspan="9" class="loading-row">Ошибка загрузки данных</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export async function initGroups() {
|
|||||||
allGroups = await api.get('/api/groups');
|
allGroups = await api.get('/api/groups');
|
||||||
applyGroupFilter();
|
applyGroupFilter();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
groupsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Ошибка загрузки</td></tr>';
|
groupsTbody.innerHTML = '<tr><td colspan="9" class="loading-row">Ошибка загрузки</td></tr>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export async function initGroups() {
|
|||||||
|
|
||||||
function renderGroups(groups) {
|
function renderGroups(groups) {
|
||||||
if (!groups || !groups.length) {
|
if (!groups || !groups.length) {
|
||||||
groupsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Нет групп</td></tr>';
|
groupsTbody.innerHTML = '<tr><td colspan="9" class="loading-row">Нет групп</td></tr>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
groupsTbody.innerHTML = groups.map(g => `
|
groupsTbody.innerHTML = groups.map(g => `
|
||||||
@@ -71,7 +71,9 @@ export async function initGroups() {
|
|||||||
<td>${escapeHtml(g.groupSize)}</td>
|
<td>${escapeHtml(g.groupSize)}</td>
|
||||||
<td><span class="badge badge-ef">${escapeHtml(g.educationFormName)}</span></td>
|
<td><span class="badge badge-ef">${escapeHtml(g.educationFormName)}</span></td>
|
||||||
<td>${g.departmentId || '-'}</td>
|
<td>${g.departmentId || '-'}</td>
|
||||||
|
<td>${g.enrollmentYear || '-'}</td>
|
||||||
<td>${g.course || '-'}</td>
|
<td>${g.course || '-'}</td>
|
||||||
|
<td>${g.semester || '-'}</td>
|
||||||
<td><button class="btn-delete" data-id="${g.id}">Удалить</button></td>
|
<td><button class="btn-delete" data-id="${g.id}">Удалить</button></td>
|
||||||
</tr>`).join('');
|
</tr>`).join('');
|
||||||
}
|
}
|
||||||
@@ -83,13 +85,13 @@ export async function initGroups() {
|
|||||||
const groupSize = document.getElementById('new-group-size').value;
|
const groupSize = document.getElementById('new-group-size').value;
|
||||||
const educationFormId = newGroupEfSelect.value;
|
const educationFormId = newGroupEfSelect.value;
|
||||||
const departmentId = document.getElementById('new-group-department').value;
|
const departmentId = document.getElementById('new-group-department').value;
|
||||||
const course = document.getElementById('new-group-course').value;
|
const enrollmentYear = document.getElementById('new-group-enrollment-year').value;
|
||||||
|
|
||||||
if (!name) { showAlert('create-group-alert', 'Введите название группы', 'error'); return; }
|
if (!name) { showAlert('create-group-alert', 'Введите название группы', 'error'); return; }
|
||||||
if (!groupSize) { showAlert('create-group-alert', 'Введите размер группы', 'error'); return; }
|
if (!groupSize) { showAlert('create-group-alert', 'Введите размер группы', 'error'); return; }
|
||||||
if (!educationFormId) { showAlert('create-group-alert', 'Выберите форму обучения', 'error'); return; }
|
if (!educationFormId) { showAlert('create-group-alert', 'Выберите форму обучения', 'error'); return; }
|
||||||
if (!departmentId) { showAlert('create-group-alert', 'Введите ID кафедры', 'error'); return; }
|
if (!departmentId) { showAlert('create-group-alert', 'Введите ID кафедры', 'error'); return; }
|
||||||
if (!course) { showAlert('create-group-alert', 'Введите курс', 'error'); return; }
|
if (!enrollmentYear) { showAlert('create-group-alert', 'Введите год начала обучения', 'error'); return; }
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await api.post('/api/groups', {
|
const data = await api.post('/api/groups', {
|
||||||
@@ -97,7 +99,7 @@ export async function initGroups() {
|
|||||||
groupSize: Number(groupSize),
|
groupSize: Number(groupSize),
|
||||||
educationFormId: Number(educationFormId),
|
educationFormId: Number(educationFormId),
|
||||||
departmentId: Number(departmentId),
|
departmentId: Number(departmentId),
|
||||||
course: Number(course)
|
enrollmentYear: Number(enrollmentYear)
|
||||||
});
|
});
|
||||||
showAlert('create-group-alert', `Группа "${escapeHtml(data.name || name)}" создана`, 'success');
|
showAlert('create-group-alert', `Группа "${escapeHtml(data.name || name)}" создана`, 'success');
|
||||||
createGroupForm.reset();
|
createGroupForm.reset();
|
||||||
|
|||||||
@@ -383,7 +383,7 @@ export async function initUsers() {
|
|||||||
const role = document.getElementById('new-role').value;
|
const role = document.getElementById('new-role').value;
|
||||||
const fullName = document.getElementById('new-fullname').value.trim();
|
const fullName = document.getElementById('new-fullname').value.trim();
|
||||||
const jobTitle = document.getElementById('new-jobtitle').value.trim();
|
const jobTitle = document.getElementById('new-jobtitle').value.trim();
|
||||||
const department = document.getElementById('new-department').value;
|
const departmentId = document.getElementById('new-department').value;
|
||||||
|
|
||||||
if (!username || !password || !fullName || !jobTitle || !departmentId) {
|
if (!username || !password || !fullName || !jobTitle || !departmentId) {
|
||||||
showAlert('create-alert', 'Заполните все поля', 'error');
|
showAlert('create-alert', 'Заполните все поля', 'error');
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<div class="card create-card">
|
<div class="card create-card">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||||
<h2>Запрос расписания кафедры</h2>
|
<h2>Запрос расписания кафедры</h2>
|
||||||
|
<button id="btn-create-schedule" class="btn-primary" style="margin-top: 0;">Создать запись</button>
|
||||||
|
</div>
|
||||||
<form id="department-schedule-form">
|
<form id="department-schedule-form">
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@@ -41,6 +44,143 @@
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ===== Общий оверлей для обеих модалок ===== -->
|
||||||
|
<div class="cs-overlay" id="cs-overlay">
|
||||||
|
<div class="cs-overlay-scroll">
|
||||||
|
|
||||||
|
<!-- Модалка 1: Форма создания записи -->
|
||||||
|
<div class="cs-modal cs-modal-form card" id="modal-create-schedule">
|
||||||
|
<div class="cs-modal-header">
|
||||||
|
<h2>Создать запись (к/ф)</h2>
|
||||||
|
<button class="btn-close-panel" id="modal-create-schedule-close" title="Закрыть (Esc)">×</button>
|
||||||
|
</div>
|
||||||
|
<form id="create-schedule-form">
|
||||||
|
<input type="hidden" id="cs-department-id" value="">
|
||||||
|
<div class="form-row"
|
||||||
|
style="align-items: flex-start; gap: 1rem; flex-wrap: wrap; width: 100%; justify-content: space-between;">
|
||||||
|
|
||||||
|
<div class="form-group" style="flex: 1 1 180px;">
|
||||||
|
<label for="cs-period">Учебный год</label>
|
||||||
|
<select id="cs-period" required>
|
||||||
|
<option value="">Выберите...</option>
|
||||||
|
<option value="2024-2025">2024/2025</option>
|
||||||
|
<option value="2025-2026">2025/2026</option>
|
||||||
|
<option value="2026-2027">2026/2027</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="flex: 1 1 180px;">
|
||||||
|
<label>Семестр</label>
|
||||||
|
<div style="display: flex; gap: 0.2rem;">
|
||||||
|
<label class="btn-checkbox">
|
||||||
|
<input type="radio" name="csSemesterType" value="autumn" required>
|
||||||
|
<span class="checkbox-btn">Осенний</span>
|
||||||
|
</label>
|
||||||
|
<label class="btn-checkbox">
|
||||||
|
<input type="radio" name="csSemesterType" value="spring" required>
|
||||||
|
<span class="checkbox-btn">Весенний</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="flex: 1 1 150px;">
|
||||||
|
<label for="cs-semester">Курс/Семестр (номер)</label>
|
||||||
|
<input type="number" id="cs-semester" required min="1" max="12" placeholder="Например: 1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="flex: 1 1 180px;">
|
||||||
|
<label for="cs-group">Группа</label>
|
||||||
|
<select id="cs-group" required>
|
||||||
|
<option value="">Загрузка...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="flex: 1 1 180px;">
|
||||||
|
<label for="cs-subject">Дисциплина</label>
|
||||||
|
<select id="cs-subject" required>
|
||||||
|
<option value="">Загрузка...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="flex: 1 1 180px;">
|
||||||
|
<label for="cs-lesson-type">Вид занятий</label>
|
||||||
|
<select id="cs-lesson-type" required>
|
||||||
|
<option value="">Выберите тип</option>
|
||||||
|
<option value="1">Лекция</option>
|
||||||
|
<option value="2">Практическая работа</option>
|
||||||
|
<option value="3">Лабораторная работа</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="flex: 1 1 150px;">
|
||||||
|
<label for="cs-hours">Часов (семестр)</label>
|
||||||
|
<input type="number" id="cs-hours" required min="1" max="500" placeholder="Например: 36">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="flex: 1 1 180px;">
|
||||||
|
<label>Деление на подгруппы</label>
|
||||||
|
<div style="display: flex; gap: 0.5rem; align-items: center; height: 42px;">
|
||||||
|
<label class="btn-checkbox" style="width:100%;">
|
||||||
|
<input type="checkbox" id="cs-division" value="true">
|
||||||
|
<span class="checkbox-btn" style="width:100%; text-align:center;">Есть деление</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="flex: 1 1 250px;">
|
||||||
|
<label for="cs-teacher">Преподаватель</label>
|
||||||
|
<select id="cs-teacher" required>
|
||||||
|
<option value="">Выберите преподавателя</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" style="flex: 0 0 auto; display:flex; align-items: flex-end;">
|
||||||
|
<button type="submit" class="btn-primary" style="white-space: nowrap;">Добавить в список</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-alert" id="create-schedule-alert" role="alert" style="margin-top: 1rem;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Модалка 2: Таблица подготовленных записей -->
|
||||||
|
<div class="cs-modal cs-modal-table card" id="modal-view-schedules" style="display: none;">
|
||||||
|
<div class="cs-modal-header">
|
||||||
|
<h2>Подготовленные записи</h2>
|
||||||
|
<div style="display:flex; gap: 0.75rem; align-items:center;">
|
||||||
|
<button id="btn-save-schedules" class="btn-primary">Сохранить в БД</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-alert" id="save-schedules-alert" role="alert" style="margin-bottom: 1rem;"></div>
|
||||||
|
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table id="prepared-schedules-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Уч. год</th>
|
||||||
|
<th>Семестр</th>
|
||||||
|
<th>№</th>
|
||||||
|
<th>Группа</th>
|
||||||
|
<th>Дисциплина</th>
|
||||||
|
<th>Вид</th>
|
||||||
|
<th>Часы</th>
|
||||||
|
<th>Деление</th>
|
||||||
|
<th>Преподаватель</th>
|
||||||
|
<th>Действие</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="prepared-schedules-tbody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="10" class="loading-row">Нет записей</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="table-wrap" id="schedule-blocks-container">
|
<div class="table-wrap" id="schedule-blocks-container">
|
||||||
<!-- Сгенерированные блоки таблиц будут появляться здесь -->
|
<!-- Сгенерированные блоки таблиц будут появляться здесь -->
|
||||||
</div>
|
</div>
|
||||||
@@ -22,8 +22,8 @@
|
|||||||
<input type="number" id="new-group-department" placeholder="ID" required>
|
<input type="number" id="new-group-department" placeholder="ID" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="new-group-course">Курс</label>
|
<label for="new-group-enrollment-year">Год начала обучения</label>
|
||||||
<input type="number" id="new-group-course" placeholder="1-6" min="1" max="6" required>
|
<input type="number" id="new-group-enrollment-year" placeholder="2023" min="2000" max="2100" required>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-primary">Создать</button>
|
<button type="submit" class="btn-primary">Создать</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -50,13 +50,15 @@
|
|||||||
<th>Численность (чел.)</th>
|
<th>Численность (чел.)</th>
|
||||||
<th>Форма обучения</th>
|
<th>Форма обучения</th>
|
||||||
<th>ID кафедры</th>
|
<th>ID кафедры</th>
|
||||||
|
<th>Год начала</th>
|
||||||
<th>Курс</th>
|
<th>Курс</th>
|
||||||
|
<th>Семестр</th>
|
||||||
<th>Действия</th>
|
<th>Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="groups-tbody">
|
<tbody id="groups-tbody">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="7" class="loading-row">Загрузка...</td>
|
<td colspan="9" class="loading-row">Загрузка...</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<input type="text" id="new-jobtitle" placeholder="Студент / Доцент" required>
|
<input type="text" id="new-jobtitle" placeholder="Студент / Доцент" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="new-department">Кафедра</label>
|
<label for="new-department">ID Кафедры</label>
|
||||||
<input type="number" id="new-department" placeholder="ID" required>
|
<input type="number" id="new-department" placeholder="ID" required>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn-primary">Создать</button>
|
<button type="submit" class="btn-primary">Создать</button>
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
<th>Имя пользователя</th>
|
<th>Имя пользователя</th>
|
||||||
<th>ФИО</th>
|
<th>ФИО</th>
|
||||||
<th>Должность</th>
|
<th>Должность</th>
|
||||||
<th>ID кафедры</th>
|
<th>Кафедра</th>
|
||||||
<th>Роль</th>
|
<th>Роль</th>
|
||||||
<th colspan="2">Действия</th>
|
<th colspan="2">Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -143,6 +143,7 @@
|
|||||||
|
|
||||||
if (data.token) localStorage.setItem('token', data.token);
|
if (data.token) localStorage.setItem('token', data.token);
|
||||||
if (data.role) localStorage.setItem('role', data.role);
|
if (data.role) localStorage.setItem('role', data.role);
|
||||||
|
if (data.departmentId) localStorage.setItem('departmentId', data.departmentId);
|
||||||
|
|
||||||
const redirect = data.redirect || '/';
|
const redirect = data.redirect || '/';
|
||||||
setTimeout(() => { window.location.href = redirect; }, 400);
|
setTimeout(() => { window.location.href = redirect; }, 400);
|
||||||
|
|||||||
120
tz2.md
Normal file
120
tz2.md
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
# План выполнения работ по новым интерфейсам расписания
|
||||||
|
|
||||||
|
На основе предоставленного технического задания составлен следующий детализированный план разработки макетов и функционала новой подсистемы составления и просмотра расписания.
|
||||||
|
|
||||||
|
## 1. Вкладка "Загрузка аудиторий"
|
||||||
|
|
||||||
|
**Концепция:** Динамическая таблица, визуализирующая текущее использование аудиторного фонда в конкретную учебную неделю.
|
||||||
|
|
||||||
|
### Интерфейс
|
||||||
|
* **Сетка данных:**
|
||||||
|
* **Столбцы:** Аудитории.
|
||||||
|
* **Строки:** Время пар (расписание звонков).
|
||||||
|
* **Ячейки:** Информация о проходящем занятии (Группа, Преподаватель, Дисциплина).
|
||||||
|
* **Элементы управления:**
|
||||||
|
* **Календарь недель:** Выпадающий список или слайдер для переключения между учебными неделями семестра. Учитывает изменения в графике (сессии, приезд заочников и т.д.).
|
||||||
|
* **Фильтр аудиторий:** Чекбоксы, мультиселект или группировка по корпусам/типам, позволяющие скрывать неотображаемые аудитории для удобства просмотра.
|
||||||
|
|
||||||
|
### Функционал
|
||||||
|
* Отображение данных на основе сохраненного расписания из БД с привязкой к выбранной неделе.
|
||||||
|
* **Интерактивность:** Возможность клика по пустой ячейке для добавления нового занятия. Открывается модальное окно с предзаполненными полями `Аудитория`, `Время` и `Неделя`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Вкладка "Загруженность преподавателей"
|
||||||
|
|
||||||
|
**Концепция:** Интерфейс, дублирующий логику загрузки аудиторий, но с фокусом на профессорско-преподавательский состав (ППС).
|
||||||
|
|
||||||
|
### Интерфейс
|
||||||
|
* **Сетка данных:**
|
||||||
|
* **Столбцы:** Список преподавателей.
|
||||||
|
* **Строки:** Время пар.
|
||||||
|
* **Ячейки:** Информация о занятии (Группа, Аудитория, Дисциплина).
|
||||||
|
* **Элементы управления:**
|
||||||
|
* Календарь недель (аналогично аудиториям).
|
||||||
|
* Поиск/фильтрация по ФИО преподавателя или кафедре.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Рабочее окно составителя расписания
|
||||||
|
|
||||||
|
**Концепция:** Основной инструмент диспетчера. Интерактивная среда для распределения нагрузки.
|
||||||
|
|
||||||
|
### Интерфейс
|
||||||
|
* **Сетка расписания:**
|
||||||
|
* **Столбцы:** Учебные группы.
|
||||||
|
* **Строки:** Время пар.
|
||||||
|
* **Панель нагрузки:** Боковая панель или вызываемое окно со списком нераспределенных предметов для выбранной группы/курса.
|
||||||
|
|
||||||
|
### Алгоритм работы (User Flow)
|
||||||
|
1. **Старт:** Диспетчер кликает в конкретную ячейку сетки (выбирает группу и время проведения пары).
|
||||||
|
2. **Выбор предмета:** Появляется меню со списком предметов, которые необходимо поставить данной группе. Диспетчер выбирает нужный.
|
||||||
|
3. **Выбор преподавателя и проверка его занятости:**
|
||||||
|
* Система автоматически подтягивает преподавателя (или список возможных), закрепленного за этой дисциплиной.
|
||||||
|
* Отображается **карта свободных слотов преподавателя**, чтобы убедиться, что он не ведет пару в это же время у другой группы (предотвращение накладок).
|
||||||
|
4. **Выбор аудитории (Умный подбор):**
|
||||||
|
* Если преподаватель свободен, всплывает **карта загрузки аудиторий**.
|
||||||
|
* Аудитории отображаются с цветовой индикацией:
|
||||||
|
* 🟢 **Зеленый:** Аудитория свободна и её характеристики (тип, вместимость) полностью подходят для занятия.
|
||||||
|
* 🟡 **Желтый:** Аудитория свободна, но не подходит по требованиям (например, это лекционный зал для маленькой группы, или обычная аудитория для компьютерного практикума).
|
||||||
|
* 🔴 **Красный:** Аудитория занята (при наведении или клике показывается, кто именно там занимается).
|
||||||
|
* Диспетчер выбирает подходящую аудиторию.
|
||||||
|
5. **Финал:** Занятие фиксируется в сетке, предмет вычитается из пула нераспределенной нагрузки.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Этапы реализации и анализ архитектуры БД
|
||||||
|
|
||||||
|
На основе анализа существующей базы данных проекта (см. `docs/DATABASE.md`) выявлено, что значительная часть необходимых данных уже присутствует, однако для полного удовлетворения ТЗ требуются точечные доработки структуры БД.
|
||||||
|
|
||||||
|
### Анализ требований ТЗ и текущей БД
|
||||||
|
|
||||||
|
1. **"Аудитории: нет пункта о том, в каком корпусе она находится"**:
|
||||||
|
* **Текущее состояние в БД**: В таблице `classrooms` **уже существуют** поля `building` (Корпус) и `floor` (Этаж).
|
||||||
|
* **Вывод**: Добавление характеристик корпуса в БД не требуется. Информацию нужно просто вывести через Backend API на Frontend.
|
||||||
|
2. **Динамическое расписание и календарь недель ("закончился семестр у магистров", "заочники")**:
|
||||||
|
* **Текущее состояние в БД**: Таблица `lessons` содержит поле `week` (с текстовыми значениями `Верхняя / Нижняя / Обе`), что подразумевает статический цикличный график (раз в 2 недели).
|
||||||
|
* **Чего не хватает**: Текущая схема не позволяет гибко привязывать занятия к конкретным календарным датам или конкретным учебным неделям семестра (например, с 1 по 18 неделю).
|
||||||
|
* **Вывод**: Потребуется миграция БД для внедрения календаря (например, таблица `academic_weeks` или изменение структуры `lessons`).
|
||||||
|
3. **Умный подбор аудиторий (желтая индикация — "не подходит оборудование")**:
|
||||||
|
* **Текущее состояние в БД**: Есть таблица `classroom_equipments`, описывающая инвентарь аудитории.
|
||||||
|
* **Чего не хватает**: В системе отсутствует информация о том, какое оборудование **требуется** для конкретной дисциплины.
|
||||||
|
* **Вывод**: Необходимо добавить новую связующую таблицу (например, `subject_equipments` или `lesson_type_equipments`), чтобы алгоритм мог сопоставлять требования предмета с оснащением выбранной аудитории.
|
||||||
|
4. **Списки нагрузки для распределения**:
|
||||||
|
* **Текущее состояние в БД**: Присутствует таблица `schedule_data` со столбцом `number_of_hours` (часы, подлежащие распределению).
|
||||||
|
* **Вывод**: Архитектура готова. Потребуется лишь бизнес-логика для связывания созданных записей `lessons` с нераспределенной нагрузкой `schedule_data` (для вычета распределенных часов).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Детализированный план реализации
|
||||||
|
|
||||||
|
#### Этап 1: Доработка базы данных (Flyway миграции)
|
||||||
|
* **Миграция БД (Календарь):** Проектирование и создание механизма привязки расписания к конкретным неделям/датам, отход от жесткой привязки "Верхняя/Нижняя".
|
||||||
|
* **Миграция БД (Оборудование):** Создание таблицы для хранения технических требований дисциплин к аудиториям (`subject_equipments`), чтобы стала возможна "желтая" индикация.
|
||||||
|
* *(Напоминание: все миграции создаются как новые файлы `V2__...sql`, `V3__...sql` в директории `db/migration/`, изменение `V1__init.sql` запрещено).*
|
||||||
|
|
||||||
|
#### Этап 2: Разработка Backend API (Java Spring Boot)
|
||||||
|
* **Эндпоинты получения видов (View API):**
|
||||||
|
* API для сетки аудиторий: агрегация занятий по аудиториям с учетом выбранной недели.
|
||||||
|
* API для сетки преподавателей: агрегация занятий по преподавателям.
|
||||||
|
* API нераспределенной нагрузки: получение остатка часов из `schedule_data` для выбранной группы.
|
||||||
|
* **Интеллектуальные алгоритмы проверок (Service Layer):**
|
||||||
|
* Логика проверки накладок преподавателей.
|
||||||
|
* Алгоритм "Цветофор" для аудитории:
|
||||||
|
* Красный (занятость по времени).
|
||||||
|
* Желтый (сопоставление вместимости `capacity` с `group_size` + проверка наличия нужного оборудования).
|
||||||
|
* Зеленый (все проверки пройдены).
|
||||||
|
|
||||||
|
#### Этап 3: UI-разработка (Frontend)
|
||||||
|
* Верстка трех основных табличных сеток (Audience Load, Teacher Workload, Schedule Maker).
|
||||||
|
* Реализация календаря/селектора недель (влияющего на выводимые данные).
|
||||||
|
* Программирование интерактивного Flow диспетчера в Vanilla JS:
|
||||||
|
1. Клик в ячейку.
|
||||||
|
2. Вызов списка нагрузки -> выбор предмета.
|
||||||
|
3. Отображение свободных слотов преподавателя.
|
||||||
|
4. Вывод карты аудиторий с динамической цветовой индикацией.
|
||||||
|
5. Сохранение результата.
|
||||||
|
|
||||||
|
#### Этап 4: Интеграция и стабилизация
|
||||||
|
* Интеграция Front и Back-частей.
|
||||||
|
* Сквозное тестирование сценариев создания, редактирования и удаления занятий с пересчетом часов нагрузки.
|
||||||
Reference in New Issue
Block a user