Compare commits
15 Commits
678cf94ad3
...
Create-Les
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bd21757d6 | ||
|
|
7a729a782d | ||
|
|
be35733e4d | ||
|
|
169f7435b1 | ||
|
|
2563c769de | ||
|
|
47039ee878 | ||
|
|
88f1abfe25 | ||
|
|
2004766855 | ||
|
|
0e03dcb2d6 | ||
|
|
438a16c383 | ||
|
|
9d06c69e2b | ||
|
|
684273de50 | ||
|
|
772b110762 | ||
|
|
18f47c6d3d | ||
|
|
007b4fb619 |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,14 +3,7 @@ db/data/
|
||||
|
||||
# Игнорируем секреты
|
||||
.env
|
||||
!GEMINI.md
|
||||
!AGENTS.md
|
||||
|
||||
# Игнорируем системные папки IDE (если редактируете с ПК)
|
||||
.idea/
|
||||
.vscode/
|
||||
*.DS_Store
|
||||
.agent/
|
||||
# Игнорируем временные файлы сборки (на будущее)
|
||||
backend/target/
|
||||
backend/build/
|
||||
|
||||
0
backend/Dockerfile
Normal file → Executable file
0
backend/Dockerfile
Normal file → Executable file
0
backend/pom.xml
Normal file → Executable file
0
backend/pom.xml
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/Application.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/Application.java
Normal file → Executable file
2
backend/src/main/java/com/magistr/app/README.md
Normal file → Executable file
2
backend/src/main/java/com/magistr/app/README.md
Normal file → Executable file
@@ -1 +1 @@
|
||||
КОММИТ12
|
||||
тест
|
||||
0
backend/src/main/java/com/magistr/app/config/AppConfig.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/config/AppConfig.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/config/DataInitializer.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/config/DataInitializer.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/AuthController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/AuthController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/ClassroomController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/ClassroomController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/EducationFormController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/EducationFormController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/EquipmentController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/EquipmentController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/GroupController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/GroupController.java
Normal file → Executable file
285
backend/src/main/java/com/magistr/app/controller/LessonsController.java
Executable file
285
backend/src/main/java/com/magistr/app/controller/LessonsController.java
Executable file
@@ -0,0 +1,285 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.dto.CreateLessonRequest;
|
||||
import com.magistr.app.dto.LessonResponse;
|
||||
import com.magistr.app.model.*;
|
||||
import com.magistr.app.repository.*;
|
||||
import com.magistr.app.utils.DayAndWeekValidator;
|
||||
import com.magistr.app.utils.TypeAndFormatLessonValidator;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users/lessons")
|
||||
public class LessonsController {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(LessonsController.class);
|
||||
|
||||
private final LessonRepository lessonRepository;
|
||||
private final UserRepository teacherRepository;
|
||||
private final GroupRepository groupRepository;
|
||||
private final SubjectRepository subjectRepository;
|
||||
private final EducationFormRepository educationFormRepository;
|
||||
private final ClassroomRepository classroomRepository;
|
||||
|
||||
public LessonsController(LessonRepository lessonRepository, UserRepository teacherRepository, GroupRepository groupRepository, SubjectRepository subjectRepository, EducationFormRepository educationForm, ClassroomRepository classroomRepository) {
|
||||
this.lessonRepository = lessonRepository;
|
||||
this.teacherRepository = teacherRepository;
|
||||
this.groupRepository = groupRepository;
|
||||
this.subjectRepository = subjectRepository;
|
||||
this.educationFormRepository = educationForm;
|
||||
this.classroomRepository = classroomRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
public ResponseEntity<?> createLesson(@RequestBody CreateLessonRequest request) {
|
||||
//Полное логирование входящего запроса
|
||||
logger.info("Получен запрос на создание занятия: teacherId={}, groupId={}, lessonTypeId={}, day={}, week={}, time={}",
|
||||
request.getTeacherId(), request.getGroupId(), request.getSubjectId(), request.getDay(), request.getWeek(), request.getTime());
|
||||
|
||||
//Проверка teacherId
|
||||
if (request.getTeacherId() == null || request.getTeacherId() == 0) {
|
||||
String errorMessage = "ID преподавателя обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка groupId
|
||||
if (request.getGroupId() == null || request.getGroupId() == 0) {
|
||||
String errorMessage = "ID группы обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка subjectId
|
||||
if (request.getSubjectId() == null || request.getSubjectId() == 0) {
|
||||
String errorMessage = "ID предмета обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка lessonFormat
|
||||
if (request.getLessonFormat() == null || request.getLessonFormat().isBlank()) {
|
||||
String errorMessage = "Выбор формата занятия обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
} else if(!TypeAndFormatLessonValidator.isValidFormat(request.getLessonFormat())){
|
||||
String errorMessage = "Некорректный формат занятий. " + TypeAndFormatLessonValidator.getValidFormatsMessage();
|
||||
logger.info("Ошибка валидации формата: '{}' - {}", request.getLessonFormat(), errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка typeLesson
|
||||
if (request.getTypeLesson() == null || request.getTypeLesson().isBlank()) {
|
||||
String errorMessage = "Выбор типа занятия обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
} else if(!TypeAndFormatLessonValidator.isValidType(request.getTypeLesson())){
|
||||
String errorMessage = "Некорректный тип занятия. " + TypeAndFormatLessonValidator.getValidTypesMessage();
|
||||
logger.info("Ошибка валидации типа: '{}' - {}", request.getTypeLesson(), errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка classroomId
|
||||
if (request.getClassroomId() == null || request.getClassroomId() == 0) {
|
||||
String errorMessage = "ID аудитории обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка day
|
||||
if (request.getDay() == null || request.getDay().isBlank()) {
|
||||
String errorMessage = "Выбор дня обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
} else if(!DayAndWeekValidator.isValidDay(request.getDay())){
|
||||
String errorMessage = "Некорректный день недели. " + DayAndWeekValidator.getValidDaysMessage();
|
||||
logger.info("Ошибка валидации дня: '{}' - {}", request.getDay(), errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка week
|
||||
if (request.getWeek() == null || request.getWeek().isBlank()) {
|
||||
String errorMessage = "Выбор недели обязателен";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
} else if(!DayAndWeekValidator.isValidWeek(request.getWeek())){
|
||||
String errorMessage = "Некорректная неделя. " + DayAndWeekValidator.getValidWeekMessage();
|
||||
logger.info("Ошибка валидации недели: '{}' - {}", request.getWeek(), errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Проверка time
|
||||
if (request.getTime() == null || request.getTime().isBlank()) {
|
||||
String errorMessage = "Время обязательно";
|
||||
logger.info("Ошибка валидации: {}", errorMessage);
|
||||
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
|
||||
}
|
||||
|
||||
//Сохранение полученных данных и формирование ответа клиенту
|
||||
try {
|
||||
Lesson lesson = new Lesson();
|
||||
lesson.setTeacherId(request.getTeacherId());
|
||||
lesson.setSubjectId(request.getSubjectId());
|
||||
lesson.setGroupId(request.getGroupId());
|
||||
lesson.setLessonFormat(request.getLessonFormat());
|
||||
lesson.setTypeLesson(request.getTypeLesson());
|
||||
lesson.setClassroomId(request.getClassroomId());
|
||||
lesson.setDay(request.getDay());
|
||||
lesson.setWeek(request.getWeek());
|
||||
lesson.setTime(request.getTime());
|
||||
|
||||
Lesson savedLesson = lessonRepository.save(lesson);
|
||||
|
||||
Map<String, Object> response = new LinkedHashMap<>();
|
||||
response.put("id", savedLesson.getId());
|
||||
response.put("teacherId", savedLesson.getTeacherId());
|
||||
response.put("groupId", savedLesson.getGroupId());
|
||||
response.put("subjectId", savedLesson.getSubjectId());
|
||||
response.put("formatLesson", savedLesson.getLessonFormat());
|
||||
response.put("typeLesson", savedLesson.getTypeLesson());
|
||||
response.put("classroomId", savedLesson.getClassroomId());
|
||||
response.put("day", savedLesson.getDay());
|
||||
response.put("week", savedLesson.getWeek());
|
||||
response.put("time", savedLesson.getTime());
|
||||
|
||||
logger.info("Занятие успешно создано с ID: {}", savedLesson.getId());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при сохранении занятия: {}", e.getMessage(),e);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message", "Произошла ошибка при создании занятия: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<LessonResponse> getAllLessons() {
|
||||
logger.info("Запрос на получение всех занятий");
|
||||
|
||||
try {
|
||||
List<Lesson> lessons = lessonRepository.findAll();
|
||||
|
||||
List<LessonResponse> response = lessons.stream()
|
||||
.map(lesson -> {
|
||||
String teacherName = teacherRepository.findById(lesson.getTeacherId())
|
||||
.map(User::getUsername)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
StudentGroup group = groupRepository.findById(lesson.getGroupId()).orElse(null);
|
||||
String groupName = groupRepository.findById(lesson.getGroupId())
|
||||
.map(StudentGroup::getName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
String educationFormName = "Неизвестно";
|
||||
if(group != null && group.getEducationForm() != null) {
|
||||
Long educationFormId = group.getEducationForm().getId();
|
||||
educationFormName = educationFormRepository.findById(educationFormId)
|
||||
.map(EducationForm::getName)
|
||||
.orElse("Неизвестно");
|
||||
}
|
||||
|
||||
String subjectName = subjectRepository.findById(lesson.getSubjectId())
|
||||
.map(Subject::getName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
String classroomName = classroomRepository.findById(lesson.getClassroomId())
|
||||
.map(Classroom::getName)
|
||||
.orElse("Неизвестно");
|
||||
|
||||
return new LessonResponse(
|
||||
lesson.getId(),
|
||||
teacherName,
|
||||
groupName,
|
||||
classroomName,
|
||||
educationFormName,
|
||||
subjectName,
|
||||
lesson.getTypeLesson(),
|
||||
lesson.getLessonFormat(),
|
||||
lesson.getDay(),
|
||||
lesson.getWeek(),
|
||||
lesson.getTime()
|
||||
);
|
||||
})
|
||||
.toList();
|
||||
|
||||
logger.info("Получено {} занятий", lessons.size());
|
||||
return response;
|
||||
} catch (Exception e) {
|
||||
logger.error("Ошибка при получении списка всех занятий: {}", e.getMessage(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/{teacherId}")
|
||||
public ResponseEntity<?> getLessonsById(@PathVariable Long teacherId) {
|
||||
logger.info("Запрос на получение занятий для преподавателя с ID: {}", teacherId);
|
||||
try {
|
||||
List<Lesson> lessons = lessonRepository.findByTeacherId(teacherId);
|
||||
|
||||
if(lessons.isEmpty()) {
|
||||
logger.info("У преподавателя с ID {} нет занятий", teacherId);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "У преподавателя с ID " + teacherId +" нет занятий.",
|
||||
"lessons", Collections.emptyList()
|
||||
));
|
||||
}
|
||||
|
||||
List<LessonResponse> lessonResponses = lessons.stream()
|
||||
.map(l -> new LessonResponse(
|
||||
l.getId(),
|
||||
l.getTeacherId(),
|
||||
l.getSubjectId(),
|
||||
l.getGroupId(),
|
||||
l.getDay(),
|
||||
l.getWeek(),
|
||||
l.getTime()
|
||||
))
|
||||
.toList();
|
||||
logger.info("Найдено {} занянтий для преподавателя с ID: {}", lessonResponses.size(), teacherId);
|
||||
return ResponseEntity.ok(lessonResponses);
|
||||
} catch (Exception e ){
|
||||
logger.error("Ошибка при получении занятий для преподавателя с ID {}: {}", teacherId, e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(Map.of("message", "Ошибка при поиске занятий: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/debug/subjects")
|
||||
public ResponseEntity<?> debugSubjects() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
// Через JPA репозиторий
|
||||
List<Subject> allSubjects = subjectRepository.findAll();
|
||||
result.put("jpa_count", allSubjects.size());
|
||||
result.put("jpa_subjects", allSubjects.stream()
|
||||
.map(s -> Map.of("id", s.getId(), "name", s.getName()))
|
||||
.toList());
|
||||
|
||||
// Проверка конкретных ID
|
||||
Map<Long, Boolean> existenceCheck = new HashMap<>();
|
||||
for (long id = 1; id <= 6; id++) {
|
||||
boolean exists = subjectRepository.existsById(id);
|
||||
existenceCheck.put(id, exists);
|
||||
}
|
||||
result.put("existence_check", existenceCheck);
|
||||
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
//Тестовый запрос на проверку доступности контроллера
|
||||
@GetMapping("/ping")
|
||||
public String ping() {
|
||||
logger.debug("Получен ping запрос");
|
||||
String response = "pong";
|
||||
logger.debug("Ответ на ping: {}", response);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
0
backend/src/main/java/com/magistr/app/controller/SubjectController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/SubjectController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/TeacherSubjectController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/TeacherSubjectController.java
Normal file → Executable file
@@ -1,67 +0,0 @@
|
||||
package com.magistr.app.controller;
|
||||
|
||||
import com.magistr.app.dto.CreateLessonRequest;
|
||||
import com.magistr.app.dto.LessonResponse;
|
||||
import com.magistr.app.model.Lesson;
|
||||
import com.magistr.app.repository.LessonRepository;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users/test")
|
||||
public class TestController {
|
||||
|
||||
private final LessonRepository lessonRepository;
|
||||
|
||||
public TestController(LessonRepository lessonRepository) {
|
||||
this.lessonRepository = lessonRepository;
|
||||
}
|
||||
|
||||
@PostMapping("/create")
|
||||
public ResponseEntity<?> createLesson(@RequestBody CreateLessonRequest request) {
|
||||
if (request.getTeacherId() == null || request.getTeacherId() == 0) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "ID преподавателя обязателен"));
|
||||
}
|
||||
if (request.getGroupId() == null || request.getGroupId() == 0) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "ID группы обязателен"));
|
||||
}
|
||||
if (request.getDisciplineId() == null || request.getDisciplineId() == 0) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "ID предмета обязателен"));
|
||||
}
|
||||
if (request.getDay() == null || request.getDay().isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Выбор дня обязателен"));
|
||||
}
|
||||
if (request.getWeek() == null || request.getWeek().isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Выбор недели обязателен"));
|
||||
}
|
||||
if (request.getTime() == null || request.getTime().isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("message", "Время обязательно"));
|
||||
}
|
||||
|
||||
Lesson lesson = new Lesson();
|
||||
lesson.setTeacherId(request.getTeacherId());
|
||||
lesson.setDisciplineId(request.getDisciplineId());
|
||||
lesson.setGroupId(request.getGroupId());
|
||||
lesson.setDay(request.getDay());
|
||||
lesson.setWeek(request.getWeek());
|
||||
lesson.setTime(request.getTime());
|
||||
lessonRepository.save(lesson);
|
||||
|
||||
return ResponseEntity.ok(new LessonResponse(lesson.getId(), lesson.getDay(), lesson.getWeek(), lesson.getTime()));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<LessonResponse> getAllLessons() {
|
||||
return lessonRepository.findAll().stream()
|
||||
.map(l -> new LessonResponse(l.getId(), l.getTeacherId(), l.getDisciplineId(), l.getDay(), l.getWeek(), l.getTime()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@GetMapping("/ping")
|
||||
public String ping() {
|
||||
return "pong";
|
||||
}
|
||||
}
|
||||
0
backend/src/main/java/com/magistr/app/controller/UserController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/controller/UserController.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/ClassroomRequest.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/ClassroomRequest.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/ClassroomResponse.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/ClassroomResponse.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/CreateGroupRequest.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/CreateGroupRequest.java
Normal file → Executable file
37
backend/src/main/java/com/magistr/app/dto/CreateLessonRequest.java
Normal file → Executable file
37
backend/src/main/java/com/magistr/app/dto/CreateLessonRequest.java
Normal file → Executable file
@@ -4,7 +4,10 @@ public class CreateLessonRequest {
|
||||
|
||||
private Long teacherId;
|
||||
private Long groupId;
|
||||
private Long disciplineId;
|
||||
private Long subjectId;
|
||||
private String lessonFormat;
|
||||
private String typeLesson;
|
||||
private Long classroomId;
|
||||
private String day;
|
||||
private String week;
|
||||
private String time;
|
||||
@@ -28,12 +31,36 @@ public class CreateLessonRequest {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public Long getDisciplineId() {
|
||||
return disciplineId;
|
||||
public Long getSubjectId() {
|
||||
return subjectId;
|
||||
}
|
||||
|
||||
public void setDisciplineId(Long disciplineId) {
|
||||
this.disciplineId= disciplineId;
|
||||
public void setSubjectId(Long subjectId) {
|
||||
this.subjectId = subjectId;
|
||||
}
|
||||
|
||||
public String getLessonFormat() {
|
||||
return lessonFormat;
|
||||
}
|
||||
|
||||
public void setLessonFormat(String lessonFormat) {
|
||||
this.lessonFormat = lessonFormat;
|
||||
}
|
||||
|
||||
public String getTypeLesson() {
|
||||
return typeLesson;
|
||||
}
|
||||
|
||||
public void setTypeLesson(String typeLesson) {
|
||||
this.typeLesson = typeLesson;
|
||||
}
|
||||
|
||||
public Long getClassroomId() {
|
||||
return classroomId;
|
||||
}
|
||||
|
||||
public void setClassroomId(Long classroomId) {
|
||||
this.classroomId = classroomId;
|
||||
}
|
||||
|
||||
public String getDay() {
|
||||
|
||||
0
backend/src/main/java/com/magistr/app/dto/CreateUserRequest.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/CreateUserRequest.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/GroupResponse.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/GroupResponse.java
Normal file → Executable file
104
backend/src/main/java/com/magistr/app/dto/LessonResponse.java
Normal file → Executable file
104
backend/src/main/java/com/magistr/app/dto/LessonResponse.java
Normal file → Executable file
@@ -1,11 +1,23 @@
|
||||
package com.magistr.app.dto;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public class LessonResponse {
|
||||
|
||||
private Long id;
|
||||
private Long teacherId;
|
||||
private String teacherName;
|
||||
private Long groupId;
|
||||
private Long disciplineId;
|
||||
private String groupName;
|
||||
private String educationFormName;
|
||||
private Long subjectId;
|
||||
private String subjectName;
|
||||
private String lessonFormat;
|
||||
private String typeLesson;
|
||||
private Long classroomId;
|
||||
private String classroomName;
|
||||
private String day;
|
||||
private String week;
|
||||
private String time;
|
||||
@@ -13,17 +25,25 @@ public class LessonResponse {
|
||||
public LessonResponse() {
|
||||
}
|
||||
|
||||
public LessonResponse(Long disciplineId, String day, String week, String time) {
|
||||
this.disciplineId = disciplineId;
|
||||
public LessonResponse(Long id, Long teacherId, Long groupId, Long subjectId, String day, String week, String time) {
|
||||
this.id = id;
|
||||
this.teacherId = teacherId;
|
||||
this.groupId = groupId;
|
||||
this.subjectId = subjectId;
|
||||
this.day = day;
|
||||
this.week = week;
|
||||
this.time = time;
|
||||
}
|
||||
|
||||
public LessonResponse(Long id, Long teacherId, Long disciplineId, String day, String week, String time) {
|
||||
public LessonResponse(Long id, String teacherName, String groupName, String classroomName, String educationFormName, String subjectName, String typeLesson, String lessonFormat, String day, String week, String time) {
|
||||
this.id = id;
|
||||
this.teacherId = teacherId;
|
||||
this.disciplineId = disciplineId;
|
||||
this.teacherName = teacherName;
|
||||
this.groupName = groupName;
|
||||
this.classroomName = classroomName;
|
||||
this.educationFormName = educationFormName;
|
||||
this.subjectName = subjectName;
|
||||
this.typeLesson = typeLesson;
|
||||
this.lessonFormat = lessonFormat;
|
||||
this.day = day;
|
||||
this.week = week;
|
||||
this.time = time;
|
||||
@@ -45,6 +65,14 @@ public class LessonResponse {
|
||||
this.teacherId = teacherId;
|
||||
}
|
||||
|
||||
public String getTeacherName() {
|
||||
return teacherName;
|
||||
}
|
||||
|
||||
public void setTeacherName(String teacherName) {
|
||||
this.teacherName = teacherName;
|
||||
}
|
||||
|
||||
public Long getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
@@ -53,12 +81,68 @@ public class LessonResponse {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public Long getDisciplineId() {
|
||||
return disciplineId;
|
||||
public String getGroupName() {
|
||||
return groupName;
|
||||
}
|
||||
|
||||
public void setDisciplineId(Long disciplineId) {
|
||||
this.disciplineId = disciplineId;
|
||||
public void setGroupName(String groupName) {
|
||||
this.groupName = groupName;
|
||||
}
|
||||
|
||||
public String getTypeLesson() {
|
||||
return typeLesson;
|
||||
}
|
||||
|
||||
public void setTypeLesson(String typeLesson) {
|
||||
this.typeLesson = typeLesson;
|
||||
}
|
||||
|
||||
public String getLessonFormat() {
|
||||
return lessonFormat;
|
||||
}
|
||||
|
||||
public void setLessonFormat(String lessonFormat) {
|
||||
this.lessonFormat = lessonFormat;
|
||||
}
|
||||
|
||||
public Long getClassroomId() {
|
||||
return classroomId;
|
||||
}
|
||||
|
||||
public void setClassroomId(Long classroomId) {
|
||||
this.classroomId = classroomId;
|
||||
}
|
||||
|
||||
public String getClassroomName() {
|
||||
return classroomName;
|
||||
}
|
||||
|
||||
public void setClassroomName(String classroomName) {
|
||||
this.classroomName = classroomName;
|
||||
}
|
||||
|
||||
public String getEducationFormName() {
|
||||
return educationFormName;
|
||||
}
|
||||
|
||||
public void setEducationFormName(String educationFormName) {
|
||||
this.educationFormName = educationFormName;
|
||||
}
|
||||
|
||||
public Long getSubjectId() {
|
||||
return subjectId;
|
||||
}
|
||||
|
||||
public void setSubjectId(Long subjectId) {
|
||||
this.subjectId = subjectId;
|
||||
}
|
||||
|
||||
public String getSubjectName() {
|
||||
return subjectName;
|
||||
}
|
||||
|
||||
public void setSubjectName(String subjectName) {
|
||||
this.subjectName = subjectName;
|
||||
}
|
||||
|
||||
public String getDay() {
|
||||
|
||||
0
backend/src/main/java/com/magistr/app/dto/LoginRequest.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/LoginRequest.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/LoginResponse.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/LoginResponse.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/TeacherSubjectResponse.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/TeacherSubjectResponse.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/UserResponse.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/dto/UserResponse.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/Classroom.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/Classroom.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/EducationForm.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/EducationForm.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/Equipment.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/Equipment.java
Normal file → Executable file
45
backend/src/main/java/com/magistr/app/model/Lesson.java
Normal file → Executable file
45
backend/src/main/java/com/magistr/app/model/Lesson.java
Normal file → Executable file
@@ -16,8 +16,17 @@ public class Lesson {
|
||||
@Column(name = "group_id", nullable = false)
|
||||
private Long groupId;
|
||||
|
||||
@Column(name = "discipline_id", nullable = false)
|
||||
private Long disciplineId;
|
||||
@Column(name = "subject_id", nullable = false)
|
||||
private Long subjectId;
|
||||
|
||||
@Column(name = "lesson_format", nullable = false, length = 255)
|
||||
private String lessonFormat;
|
||||
|
||||
@Column(name = "type_lesson", nullable = false, length = 255)
|
||||
private String typeLesson;
|
||||
|
||||
@Column(name = "classroom_id", nullable = false)
|
||||
private Long classroomId;
|
||||
|
||||
@Column(name = "day", nullable = false, length = 255)
|
||||
private String day;
|
||||
@@ -55,12 +64,36 @@ public class Lesson {
|
||||
this.groupId = groupId;
|
||||
}
|
||||
|
||||
public Long getDisciplineId() {
|
||||
return disciplineId;
|
||||
public Long getSubjectId() {
|
||||
return subjectId;
|
||||
}
|
||||
|
||||
public void setDisciplineId(Long disciplineId) {
|
||||
this.disciplineId = disciplineId;
|
||||
public void setSubjectId(Long subjectId) {
|
||||
this.subjectId = subjectId;
|
||||
}
|
||||
|
||||
public String getLessonFormat() {
|
||||
return lessonFormat;
|
||||
}
|
||||
|
||||
public void setLessonFormat(String lessonFormat) {
|
||||
this.lessonFormat = lessonFormat;
|
||||
}
|
||||
|
||||
public String getTypeLesson() {
|
||||
return typeLesson;
|
||||
}
|
||||
|
||||
public void setTypeLesson(String typeLesson) {
|
||||
this.typeLesson = typeLesson;
|
||||
}
|
||||
|
||||
public Long getClassroomId() {
|
||||
return classroomId;
|
||||
}
|
||||
|
||||
public void setClassroomId(Long classroomId) {
|
||||
this.classroomId = classroomId;
|
||||
}
|
||||
|
||||
public String getDay() {
|
||||
|
||||
0
backend/src/main/java/com/magistr/app/model/Role.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/Role.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/StudentGroup.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/StudentGroup.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/Subject.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/Subject.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/TeacherSubject.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/TeacherSubject.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/TeacherSubjectId.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/TeacherSubjectId.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/User.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/model/User.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/ClassroomRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/ClassroomRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/EducationFormRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/EducationFormRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/EquipmentRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/EquipmentRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/GroupRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/GroupRepository.java
Normal file → Executable file
5
backend/src/main/java/com/magistr/app/repository/LessonRepository.java
Normal file → Executable file
5
backend/src/main/java/com/magistr/app/repository/LessonRepository.java
Normal file → Executable file
@@ -3,9 +3,12 @@ package com.magistr.app.repository;
|
||||
import com.magistr.app.model.Lesson;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface LessonRepository extends JpaRepository<Lesson, Long> {
|
||||
|
||||
Optional<Lesson> findByDisciplineId(Long disciplineId);
|
||||
Optional<Lesson> findBySubjectId(Long subjectId);
|
||||
|
||||
List<Lesson> findByTeacherId(Long teacherId);
|
||||
}
|
||||
|
||||
0
backend/src/main/java/com/magistr/app/repository/SubjectRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/SubjectRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/TeacherSubjectRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/TeacherSubjectRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/UserRepository.java
Normal file → Executable file
0
backend/src/main/java/com/magistr/app/repository/UserRepository.java
Normal file → Executable file
30
backend/src/main/java/com/magistr/app/utils/DayAndWeekValidator.java
Executable file
30
backend/src/main/java/com/magistr/app/utils/DayAndWeekValidator.java
Executable file
@@ -0,0 +1,30 @@
|
||||
package com.magistr.app.utils;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class DayAndWeekValidator {
|
||||
|
||||
private static final Set<String> VALID_DAYS = Set.of(
|
||||
"Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"
|
||||
);
|
||||
|
||||
private static final Set<String> VALID_WEEKS = Set.of(
|
||||
"Верхняя", "Нижняя", "Обе"
|
||||
);
|
||||
|
||||
public static boolean isValidDay(String day) {
|
||||
return day != null && VALID_DAYS.contains(day);
|
||||
}
|
||||
|
||||
public static boolean isValidWeek(String week) {
|
||||
return week != null && VALID_WEEKS.contains(week);
|
||||
}
|
||||
|
||||
public static String getValidDaysMessage() {
|
||||
return "Допустимые дни: " + String.join(", ", VALID_DAYS);
|
||||
}
|
||||
|
||||
public static String getValidWeekMessage() {
|
||||
return "Допустимы для выбора: " + String.join(", ", VALID_WEEKS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.magistr.app.utils;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class TypeAndFormatLessonValidator {
|
||||
|
||||
private static final Set<String> VALID_TYPES = Set.of(
|
||||
"Лекция", "Лабораторная работа", "Практическая"
|
||||
);
|
||||
|
||||
private static final Set<String> VALID_FORMATS = Set.of(
|
||||
"Онлайн", "Очно"
|
||||
);
|
||||
|
||||
public static boolean isValidType(String type) {
|
||||
return type != null && VALID_TYPES.contains(type);
|
||||
}
|
||||
|
||||
public static boolean isValidFormat(String format) {
|
||||
return format != null && VALID_FORMATS.contains(format);
|
||||
}
|
||||
|
||||
public static String getValidTypesMessage() {
|
||||
return "Допустимые типы: " + String.join(", ", VALID_TYPES);
|
||||
}
|
||||
|
||||
public static String getValidFormatsMessage() {
|
||||
return "Допустимые форматы: " + String.join(", ", VALID_FORMATS);
|
||||
}
|
||||
}
|
||||
3
backend/src/main/resources/application.properties
Normal file → Executable file
3
backend/src/main/resources/application.properties
Normal file → Executable file
@@ -10,3 +10,6 @@ spring.datasource.driver-class-name=org.postgresql.Driver
|
||||
spring.jpa.hibernate.ddl-auto=validate
|
||||
spring.jpa.show-sql=false
|
||||
spring.jpa.open-in-view=false
|
||||
|
||||
#Eta nastroyka otvechayet za vklyucheniye vidimosti logov urovnya DEBUG v logakh BE, poka vyklyuchil chtoby ne zasoryat'. Zapisi INFO otobrazhat'sya budut
|
||||
#logging.level.root=DEBUG
|
||||
|
||||
0
compose.yaml
Normal file → Executable file
0
compose.yaml
Normal file → Executable file
189
db/init/init.sql
Normal file → Executable file
189
db/init/init.sql
Normal file → Executable file
@@ -1,10 +1,18 @@
|
||||
-- ==========================================
|
||||
-- Инициализация расширений
|
||||
-- ==========================================
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- ==========================================
|
||||
-- Пользователи и роли
|
||||
-- ==========================================
|
||||
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(20) NOT NULL DEFAULT 'STUDENT',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Админ по умолчанию: admin / admin (bcrypt через pgcrypto)
|
||||
@@ -12,19 +20,49 @@ INSERT INTO users (username, password, role)
|
||||
VALUES ('admin', crypt('admin', gen_salt('bf', 10)), 'ADMIN')
|
||||
ON CONFLICT (username) DO NOTHING;
|
||||
|
||||
-- ==========================================
|
||||
-- Образовательные формы
|
||||
-- ==========================================
|
||||
CREATE TABLE IF NOT EXISTS education_forms (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO education_forms (name) VALUES ('Бакалавриат'), ('Магистратура'), ('Специалитет')
|
||||
INSERT INTO education_forms (name) VALUES
|
||||
('Бакалавриат'),
|
||||
('Магистратура'),
|
||||
('Специалитет')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- ==========================================
|
||||
-- Учебные группы
|
||||
-- ==========================================
|
||||
CREATE TABLE IF NOT EXISTS student_groups (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
education_form_id BIGINT NOT NULL REFERENCES education_forms(id)
|
||||
education_form_id BIGINT NOT NULL REFERENCES education_forms(id),
|
||||
course INT CHECK (course BETWEEN 1 AND 6),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Тестовая базовая группа для работы
|
||||
INSERT INTO student_groups (name, education_form_id, course)
|
||||
VALUES ('ИВТ-21-1', 1, 3)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- ==========================================
|
||||
-- Подгруппы (например: "ИВТ-21-1 Подгруппа 1")
|
||||
-- ==========================================
|
||||
CREATE TABLE IF NOT EXISTS subgroups (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
group_id BIGINT NOT NULL REFERENCES student_groups(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
student_capacity INT,
|
||||
UNIQUE(group_id, name)
|
||||
);
|
||||
|
||||
-- ==========================================
|
||||
-- Справочники
|
||||
-- ==========================================
|
||||
@@ -32,110 +70,149 @@ CREATE TABLE IF NOT EXISTS student_groups (
|
||||
-- Дисциплины
|
||||
CREATE TABLE IF NOT EXISTS subjects (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(200) UNIQUE NOT NULL
|
||||
name VARCHAR(200) UNIQUE NOT NULL,
|
||||
code VARCHAR(20),
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO subjects (name) VALUES
|
||||
('Высшая математика'),
|
||||
('Философия'),
|
||||
('Информатика'),
|
||||
('Базы данных'),
|
||||
('Английский язык')
|
||||
('Высшая математика'),
|
||||
('Философия'),
|
||||
('Информатика'),
|
||||
('Базы данных'),
|
||||
('Английский язык')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Типы занятий
|
||||
CREATE TABLE IF NOT EXISTS lesson_types (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) UNIQUE NOT NULL
|
||||
name VARCHAR(50) UNIQUE NOT NULL,
|
||||
color_code VARCHAR(7) DEFAULT '#3788d8', -- для цветовой индикации в календаре
|
||||
duration_minutes INT DEFAULT 90
|
||||
);
|
||||
|
||||
INSERT INTO lesson_types (name) VALUES
|
||||
('Лекция'),
|
||||
('Практика'),
|
||||
('Лабораторная работа')
|
||||
INSERT INTO lesson_types (name, color_code) VALUES
|
||||
('Лекция', '#FF6B6B'),
|
||||
('Практика', '#4ECDC4'),
|
||||
('Лабораторная работа', '#45B7D1')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Оборудование
|
||||
CREATE TABLE IF NOT EXISTS equipments (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) UNIQUE NOT NULL
|
||||
name VARCHAR(50) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
inventory_number VARCHAR(50)
|
||||
);
|
||||
|
||||
INSERT INTO equipments (name) VALUES
|
||||
('Проектор'),
|
||||
('ПК'),
|
||||
('Лаборатория'),
|
||||
('Интерактивная доска')
|
||||
('Проектор'),
|
||||
('ПК'),
|
||||
('Лаборатория'),
|
||||
('Интерактивная доска'),
|
||||
('Документ-камера'),
|
||||
('Аудиосистема')
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Аудитории
|
||||
CREATE TABLE IF NOT EXISTS classrooms (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
name VARCHAR(50) UNIQUE NOT NULL,
|
||||
capacity INT NOT NULL,
|
||||
is_available BOOLEAN DEFAULT TRUE
|
||||
capacity INT NOT NULL CHECK (capacity > 0),
|
||||
building VARCHAR(50),
|
||||
floor INT,
|
||||
is_available BOOLEAN DEFAULT TRUE,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
INSERT INTO classrooms (name, capacity) VALUES
|
||||
('101 Ленинская', 120),
|
||||
('202 IT Lab', 20),
|
||||
('303 Обычная', 30)
|
||||
INSERT INTO classrooms (name, capacity, building, floor) VALUES
|
||||
('101 Ленинская', 120, 'Главный корпус', 1),
|
||||
('202 IT Lab', 20, 'Корпус IT', 2),
|
||||
('303 Обычная', 30, 'Главный корпус', 3)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Привязка оборудования к аудиториям (Many-to-Many)
|
||||
CREATE TABLE IF NOT EXISTS classroom_equipments (
|
||||
classroom_id BIGINT NOT NULL REFERENCES classrooms(id) ON DELETE CASCADE,
|
||||
equipment_id BIGINT NOT NULL REFERENCES equipments(id) ON DELETE CASCADE,
|
||||
quantity INT DEFAULT 1 CHECK (quantity > 0),
|
||||
notes TEXT,
|
||||
PRIMARY KEY (classroom_id, equipment_id)
|
||||
);
|
||||
|
||||
-- Заполнение привязок оборудования (на основе ID базовых данных)
|
||||
-- '101 Ленинская' -> Проектор (1), Интерактивная доска (4)
|
||||
INSERT INTO classroom_equipments (classroom_id, equipment_id) VALUES
|
||||
(1, 1), (1, 4),
|
||||
-- '202 IT Lab' -> ПК (2), Проектор (1), Лаборатория (3)
|
||||
(2, 2), (2, 1), (2, 3)
|
||||
-- '303 Обычная' -> ничего
|
||||
ON CONFLICT DO NOTHING;
|
||||
-- Заполнение привязок оборудования с использованием подзапросов
|
||||
INSERT INTO classroom_equipments (classroom_id, equipment_id, quantity)
|
||||
SELECT c.id, e.id,
|
||||
CASE
|
||||
WHEN e.name = 'ПК' AND c.name = '202 IT Lab' THEN 15
|
||||
WHEN e.name = 'ПК' THEN 1
|
||||
ELSE 1
|
||||
END
|
||||
FROM classrooms c, equipments e
|
||||
WHERE
|
||||
(c.name = '101 Ленинская' AND e.name IN ('Проектор', 'Интерактивная доска', 'Аудиосистема'))
|
||||
OR (c.name = '202 IT Lab' AND e.name IN ('ПК', 'Проектор', 'Лаборатория', 'Интерактивная доска'))
|
||||
OR (c.name = '303 Обычная' AND e.name IN ('Проектор'))
|
||||
ON CONFLICT (classroom_id, equipment_id) DO NOTHING;
|
||||
|
||||
-- ==========================================
|
||||
-- Связи для преподавателей и студентов
|
||||
-- Связи для преподавателей
|
||||
-- ==========================================
|
||||
|
||||
-- Подгруппы (например: "ИВТ-21-1 Подгруппа 1")
|
||||
CREATE TABLE IF NOT EXISTS subgroups (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
group_id BIGINT NOT NULL REFERENCES student_groups(id) ON DELETE CASCADE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
UNIQUE(group_id, name)
|
||||
);
|
||||
|
||||
-- Тестовая базовая группа для работы
|
||||
INSERT INTO student_groups (name, education_form_id)
|
||||
VALUES ('ИВТ-21-1', 1)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
-- Привязка преподавателей к дисциплинам
|
||||
CREATE TABLE IF NOT EXISTS teacher_subjects (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
subject_id BIGINT NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,
|
||||
qualification_level VARCHAR(50),
|
||||
experience_years INT,
|
||||
PRIMARY KEY(user_id, subject_id)
|
||||
);
|
||||
|
||||
-- Какие типы занятий может вести преподаватель по дисциплине
|
||||
CREATE TABLE IF NOT EXISTS teacher_lesson_types (
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
subject_id BIGINT NOT NULL REFERENCES subjects(id) ON DELETE CASCADE,
|
||||
lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (user_id, subject_id, lesson_type_id)
|
||||
);
|
||||
|
||||
-- ==========================================
|
||||
-- Основная таблица Расписания (Lessons)
|
||||
-- ==========================================
|
||||
CREATE TABLE IF NOT EXISTS lessons (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
teacher_id BIGINT NOT NULL REFERENCES users(id),
|
||||
group_id BIGINT NOT NULL REFERENCES student_groups(id),
|
||||
subject_id BIGINT NOT NULL REFERENCES subjects(id),
|
||||
lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id),
|
||||
lesson_format VARCHAR(255) NOT NULL,
|
||||
type_lesson VARCHAR(255) NOT NULL,
|
||||
classroom_id BIGINT NOT NULL REFERENCES classrooms(id),
|
||||
group_id BIGINT NOT NULL REFERENCES student_groups(id), -- первичная группа
|
||||
subgroup_id BIGINT REFERENCES subgroups(id), -- необязательно (если делим группу)
|
||||
|
||||
day_of_week INT NOT NULL CHECK (day_of_week BETWEEN 1 AND 7), -- 1=Понедельник, 7=Воскресенье
|
||||
is_even_week BOOLEAN NOT NULL, -- Четная/нечетная неделя
|
||||
start_time TIME NOT NULL, -- Напр. '08:00:00'
|
||||
end_time TIME NOT NULL -- Напр. '09:30:00'
|
||||
day VARCHAR(255) NOT NULL,
|
||||
week VARCHAR(255) NOT NULL,
|
||||
time VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
-- ==========================================
|
||||
-- Функция обновления timestamp
|
||||
-- ==========================================
|
||||
CREATE OR REPLACE FUNCTION update_updated_at_column()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = CURRENT_TIMESTAMP;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Триггеры для обновления updated_at
|
||||
CREATE TRIGGER update_users_updated_at
|
||||
BEFORE UPDATE ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION update_updated_at_column();
|
||||
|
||||
-- ==========================================
|
||||
-- Комментарии к таблицам и полям (для документации)
|
||||
-- ==========================================
|
||||
COMMENT ON TABLE users IS 'Пользователи системы (студенты, преподаватели, администраторы)';
|
||||
COMMENT ON TABLE lessons IS 'Основное расписание занятий';
|
||||
0
frontend/.dockerignore
Normal file → Executable file
0
frontend/.dockerignore
Normal file → Executable file
0
frontend/Dockerfile
Normal file → Executable file
0
frontend/Dockerfile
Normal file → Executable file
@@ -1,911 +0,0 @@
|
||||
(() => {
|
||||
'use strict';
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
const role = localStorage.getItem('role');
|
||||
|
||||
if (!token || role !== 'ADMIN') {
|
||||
window.location.href = '/';
|
||||
return;
|
||||
}
|
||||
|
||||
// ---- DOM refs ----
|
||||
const pageTitle = document.getElementById('page-title');
|
||||
const btnLogout = document.getElementById('btn-logout');
|
||||
const menuToggle = document.getElementById('menu-toggle');
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
|
||||
// Global Ripple Effect
|
||||
document.addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('.btn-create, .btn-delete, .btn-logout');
|
||||
if (!btn) return;
|
||||
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const ripple = document.createElement('span');
|
||||
ripple.classList.add('ripple');
|
||||
ripple.style.left = `${x}px`;
|
||||
ripple.style.top = `${y}px`;
|
||||
|
||||
if (getComputedStyle(btn).position === 'static') {
|
||||
btn.style.position = 'relative';
|
||||
}
|
||||
btn.style.overflow = 'hidden';
|
||||
|
||||
btn.appendChild(ripple);
|
||||
|
||||
setTimeout(() => ripple.remove(), 600);
|
||||
});
|
||||
|
||||
// Users
|
||||
const usersTbody = document.getElementById('users-tbody');
|
||||
const createForm = document.getElementById('create-form');
|
||||
const createAlert = document.getElementById('create-alert');
|
||||
|
||||
// Groups
|
||||
const groupsTbody = document.getElementById('groups-tbody');
|
||||
const createGroupForm = document.getElementById('create-group-form');
|
||||
const createGroupAlert = document.getElementById('create-group-alert');
|
||||
const newGroupEfSelect = document.getElementById('new-group-ef');
|
||||
const filterEfSelect = document.getElementById('filter-ef');
|
||||
|
||||
// Education Forms
|
||||
const efTbody = document.getElementById('ef-tbody');
|
||||
const createEfForm = document.getElementById('create-ef-form');
|
||||
const createEfAlert = document.getElementById('create-ef-alert');
|
||||
|
||||
// Classrooms
|
||||
const classroomsTbody = document.getElementById('classrooms-tbody');
|
||||
const createClassroomForm = document.getElementById('create-classroom-form');
|
||||
const createClassroomAlert = document.getElementById('create-classroom-alert');
|
||||
const modalEditClassroom = document.getElementById('modal-edit-classroom');
|
||||
const modalEditClassroomClose = document.getElementById('modal-edit-classroom-close');
|
||||
const editClassroomForm = document.getElementById('edit-classroom-form');
|
||||
const editClassroomAlert = document.getElementById('edit-classroom-alert');
|
||||
const editEquipmentCheckboxes = document.getElementById('edit-equipment-checkboxes');
|
||||
|
||||
// Equipments
|
||||
const equipmentsTbody = document.getElementById('equipments-tbody');
|
||||
const createEquipmentForm = document.getElementById('create-equipment-form');
|
||||
const createEquipmentAlert = document.getElementById('create-equipment-alert');
|
||||
const equipmentCheckboxes = document.getElementById('equipment-checkboxes');
|
||||
|
||||
// Subjects
|
||||
const subjectsTbody = document.getElementById('subjects-tbody');
|
||||
const createSubjectForm = document.getElementById('create-subject-form');
|
||||
const createSubjectAlert = document.getElementById('create-subject-alert');
|
||||
const assignTeacherForm = document.getElementById('assign-teacher-form');
|
||||
const assignTeacherAlert = document.getElementById('assign-teacher-alert');
|
||||
const assignTeacherSelect = document.getElementById('assign-teacher-select');
|
||||
const assignSubjectSelect = document.getElementById('assign-subject-select');
|
||||
const teacherSubjectsTbody = document.getElementById('teacher-subjects-tbody');
|
||||
|
||||
// --- Multi-select logic ---
|
||||
function updateSelectText(containerId, textId) {
|
||||
const container = document.getElementById(containerId);
|
||||
const textEl = document.getElementById(textId);
|
||||
if (!container || !textEl) return;
|
||||
const checked = Array.from(container.querySelectorAll('input:checked'));
|
||||
if (checked.length === 0) {
|
||||
textEl.textContent = 'Выберите оборудование...';
|
||||
} else if (checked.length === 1) {
|
||||
textEl.textContent = checked[0].parentElement.textContent.trim();
|
||||
} else {
|
||||
textEl.textContent = `Выбрано: ${checked.length}`;
|
||||
}
|
||||
}
|
||||
|
||||
function initMultiSelect(boxId, menuId, textId, checkboxContainerId) {
|
||||
const box = document.getElementById(boxId);
|
||||
const menu = document.getElementById(menuId);
|
||||
const container = document.getElementById(checkboxContainerId);
|
||||
if (!box || !menu || !container) return;
|
||||
|
||||
box.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = menu.classList.contains('open');
|
||||
document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('open'));
|
||||
document.querySelectorAll('.select-box').forEach(b => b.classList.remove('active'));
|
||||
if (!isOpen) {
|
||||
menu.classList.add('open');
|
||||
box.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
menu.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
container.addEventListener('change', () => {
|
||||
updateSelectText(checkboxContainerId, textId);
|
||||
});
|
||||
}
|
||||
|
||||
initMultiSelect('equipment-select-box', 'equipment-dropdown-menu', 'equipment-select-text', 'equipment-checkboxes');
|
||||
initMultiSelect('edit-equipment-select-box', 'edit-equipment-dropdown-menu', 'edit-equipment-select-text', 'edit-equipment-checkboxes');
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('open'));
|
||||
document.querySelectorAll('.select-box').forEach(b => b.classList.remove('active'));
|
||||
});
|
||||
// --------------------------
|
||||
|
||||
const navItems = document.querySelectorAll('.nav-item[data-tab]');
|
||||
const tabContents = document.querySelectorAll('.tab-content');
|
||||
|
||||
// ---- State ----
|
||||
let allGroups = [];
|
||||
let allEducationForms = [];
|
||||
let allEquipments = [];
|
||||
let allSubjects = [];
|
||||
let allTeachers = [];
|
||||
|
||||
// ---- Tab Switching ----
|
||||
const TAB_TITLES = {
|
||||
users: 'Управление пользователями',
|
||||
groups: 'Управление группами',
|
||||
'edu-forms': 'Формы обучения',
|
||||
equipments: 'Оборудование',
|
||||
classrooms: 'Аудитории',
|
||||
subjects: 'Дисциплины и преподаватели'
|
||||
};
|
||||
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
switchTab(item.dataset.tab);
|
||||
});
|
||||
});
|
||||
|
||||
function switchTab(tab) {
|
||||
navItems.forEach(n => n.classList.remove('active'));
|
||||
document.querySelector(`.nav-item[data-tab="${tab}"]`)?.classList.add('active');
|
||||
|
||||
tabContents.forEach(tc => tc.style.display = 'none');
|
||||
const target = document.getElementById('tab-' + tab);
|
||||
if (target) target.style.display = '';
|
||||
|
||||
pageTitle.textContent = TAB_TITLES[tab] || '';
|
||||
|
||||
if (tab === 'users') loadUsers();
|
||||
if (tab === 'groups') { loadEducationForms().then(() => loadGroups()); }
|
||||
if (tab === 'edu-forms') loadEducationForms();
|
||||
if (tab === 'equipments') loadEquipments();
|
||||
if (tab === 'classrooms') { loadEquipments().then(() => loadClassrooms()); }
|
||||
if (tab === 'subjects') { Promise.all([loadSubjects(), loadTeachers()]).then(() => loadTeacherSubjects()); }
|
||||
|
||||
sidebar.classList.remove('open');
|
||||
sidebarOverlay.classList.remove('open');
|
||||
}
|
||||
|
||||
// ---- Mobile Menu ----
|
||||
menuToggle.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('open');
|
||||
sidebarOverlay.classList.toggle('open');
|
||||
});
|
||||
sidebarOverlay.addEventListener('click', () => {
|
||||
sidebar.classList.remove('open');
|
||||
sidebarOverlay.classList.remove('open');
|
||||
});
|
||||
|
||||
// ---- Helpers ----
|
||||
const ROLE_LABELS = { ADMIN: 'Администратор', TEACHER: 'Преподаватель', STUDENT: 'Студент' };
|
||||
const ROLE_BADGE = { ADMIN: 'badge-admin', TEACHER: 'badge-teacher', STUDENT: 'badge-student' };
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function showAlert(el, msg, type) {
|
||||
el.className = 'form-alert ' + type;
|
||||
el.textContent = msg;
|
||||
}
|
||||
|
||||
function hideAlert(el) {
|
||||
el.className = 'form-alert';
|
||||
el.textContent = '';
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// USERS
|
||||
// ============================================================
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const res = await fetch('/api/users', {
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
const users = await res.json();
|
||||
renderUsers(users);
|
||||
} catch (e) {
|
||||
usersTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderUsers(users) {
|
||||
if (!users.length) {
|
||||
usersTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Нет пользователей</td></tr>';
|
||||
return;
|
||||
}
|
||||
usersTbody.innerHTML = users.map(u => `
|
||||
<tr>
|
||||
<td>${u.id}</td>
|
||||
<td>${escapeHtml(u.username)}</td>
|
||||
<td><span class="badge ${ROLE_BADGE[u.role] || ''}">${ROLE_LABELS[u.role] || u.role}</span></td>
|
||||
<td><button class="btn-delete" data-id="${u.id}">Удалить</button></td>
|
||||
<td><button class="btn-delete" data-role="${u.role}">Добавить занятие</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
createForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert(createAlert);
|
||||
const username = document.getElementById('new-username').value.trim();
|
||||
const password = document.getElementById('new-password').value;
|
||||
const role = document.getElementById('new-role').value;
|
||||
if (!username || !password) { showAlert(createAlert, 'Заполните все поля', 'error'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/users', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ username, password, role }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(createAlert, `Пользователь "${data.username}" создан`, 'success');
|
||||
createForm.reset();
|
||||
loadUsers();
|
||||
} else {
|
||||
showAlert(createAlert, data.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
} catch (e) { showAlert(createAlert, 'Ошибка соединения', 'error'); }
|
||||
});
|
||||
|
||||
usersTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить пользователя?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/users/' + btn.dataset.id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
if (res.ok) loadUsers();
|
||||
else alert('Ошибка удаления');
|
||||
} catch (e) { alert('Ошибка соединения'); }
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// EDUCATION FORMS
|
||||
// ============================================================
|
||||
|
||||
async function loadEducationForms() {
|
||||
try {
|
||||
const res = await fetch('/api/education-forms', {
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
allEducationForms = await res.json();
|
||||
renderEfTable(allEducationForms);
|
||||
populateEfSelects(allEducationForms);
|
||||
} catch (e) {
|
||||
efTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderEfTable(forms) {
|
||||
if (!forms.length) {
|
||||
efTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет форм обучения</td></tr>';
|
||||
return;
|
||||
}
|
||||
efTbody.innerHTML = forms.map(ef => `
|
||||
<tr>
|
||||
<td>${ef.id}</td>
|
||||
<td>${escapeHtml(ef.name)}</td>
|
||||
<td><button class="btn-delete" data-id="${ef.id}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
function populateEfSelects(forms) {
|
||||
// Group creation select
|
||||
const currentVal = newGroupEfSelect.value;
|
||||
newGroupEfSelect.innerHTML = forms.map(ef =>
|
||||
`<option value="${ef.id}">${escapeHtml(ef.name)}</option>`
|
||||
).join('');
|
||||
if (currentVal && forms.find(f => f.id == currentVal)) {
|
||||
newGroupEfSelect.value = currentVal;
|
||||
}
|
||||
|
||||
// Filter select
|
||||
const currentFilter = filterEfSelect.value;
|
||||
filterEfSelect.innerHTML = '<option value="">Все формы</option>' +
|
||||
forms.map(ef =>
|
||||
`<option value="${ef.id}">${escapeHtml(ef.name)}</option>`
|
||||
).join('');
|
||||
if (currentFilter) filterEfSelect.value = currentFilter;
|
||||
}
|
||||
|
||||
createEfForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert(createEfAlert);
|
||||
const name = document.getElementById('new-ef-name').value.trim();
|
||||
if (!name) { showAlert(createEfAlert, 'Введите название', 'error'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/education-forms', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(createEfAlert, `Форма "${data.name}" создана`, 'success');
|
||||
createEfForm.reset();
|
||||
loadEducationForms();
|
||||
} else {
|
||||
showAlert(createEfAlert, data.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
} catch (e) { showAlert(createEfAlert, 'Ошибка соединения', 'error'); }
|
||||
});
|
||||
|
||||
efTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить форму обучения?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/education-forms/' + btn.dataset.id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
if (res.ok) {
|
||||
loadEducationForms();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.message || 'Ошибка удаления');
|
||||
}
|
||||
} catch (e) { alert('Ошибка соединения'); }
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// GROUPS
|
||||
// ============================================================
|
||||
|
||||
async function loadGroups() {
|
||||
try {
|
||||
const res = await fetch('/api/groups', {
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
allGroups = await res.json();
|
||||
applyGroupFilter();
|
||||
} catch (e) {
|
||||
groupsTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function applyGroupFilter() {
|
||||
const filterId = filterEfSelect.value;
|
||||
const filtered = filterId
|
||||
? allGroups.filter(g => g.educationFormId == filterId)
|
||||
: allGroups;
|
||||
renderGroups(filtered);
|
||||
}
|
||||
|
||||
filterEfSelect.addEventListener('change', applyGroupFilter);
|
||||
|
||||
function renderGroups(groups) {
|
||||
if (!groups.length) {
|
||||
groupsTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Нет групп</td></tr>';
|
||||
return;
|
||||
}
|
||||
groupsTbody.innerHTML = groups.map(g => `
|
||||
<tr>
|
||||
<td>${g.id}</td>
|
||||
<td>${escapeHtml(g.name)}</td>
|
||||
<td><span class="badge badge-ef">${escapeHtml(g.educationFormName)}</span></td>
|
||||
<td><button class="btn-delete" data-id="${g.id}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
createGroupForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert(createGroupAlert);
|
||||
const name = document.getElementById('new-group-name').value.trim();
|
||||
const educationFormId = newGroupEfSelect.value;
|
||||
if (!name) { showAlert(createGroupAlert, 'Введите название группы', 'error'); return; }
|
||||
if (!educationFormId) { showAlert(createGroupAlert, 'Выберите форму обучения', 'error'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/groups', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ name, educationFormId: Number(educationFormId) }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(createGroupAlert, `Группа "${data.name}" создана`, 'success');
|
||||
createGroupForm.reset();
|
||||
loadGroups();
|
||||
} else {
|
||||
showAlert(createGroupAlert, data.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
} catch (e) { showAlert(createGroupAlert, 'Ошибка соединения', 'error'); }
|
||||
});
|
||||
|
||||
groupsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить группу?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/groups/' + btn.dataset.id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
if (res.ok) loadGroups();
|
||||
else alert('Ошибка удаления');
|
||||
} catch (e) { alert('Ошибка соединения'); }
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// EQUIPMENTS
|
||||
// ============================================================
|
||||
|
||||
async function loadEquipments() {
|
||||
try {
|
||||
const res = await fetch('/api/equipments', {
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
allEquipments = await res.json();
|
||||
renderEquipments(allEquipments);
|
||||
renderEquipmentCheckboxes(allEquipments);
|
||||
} catch (e) {
|
||||
if (equipmentsTbody) equipmentsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
if (equipmentCheckboxes) equipmentCheckboxes.innerHTML = '<p class="text-error">Ошибка загрузки</p>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderEquipments(equipments) {
|
||||
if (!equipments.length) {
|
||||
equipmentsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет оборудования</td></tr>';
|
||||
return;
|
||||
}
|
||||
equipmentsTbody.innerHTML = equipments.map(eq => `
|
||||
<tr>
|
||||
<td>${eq.id}</td>
|
||||
<td>${escapeHtml(eq.name)}</td>
|
||||
<td><button class="btn-delete" data-id="${eq.id}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
function renderEquipmentCheckboxes(equipments) {
|
||||
if (!equipments.length) {
|
||||
equipmentCheckboxes.innerHTML = '<p class="text-muted"><small>Нет доступного оборудования</small></p>';
|
||||
return;
|
||||
}
|
||||
equipmentCheckboxes.innerHTML = equipments.map(eq => `
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="${eq.id}"> ${escapeHtml(eq.name)}
|
||||
</label>
|
||||
`).join('');
|
||||
updateSelectText('equipment-checkboxes', 'equipment-select-text');
|
||||
}
|
||||
|
||||
createEquipmentForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert(createEquipmentAlert);
|
||||
const name = document.getElementById('new-equipment-name').value.trim();
|
||||
if (!name) { showAlert(createEquipmentAlert, 'Введите название', 'error'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/equipments', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(createEquipmentAlert, `Оборудование "${data.name}" добавлено`, 'success');
|
||||
createEquipmentForm.reset();
|
||||
loadEquipments();
|
||||
} else {
|
||||
showAlert(createEquipmentAlert, data.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
} catch (e) { showAlert(createEquipmentAlert, 'Ошибка соединения', 'error'); }
|
||||
});
|
||||
|
||||
equipmentsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить оборудование?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/equipments/' + btn.dataset.id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
if (res.ok) {
|
||||
loadEquipments();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.message || 'Ошибка удаления');
|
||||
}
|
||||
} catch (e) { alert('Ошибка соединения'); }
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// CLASSROOMS
|
||||
// ============================================================
|
||||
|
||||
async function loadClassrooms() {
|
||||
try {
|
||||
const res = await fetch('/api/classrooms', {
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
const classrooms = await res.json();
|
||||
renderClassrooms(classrooms);
|
||||
} catch (e) {
|
||||
classroomsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderClassrooms(classrooms) {
|
||||
if (!classrooms.length) {
|
||||
classroomsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Нет аудиторий</td></tr>';
|
||||
return;
|
||||
}
|
||||
classroomsTbody.innerHTML = classrooms.map(c => {
|
||||
const equipHtml = c.equipments && c.equipments.length
|
||||
? c.equipments.map(eq => escapeHtml(eq.name)).join(', ')
|
||||
: '—';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${c.id}</td>
|
||||
<td><strong>${escapeHtml(c.name)}</strong></td>
|
||||
<td>${c.capacity} чел.</td>
|
||||
<td><small>${equipHtml}</small></td>
|
||||
<td>
|
||||
<div class="status-cell">
|
||||
<span class="badge ${c.isAvailable ? 'badge-available' : 'badge-unavailable'}">
|
||||
${c.isAvailable ? 'Доступна' : 'Не доступна'}
|
||||
</span>
|
||||
<button class="btn-icon-toggle" data-id="${c.id}" data-current-status="${c.isAvailable}" title="Сменить статус">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align: right;">
|
||||
<button class="btn-edit-classroom" data-id="${c.id}" style="padding: 0.35rem 0.7rem; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.2); border-radius: var(--radius-sm); color: var(--accent-hover); cursor: pointer; margin-right: 0.5rem;">Изменить</button>
|
||||
<button class="btn-delete" data-id="${c.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
createClassroomForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert(createClassroomAlert);
|
||||
const name = document.getElementById('new-classroom-name').value.trim();
|
||||
const capacity = parseInt(document.getElementById('new-classroom-capacity').value, 10);
|
||||
|
||||
const checkedBoxes = Array.from(equipmentCheckboxes.querySelectorAll('input:checked'));
|
||||
const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10));
|
||||
|
||||
if (!name || isNaN(capacity)) { showAlert(createClassroomAlert, 'Заполните обязательные поля', 'error'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/classrooms', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ name, capacity, equipmentIds, isAvailable: true }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(createClassroomAlert, `Аудитория "${data.name}" добавлена`, 'success');
|
||||
createClassroomForm.reset();
|
||||
updateSelectText('equipment-checkboxes', 'equipment-select-text');
|
||||
loadClassrooms();
|
||||
} else {
|
||||
showAlert(createClassroomAlert, data.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
} catch (e) { showAlert(createClassroomAlert, 'Ошибка соединения', 'error'); }
|
||||
});
|
||||
|
||||
classroomsTbody.addEventListener('click', async (e) => {
|
||||
const btnDelete = e.target.closest('.btn-delete');
|
||||
const btnToggleStatus = e.target.closest('.btn-icon-toggle');
|
||||
const btnEdit = e.target.closest('.btn-edit-classroom');
|
||||
|
||||
if (btnDelete) {
|
||||
if (!confirm('Удалить аудиторию?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/classrooms/' + btnDelete.dataset.id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
if (res.ok) loadClassrooms();
|
||||
else alert('Ошибка удаления');
|
||||
} catch (err) { alert('Ошибка соединения'); }
|
||||
}
|
||||
|
||||
if (btnToggleStatus) {
|
||||
const id = btnToggleStatus.dataset.id;
|
||||
const currentStatus = btnToggleStatus.dataset.currentStatus === 'true';
|
||||
try {
|
||||
const res = await fetch('/api/classrooms/' + id, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ isAvailable: !currentStatus }),
|
||||
});
|
||||
if (res.ok) loadClassrooms();
|
||||
else alert('Ошибка изменения статуса');
|
||||
} catch (err) { alert('Ошибка соединения'); }
|
||||
}
|
||||
|
||||
if (btnEdit) {
|
||||
const id = btnEdit.dataset.id;
|
||||
openEditClassroomModal(id);
|
||||
}
|
||||
});
|
||||
|
||||
let editingClassroomData = null;
|
||||
|
||||
async function openEditClassroomModal(id) {
|
||||
try {
|
||||
const res = await fetch('/api/classrooms', { headers: { 'Authorization': 'Bearer ' + token } });
|
||||
const classrooms = await res.json();
|
||||
editingClassroomData = classrooms.find(c => c.id == id);
|
||||
|
||||
if (!editingClassroomData) return;
|
||||
|
||||
document.getElementById('edit-classroom-id').value = editingClassroomData.id;
|
||||
document.getElementById('edit-classroom-name').value = editingClassroomData.name;
|
||||
document.getElementById('edit-classroom-capacity').value = editingClassroomData.capacity;
|
||||
|
||||
if (allEquipments.length) {
|
||||
editEquipmentCheckboxes.innerHTML = allEquipments.map(eq => {
|
||||
const isChecked = editingClassroomData.equipments.some(e => e.id === eq.id) ? 'checked' : '';
|
||||
return `
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="${eq.id}" ${isChecked}> ${escapeHtml(eq.name)}
|
||||
</label>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
editEquipmentCheckboxes.innerHTML = '<p class="text-muted"><small>Нет доступного оборудования</small></p>';
|
||||
}
|
||||
updateSelectText('edit-equipment-checkboxes', 'edit-equipment-select-text');
|
||||
|
||||
hideAlert(editClassroomAlert);
|
||||
modalEditClassroom.classList.add('open');
|
||||
} catch (e) {
|
||||
alert('Ошибка загрузки данных аудитории');
|
||||
}
|
||||
}
|
||||
|
||||
modalEditClassroomClose.addEventListener('click', () => {
|
||||
modalEditClassroom.classList.remove('open');
|
||||
});
|
||||
|
||||
modalEditClassroom.addEventListener('click', (e) => {
|
||||
if (e.target === modalEditClassroom) {
|
||||
modalEditClassroom.classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
editClassroomForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert(editClassroomAlert);
|
||||
const id = document.getElementById('edit-classroom-id').value;
|
||||
const name = document.getElementById('edit-classroom-name').value.trim();
|
||||
const capacity = parseInt(document.getElementById('edit-classroom-capacity').value, 10);
|
||||
|
||||
const checkedBoxes = Array.from(editEquipmentCheckboxes.querySelectorAll('input:checked'));
|
||||
const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10));
|
||||
|
||||
if (!name || isNaN(capacity)) { showAlert(editClassroomAlert, 'Заполните обязательные поля', 'error'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/classrooms/' + id, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ name, capacity, equipmentIds, isAvailable: editingClassroomData.isAvailable }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
modalEditClassroom.classList.remove('open');
|
||||
showAlert(createClassroomAlert, `Аудитория "${data.name}" обновлена`, 'success');
|
||||
loadClassrooms();
|
||||
} else {
|
||||
showAlert(editClassroomAlert, data.message || 'Ошибка обновления', 'error');
|
||||
}
|
||||
} catch (e) { showAlert(editClassroomAlert, 'Ошибка соединения', 'error'); }
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// SUBJECTS
|
||||
// ============================================================
|
||||
|
||||
async function loadSubjects() {
|
||||
try {
|
||||
const res = await fetch('/api/subjects', {
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
allSubjects = await res.json();
|
||||
renderSubjects(allSubjects);
|
||||
populateSubjectSelect(allSubjects);
|
||||
} catch (e) {
|
||||
if (subjectsTbody) subjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderSubjects(subjects) {
|
||||
if (!subjects.length) {
|
||||
subjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет дисциплин</td></tr>';
|
||||
return;
|
||||
}
|
||||
subjectsTbody.innerHTML = subjects.map(s => `
|
||||
<tr>
|
||||
<td>${s.id}</td>
|
||||
<td>${escapeHtml(s.name)}</td>
|
||||
<td><button class="btn-delete" data-id="${s.id}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
function populateSubjectSelect(subjects) {
|
||||
if (!assignSubjectSelect) return;
|
||||
const currentVal = assignSubjectSelect.value;
|
||||
assignSubjectSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
|
||||
subjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
|
||||
if (currentVal && subjects.find(s => s.id == currentVal)) {
|
||||
assignSubjectSelect.value = currentVal;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTeachers() {
|
||||
try {
|
||||
const res = await fetch('/api/users/teachers', {
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
allTeachers = await res.json();
|
||||
populateTeacherSelect(allTeachers);
|
||||
} catch (e) {
|
||||
if (assignTeacherSelect) assignTeacherSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
|
||||
}
|
||||
}
|
||||
|
||||
function populateTeacherSelect(teachers) {
|
||||
if (!assignTeacherSelect) return;
|
||||
const currentVal = assignTeacherSelect.value;
|
||||
if (!teachers.length) {
|
||||
assignTeacherSelect.innerHTML = '<option value="">Нет преподавателей</option>';
|
||||
return;
|
||||
}
|
||||
assignTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' +
|
||||
teachers.map(t => `<option value="${t.id}">${escapeHtml(t.username)}</option>`).join('');
|
||||
if (currentVal && teachers.find(t => t.id == currentVal)) {
|
||||
assignTeacherSelect.value = currentVal;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTeacherSubjects() {
|
||||
try {
|
||||
const res = await fetch('/api/teacher-subjects', {
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
const tsData = await res.json();
|
||||
renderTeacherSubjects(tsData);
|
||||
} catch (e) {
|
||||
if (teacherSubjectsTbody) teacherSubjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTeacherSubjects(tsArray) {
|
||||
if (!tsArray.length) {
|
||||
teacherSubjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет привязок</td></tr>';
|
||||
return;
|
||||
}
|
||||
teacherSubjectsTbody.innerHTML = tsArray.map(ts => `
|
||||
<tr>
|
||||
<td>${escapeHtml(ts.username)}</td>
|
||||
<td>${escapeHtml(ts.subjectName)}</td>
|
||||
<td><button class="btn-delete" data-user-id="${ts.userId}" data-subject-id="${ts.subjectId}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
createSubjectForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert(createSubjectAlert);
|
||||
const name = document.getElementById('new-subject-name').value.trim();
|
||||
if (!name) { showAlert(createSubjectAlert, 'Введите название', 'error'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/subjects', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(createSubjectAlert, `Дисциплина "${data.name}" добавлена`, 'success');
|
||||
createSubjectForm.reset();
|
||||
loadSubjects();
|
||||
} else {
|
||||
showAlert(createSubjectAlert, data.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
} catch (e) { showAlert(createSubjectAlert, 'Ошибка соединения', 'error'); }
|
||||
});
|
||||
|
||||
subjectsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить дисциплину?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/subjects/' + btn.dataset.id, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
});
|
||||
if (res.ok) {
|
||||
loadSubjects();
|
||||
loadTeacherSubjects();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
alert(data.message || 'Ошибка удаления');
|
||||
}
|
||||
} catch (e) { alert('Ошибка соединения'); }
|
||||
});
|
||||
|
||||
assignTeacherForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert(assignTeacherAlert);
|
||||
const userId = assignTeacherSelect.value;
|
||||
const subjectId = assignSubjectSelect.value;
|
||||
if (!userId) { showAlert(assignTeacherAlert, 'Выберите преподавателя', 'error'); return; }
|
||||
if (!subjectId) { showAlert(assignTeacherAlert, 'Выберите дисциплину', 'error'); return; }
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/teacher-subjects', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ userId: Number(userId), subjectId: Number(subjectId) }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
showAlert(assignTeacherAlert, 'Привязка создана', 'success');
|
||||
loadTeacherSubjects();
|
||||
} else {
|
||||
showAlert(assignTeacherAlert, data.message || 'Ошибка привязки', 'error');
|
||||
}
|
||||
} catch (e) { showAlert(assignTeacherAlert, 'Ошибка соединения', 'error'); }
|
||||
});
|
||||
|
||||
teacherSubjectsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить привязку?')) return;
|
||||
try {
|
||||
const res = await fetch('/api/teacher-subjects', {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||
body: JSON.stringify({ userId: Number(btn.dataset.userId), subjectId: Number(btn.dataset.subjectId) }),
|
||||
});
|
||||
if (res.ok) loadTeacherSubjects();
|
||||
else alert('Ошибка удаления');
|
||||
} catch (e) { alert('Ошибка соединения'); }
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// LOGOUT & INIT
|
||||
// ============================================================
|
||||
|
||||
btnLogout.addEventListener('click', () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('role');
|
||||
window.location.href = '/';
|
||||
});
|
||||
|
||||
loadUsers();
|
||||
})();
|
||||
1253
frontend/admin/admin.css → frontend/admin/css/components.css
Normal file → Executable file
1253
frontend/admin/admin.css → frontend/admin/css/components.css
Normal file → Executable file
File diff suppressed because it is too large
Load Diff
216
frontend/admin/css/layout.css
Executable file
216
frontend/admin/css/layout.css
Executable file
@@ -0,0 +1,216 @@
|
||||
/* ===== Sidebar ===== */
|
||||
.sidebar {
|
||||
width: 260px;
|
||||
min-height: 100vh;
|
||||
background: var(--bg-sidebar);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-right: 1px solid var(--bg-card-border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
transition: background 0.4s ease, border-color 0.4s ease, transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
padding: 1.25rem;
|
||||
border-bottom: 1px solid var(--bg-card-border);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 0.25rem;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
transition: all var(--transition);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: var(--accent);
|
||||
border-radius: 0 4px 4px 0;
|
||||
transform: scaleY(0);
|
||||
transition: transform var(--transition);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: rgba(139, 92, 246, 0.12);
|
||||
color: var(--accent-hover);
|
||||
}
|
||||
|
||||
[data-theme="light"] .nav-item.active {
|
||||
background: rgba(99, 102, 241, 0.18);
|
||||
}
|
||||
|
||||
.nav-item.active::before {
|
||||
transform: scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-item svg {
|
||||
transition: transform var(--transition);
|
||||
}
|
||||
|
||||
.nav-item:hover svg,
|
||||
.nav-item.active svg {
|
||||
transform: scale(1.15) rotate(-5deg);
|
||||
}
|
||||
|
||||
.sidebar-footer {
|
||||
padding: 0.75rem;
|
||||
border-top: 1px solid var(--bg-card-border);
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
padding: 0.65rem 0.8rem;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: none;
|
||||
color: var(--text-secondary);
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
transition: background var(--transition), color var(--transition);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* ===== Main ===== */
|
||||
.main {
|
||||
flex: 1;
|
||||
margin-left: 260px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
padding: 1.5rem 2rem;
|
||||
border-bottom: 1px solid var(--bg-card-border);
|
||||
transition: border-color 0.4s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.topbar h1 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.02em;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
/* ===== Mobile Menu Toggle ===== */
|
||||
.menu-toggle {
|
||||
display: none;
|
||||
padding: 0.4rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
border-radius: var(--radius-sm);
|
||||
transition: background var(--transition);
|
||||
}
|
||||
|
||||
.menu-toggle:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.sidebar-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
z-index: 9;
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition);
|
||||
}
|
||||
|
||||
/* ===== Responsive Mobile ===== */
|
||||
@media (max-width: 768px) {
|
||||
.sidebar {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.sidebar.open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.main {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.topbar {
|
||||
padding: 1rem 1.25rem;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.menu-toggle,
|
||||
.sidebar-overlay {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar-overlay.open {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
110
frontend/admin/css/main.css
Executable file
110
frontend/admin/css/main.css
Executable file
@@ -0,0 +1,110 @@
|
||||
/* ===== Reset & Base ===== */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* Deep dark premium background */
|
||||
--bg-primary: #0a0a0f;
|
||||
--bg-sidebar: rgba(255, 255, 255, 0.02);
|
||||
--bg-card: rgba(255, 255, 255, 0.03);
|
||||
--bg-card-border: rgba(255, 255, 255, 0.05);
|
||||
--bg-input: rgba(255, 255, 255, 0.04);
|
||||
--bg-input-focus: rgba(255, 255, 255, 0.08);
|
||||
--bg-hover: rgba(255, 255, 255, 0.06);
|
||||
|
||||
/* Typography */
|
||||
--text-primary: #f8fafc;
|
||||
--text-secondary: #94a3b8;
|
||||
--text-placeholder: #475569;
|
||||
|
||||
/* Vibrant Accents */
|
||||
--accent: #8b5cf6;
|
||||
--accent-hover: #a78bfa;
|
||||
--accent-glow: rgba(139, 92, 246, 0.4);
|
||||
--accent-secondary: #ec4899;
|
||||
|
||||
/* Status Colors */
|
||||
--error: #ef4444;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
|
||||
/* Spatial */
|
||||
--radius-sm: 10px;
|
||||
--radius-md: 16px;
|
||||
--transition: 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||
}
|
||||
|
||||
/* ===== Light Theme ===== */
|
||||
[data-theme="light"] {
|
||||
--bg-primary: #f8fafc;
|
||||
--bg-sidebar: rgba(255, 255, 255, 0.7);
|
||||
--bg-card: rgba(255, 255, 255, 0.75);
|
||||
--bg-card-border: rgba(0, 0, 0, 0.08);
|
||||
--bg-input: rgba(0, 0, 0, 0.03);
|
||||
--bg-input-focus: rgba(0, 0, 0, 0.06);
|
||||
--bg-hover: rgba(0, 0, 0, 0.05);
|
||||
--text-primary: #0f172a;
|
||||
--text-secondary: #475569;
|
||||
--text-placeholder: #94a3b8;
|
||||
--accent: #6366f1;
|
||||
--accent-hover: #4f46e5;
|
||||
--accent-glow: rgba(99, 102, 241, 0.3);
|
||||
--accent-secondary: #d946ef;
|
||||
|
||||
--error: #ef4444;
|
||||
--success: #10b981;
|
||||
--warning: #f59e0b;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
transition: background 0.4s ease, color 0.4s ease;
|
||||
}
|
||||
|
||||
/* ===== Animations ===== */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(4px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideUpCard {
|
||||
from { opacity: 0; transform: translateY(15px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideDownAlert {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes slideInRow {
|
||||
from { opacity: 0; transform: translateX(-10px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
}
|
||||
|
||||
@keyframes admin-ripple {
|
||||
to { transform: scale(4); opacity: 0; }
|
||||
}
|
||||
|
||||
.ripple {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
transform: scale(0);
|
||||
animation: admin-ripple 0.6s linear;
|
||||
background-color: rgba(255, 255, 255, 0.3);
|
||||
pointer-events: none;
|
||||
}
|
||||
382
frontend/admin/index.html
Normal file → Executable file
382
frontend/admin/index.html
Normal file → Executable file
@@ -8,7 +8,11 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
|
||||
<!-- CSS Modules -->
|
||||
<link rel="stylesheet" href="css/main.css">
|
||||
<link rel="stylesheet" href="css/layout.css">
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -31,7 +35,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<nav class="sidebar-nav">
|
||||
<a href="#" class="nav-item active" data-tab="users">
|
||||
<a href="#" class="nav-item" data-tab="users">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
@@ -82,6 +86,15 @@
|
||||
</svg>
|
||||
Дисциплины
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-tab="schedule">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||||
</svg>
|
||||
Расписание занятий
|
||||
</a>
|
||||
</nav>
|
||||
<div class="sidebar-footer">
|
||||
<button class="btn-logout" id="btn-logout">
|
||||
@@ -110,372 +123,17 @@
|
||||
<line x1="3" y1="18" x2="21" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
<h1 id="page-title">Управление пользователями</h1>
|
||||
<h1 id="page-title">Загрузка...</h1>
|
||||
</header>
|
||||
|
||||
<!-- ===== Users Tab ===== -->
|
||||
<section class="content tab-content" id="tab-users">
|
||||
<div class="card create-card">
|
||||
<h2>Новый пользователь</h2>
|
||||
<form id="create-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-username">Имя пользователя</label>
|
||||
<input type="text" id="new-username" placeholder="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-password">Пароль</label>
|
||||
<input type="text" id="new-password" placeholder="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-role">Роль</label>
|
||||
<select id="new-role">
|
||||
<option value="STUDENT">Студент</option>
|
||||
<option value="TEACHER">Преподаватель</option>
|
||||
<option value="ADMIN">Администратор</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-create">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Все пользователи</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Имя пользователя</th>
|
||||
<th>Роль</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-tbody">
|
||||
<tr>
|
||||
<td colspan="4" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Dynamic Content Injected Here -->
|
||||
<section class="content" id="app-content">
|
||||
<!-- Content loaded via main.js -->
|
||||
</section>
|
||||
|
||||
<!-- ===== Groups Tab ===== -->
|
||||
<section class="content tab-content" id="tab-groups" style="display:none;">
|
||||
<div class="card create-card">
|
||||
<h2>Новая группа</h2>
|
||||
<form id="create-group-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-group-name">Название группы</label>
|
||||
<input type="text" id="new-group-name" placeholder="ИВТ-21-1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-group-ef">Форма обучения</label>
|
||||
<select id="new-group-ef">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-create">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-group-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Все группы</h2>
|
||||
<div class="filter-row">
|
||||
<label for="filter-ef">Фильтр:</label>
|
||||
<select id="filter-ef">
|
||||
<option value="">Все формы</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="groups-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Форма обучения</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="groups-tbody">
|
||||
<tr>
|
||||
<td colspan="4" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Education Forms Tab ===== -->
|
||||
<section class="content tab-content" id="tab-edu-forms" style="display:none;">
|
||||
<div class="card create-card">
|
||||
<h2>Новая форма обучения</h2>
|
||||
<form id="create-ef-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-ef-name">Название</label>
|
||||
<input type="text" id="new-ef-name" placeholder="Бакалавриат" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-create">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-ef-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Все формы обучения</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="ef-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ef-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Equipments Tab ===== -->
|
||||
<section class="content tab-content" id="tab-equipments" style="display:none;">
|
||||
<div class="card create-card">
|
||||
<h2>Новое оборудование</h2>
|
||||
<form id="create-equipment-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-equipment-name">Название оборудования</label>
|
||||
<input type="text" id="new-equipment-name" placeholder="Проектор" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-create">Добавить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-equipment-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Справочник оборудования</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="equipments-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="equipments-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Classrooms Tab ===== -->
|
||||
<section class="content tab-content" id="tab-classrooms" style="display:none;">
|
||||
<div class="card create-card">
|
||||
<h2>Новая аудитория</h2>
|
||||
<form id="create-classroom-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-classroom-name">Номер / Название</label>
|
||||
<input type="text" id="new-classroom-name" placeholder="101 Ленинская" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-classroom-capacity">Вместимость (чел.)</label>
|
||||
<input type="number" id="new-classroom-capacity" placeholder="30" min="1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top: 1rem;">
|
||||
<div class="form-group" style="flex: 2;">
|
||||
<label>Оборудование</label>
|
||||
<div class="custom-multi-select">
|
||||
<div class="select-box" id="equipment-select-box">
|
||||
<span class="select-text" id="equipment-select-text">Выберите оборудование...</span>
|
||||
<svg class="dropdown-icon" width="12" height="8" viewBox="0 0 12 8" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="#9ca3af" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="equipment-dropdown-menu">
|
||||
<div id="equipment-checkboxes" class="checkbox-group-vertical">
|
||||
<!-- Подгружается через JS -->
|
||||
<p class="text-muted"><small>Загрузка...</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: flex-end;">
|
||||
<button type="submit" class="btn-create">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-alert" id="create-classroom-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Список аудиторий</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="classrooms-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Вместимость</th>
|
||||
<th>Оборудование</th>
|
||||
<th>Статус</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="classrooms-tbody">
|
||||
<tr>
|
||||
<td colspan="6" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Subjects Tab ===== -->
|
||||
<section class="content tab-content" id="tab-subjects" style="display:none;">
|
||||
<div class="card create-card">
|
||||
<h2>Новая дисциплина</h2>
|
||||
<form id="create-subject-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-subject-name">Название дисциплины</label>
|
||||
<input type="text" id="new-subject-name" placeholder="Высшая математика" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-create">Добавить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-subject-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card create-card">
|
||||
<h2>Привязка преподавателя к дисциплине</h2>
|
||||
<form id="assign-teacher-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="assign-teacher-select">Преподаватель</label>
|
||||
<select id="assign-teacher-select">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="assign-subject-select">Дисциплина</label>
|
||||
<select id="assign-subject-select">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-create">Привязать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="assign-teacher-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Все дисциплины</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="subjects-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="subjects-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Привязки преподавателей</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="teacher-subjects-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Преподаватель</th>
|
||||
<th>Дисциплина</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="teacher-subjects-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Edit Classroom Modal ===== -->
|
||||
<div class="modal-overlay" id="modal-edit-classroom">
|
||||
<div class="modal-content">
|
||||
<h2>Редактировать аудиторию</h2>
|
||||
<button class="modal-close" id="modal-edit-classroom-close">×</button>
|
||||
<form id="edit-classroom-form">
|
||||
<input type="hidden" id="edit-classroom-id">
|
||||
<div class="form-row" style="margin-top: 1rem;">
|
||||
<div class="form-group">
|
||||
<label for="edit-classroom-name">Номер / Название</label>
|
||||
<input type="text" id="edit-classroom-name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-classroom-capacity">Вместимость (чел.)</label>
|
||||
<input type="number" id="edit-classroom-capacity" min="1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top: 1rem;">
|
||||
<div class="form-group" style="flex: 2;">
|
||||
<label>Оборудование</label>
|
||||
<div class="custom-multi-select">
|
||||
<div class="select-box" id="edit-equipment-select-box">
|
||||
<span class="select-text" id="edit-equipment-select-text">Выберите
|
||||
оборудование...</span>
|
||||
<svg class="dropdown-icon" width="12" height="8" viewBox="0 0 12 8" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="#9ca3af" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="edit-equipment-dropdown-menu">
|
||||
<div id="edit-equipment-checkboxes" class="checkbox-group-vertical">
|
||||
<!-- Подгружается через JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: flex-end;">
|
||||
<button type="submit" class="btn-create" style="width: 100%;">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-alert" id="edit-classroom-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script src="/theme-toggle.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
<script type="module" src="js/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
57
frontend/admin/js/api.js
Executable file
57
frontend/admin/js/api.js
Executable file
@@ -0,0 +1,57 @@
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
export function getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
export function isAuthenticatedAsAdmin() {
|
||||
const role = localStorage.getItem('role');
|
||||
return token && role === 'ADMIN';
|
||||
}
|
||||
|
||||
function getHeaders(contentType = 'application/json') {
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${token}`
|
||||
};
|
||||
if (contentType) {
|
||||
headers['Content-Type'] = contentType;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
export async function apiFetch(endpoint, method = 'GET', body = null) {
|
||||
const options = {
|
||||
method,
|
||||
headers: getHeaders(body ? 'application/json' : null)
|
||||
};
|
||||
|
||||
if (body) {
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
|
||||
const response = await fetch(endpoint, options);
|
||||
|
||||
// Si status is 401 or 403, we should probably redirect to login,
|
||||
// but for now let's just throw an error or handle it in the view.
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = await response.json();
|
||||
} catch (e) {
|
||||
data = null;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data?.message || `Ошибка HTTP: ${response.status}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// Shortcut methods
|
||||
export const api = {
|
||||
get: (url) => apiFetch(url, 'GET'),
|
||||
post: (url, body) => apiFetch(url, 'POST', body),
|
||||
put: (url, body) => apiFetch(url, 'PUT', body),
|
||||
delete: (url, body = null) => apiFetch(url, 'DELETE', body)
|
||||
};
|
||||
104
frontend/admin/js/main.js
Executable file
104
frontend/admin/js/main.js
Executable file
@@ -0,0 +1,104 @@
|
||||
import { isAuthenticatedAsAdmin } from './api.js';
|
||||
import { applyRippleEffect, closeAllDropdownsOnOutsideClick } from './utils.js';
|
||||
|
||||
import { initUsers } from './views/users.js';
|
||||
import { initGroups } from './views/groups.js';
|
||||
import { initEduForms } from './views/edu-forms.js';
|
||||
import { initEquipments } from './views/equipments.js';
|
||||
import { initClassrooms } from './views/classrooms.js';
|
||||
import { initSubjects } from './views/subjects.js';
|
||||
import {initSchedule} from "./views/schedule.js";
|
||||
|
||||
// Configuration
|
||||
const ROUTES = {
|
||||
users: { title: 'Управление пользователями', file: 'views/users.html', init: initUsers },
|
||||
groups: { title: 'Управление группами', file: 'views/groups.html', init: initGroups },
|
||||
'edu-forms': { title: 'Формы обучения', file: 'views/edu-forms.html', init: initEduForms },
|
||||
equipments: { title: 'Оборудование', file: 'views/equipments.html', init: initEquipments },
|
||||
classrooms: { title: 'Аудитории', file: 'views/classrooms.html', init: initClassrooms },
|
||||
subjects: { title: 'Дисциплины и преподаватели', file: 'views/subjects.html', init: initSubjects },
|
||||
// Новая вкладка
|
||||
schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule },
|
||||
};
|
||||
|
||||
let currentTab = null;
|
||||
|
||||
// DOM Elements
|
||||
const appContent = document.getElementById('app-content');
|
||||
const pageTitle = document.getElementById('page-title');
|
||||
const navItems = document.querySelectorAll('.nav-item[data-tab]');
|
||||
const sidebar = document.querySelector('.sidebar');
|
||||
const sidebarOverlay = document.getElementById('sidebar-overlay');
|
||||
const menuToggle = document.getElementById('menu-toggle');
|
||||
const btnLogout = document.getElementById('btn-logout');
|
||||
|
||||
// Initial auth check
|
||||
if (!isAuthenticatedAsAdmin()) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
|
||||
// Setup Global Effects
|
||||
applyRippleEffect();
|
||||
closeAllDropdownsOnOutsideClick();
|
||||
|
||||
// Menu Toggle
|
||||
menuToggle.addEventListener('click', () => {
|
||||
sidebar.classList.toggle('open');
|
||||
sidebarOverlay.classList.toggle('open');
|
||||
});
|
||||
sidebarOverlay.addEventListener('click', () => {
|
||||
sidebar.classList.remove('open');
|
||||
sidebarOverlay.classList.remove('open');
|
||||
});
|
||||
|
||||
// Logout
|
||||
btnLogout.addEventListener('click', () => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('role');
|
||||
window.location.href = '/';
|
||||
});
|
||||
|
||||
// Navigation
|
||||
navItems.forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const tab = item.dataset.tab;
|
||||
switchTab(tab);
|
||||
});
|
||||
});
|
||||
|
||||
async function switchTab(tab) {
|
||||
if (currentTab === tab || !ROUTES[tab]) return;
|
||||
|
||||
// UI Update
|
||||
navItems.forEach(n => n.classList.remove('active'));
|
||||
document.querySelector(`.nav-item[data-tab="${tab}"]`)?.classList.add('active');
|
||||
pageTitle.textContent = ROUTES[tab].title;
|
||||
|
||||
// Load template
|
||||
try {
|
||||
appContent.innerHTML = '<div class="loading-row">Загрузка...</div>';
|
||||
const response = await fetch(ROUTES[tab].file);
|
||||
if (!response.ok) throw new Error('Failed to load view');
|
||||
|
||||
const html = await response.text();
|
||||
appContent.innerHTML = html;
|
||||
|
||||
// Initialize logic for the tab
|
||||
if (ROUTES[tab].init) {
|
||||
ROUTES[tab].init();
|
||||
}
|
||||
|
||||
currentTab = tab;
|
||||
} catch (e) {
|
||||
appContent.innerHTML = `<div class="form-alert error">Ошибка загрузки вкладки: ${e.message}</div>`;
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
// Close mobile menu if open
|
||||
sidebar.classList.remove('open');
|
||||
sidebarOverlay.classList.remove('open');
|
||||
}
|
||||
|
||||
// Load default tab
|
||||
switchTab('users');
|
||||
102
frontend/admin/js/utils.js
Executable file
102
frontend/admin/js/utils.js
Executable file
@@ -0,0 +1,102 @@
|
||||
export const ESCAPE_MAP = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
|
||||
export function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/[&<>"']/g, m => ESCAPE_MAP[m]);
|
||||
}
|
||||
|
||||
export function showAlert(elementId, msg, type) {
|
||||
const el = document.getElementById(elementId);
|
||||
if (!el) return;
|
||||
el.className = 'form-alert ' + type;
|
||||
el.textContent = msg;
|
||||
}
|
||||
|
||||
export function hideAlert(elementId) {
|
||||
const el = document.getElementById(elementId);
|
||||
if (!el) return;
|
||||
el.className = 'form-alert';
|
||||
el.textContent = '';
|
||||
}
|
||||
|
||||
export function applyRippleEffect() {
|
||||
document.addEventListener('click', function (e) {
|
||||
const btn = e.target.closest('.btn-primary, .btn-delete, .btn-logout');
|
||||
if (!btn) return;
|
||||
|
||||
const rect = btn.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const ripple = document.createElement('span');
|
||||
ripple.classList.add('ripple');
|
||||
ripple.style.left = `${x}px`;
|
||||
ripple.style.top = `${y}px`;
|
||||
|
||||
if (getComputedStyle(btn).position === 'static') {
|
||||
btn.style.position = 'relative';
|
||||
}
|
||||
btn.style.overflow = 'hidden';
|
||||
|
||||
btn.appendChild(ripple);
|
||||
|
||||
setTimeout(() => ripple.remove(), 600);
|
||||
});
|
||||
}
|
||||
|
||||
export function initMultiSelect(boxId, menuId, textId, checkboxContainerId) {
|
||||
const box = document.getElementById(boxId);
|
||||
const menu = document.getElementById(menuId);
|
||||
const container = document.getElementById(checkboxContainerId);
|
||||
if (!box || !menu || !container) return;
|
||||
|
||||
// Remove old listeners to prevent duplication if re-initialized
|
||||
const newBox = box.cloneNode(true);
|
||||
box.parentNode.replaceChild(newBox, box);
|
||||
|
||||
newBox.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = menu.classList.contains('open');
|
||||
document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('open'));
|
||||
document.querySelectorAll('.select-box').forEach(b => b.classList.remove('active'));
|
||||
if (!isOpen) {
|
||||
menu.classList.add('open');
|
||||
newBox.classList.add('active');
|
||||
}
|
||||
});
|
||||
|
||||
menu.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
|
||||
container.addEventListener('change', () => {
|
||||
updateSelectText(checkboxContainerId, textId);
|
||||
});
|
||||
}
|
||||
|
||||
export function updateSelectText(containerId, textId) {
|
||||
const container = document.getElementById(containerId);
|
||||
const textEl = document.getElementById(textId);
|
||||
if (!container || !textEl) return;
|
||||
const checked = Array.from(container.querySelectorAll('input:checked'));
|
||||
if (checked.length === 0) {
|
||||
textEl.textContent = 'Выберите оборудование...';
|
||||
} else if (checked.length === 1) {
|
||||
textEl.textContent = checked[0].parentElement.textContent.trim();
|
||||
} else {
|
||||
textEl.textContent = `Выбрано: ${checked.length}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function closeAllDropdownsOnOutsideClick() {
|
||||
document.addEventListener('click', () => {
|
||||
document.querySelectorAll('.dropdown-menu').forEach(m => m.classList.remove('open'));
|
||||
document.querySelectorAll('.select-box').forEach(b => b.classList.remove('active'));
|
||||
});
|
||||
}
|
||||
185
frontend/admin/js/views/classrooms.js
Executable file
185
frontend/admin/js/views/classrooms.js
Executable file
@@ -0,0 +1,185 @@
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert, initMultiSelect, updateSelectText } from '../utils.js';
|
||||
import { fetchEquipments, renderEquipmentCheckboxes } from './equipments.js';
|
||||
|
||||
export async function initClassrooms() {
|
||||
const classroomsTbody = document.getElementById('classrooms-tbody');
|
||||
const createClassroomForm = document.getElementById('create-classroom-form');
|
||||
|
||||
const equipmentCheckboxes = document.getElementById('equipment-checkboxes');
|
||||
const editEquipmentCheckboxes = document.getElementById('edit-equipment-checkboxes');
|
||||
|
||||
const modalEditClassroom = document.getElementById('modal-edit-classroom');
|
||||
const modalEditClassroomClose = document.getElementById('modal-edit-classroom-close');
|
||||
const editClassroomForm = document.getElementById('edit-classroom-form');
|
||||
|
||||
let allEquipments = [];
|
||||
let editingClassroomData = null;
|
||||
|
||||
initMultiSelect('equipment-select-box', 'equipment-dropdown-menu', 'equipment-select-text', 'equipment-checkboxes');
|
||||
initMultiSelect('edit-equipment-select-box', 'edit-equipment-dropdown-menu', 'edit-equipment-select-text', 'edit-equipment-checkboxes');
|
||||
|
||||
async function loadInitialData() {
|
||||
try {
|
||||
allEquipments = await fetchEquipments();
|
||||
renderEquipmentCheckboxes(allEquipments, 'equipment-checkboxes', 'equipment-select-text');
|
||||
await loadClassrooms();
|
||||
} catch (e) {
|
||||
classroomsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Ошибка загрузки данных</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadClassrooms() {
|
||||
try {
|
||||
const classrooms = await api.get('/api/classrooms');
|
||||
renderClassrooms(classrooms);
|
||||
} catch (e) {
|
||||
classroomsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderClassrooms(classrooms) {
|
||||
if (!classrooms || !classrooms.length) {
|
||||
classroomsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Нет аудиторий</td></tr>';
|
||||
return;
|
||||
}
|
||||
classroomsTbody.innerHTML = classrooms.map(c => {
|
||||
const equipHtml = c.equipments && c.equipments.length
|
||||
? c.equipments.map(eq => escapeHtml(eq.name)).join(', ')
|
||||
: '—';
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td>${c.id}</td>
|
||||
<td><strong>${escapeHtml(c.name)}</strong></td>
|
||||
<td>${c.capacity} чел.</td>
|
||||
<td><small>${equipHtml}</small></td>
|
||||
<td>
|
||||
<div class="status-cell">
|
||||
<span class="badge ${c.isAvailable ? 'badge-available' : 'badge-unavailable'}">
|
||||
${c.isAvailable ? 'Доступна' : 'Не доступна'}
|
||||
</span>
|
||||
<button class="btn-icon-toggle" data-id="${c.id}" data-current-status="${c.isAvailable}" title="Сменить статус">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td style="text-align: right;">
|
||||
<button class="btn-edit-classroom" data-id="${c.id}">Изменить</button>
|
||||
<button class="btn-delete" data-id="${c.id}" style="margin-left: 0.5rem;">Удалить</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
createClassroomForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-classroom-alert');
|
||||
const name = document.getElementById('new-classroom-name').value.trim();
|
||||
const capacity = parseInt(document.getElementById('new-classroom-capacity').value, 10);
|
||||
|
||||
const checkedBoxes = Array.from(equipmentCheckboxes.querySelectorAll('input:checked'));
|
||||
const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10));
|
||||
|
||||
if (!name || isNaN(capacity)) { showAlert('create-classroom-alert', 'Заполните обязательные поля', 'error'); return; }
|
||||
|
||||
try {
|
||||
const data = await api.post('/api/classrooms', { name, capacity, equipmentIds, isAvailable: true });
|
||||
showAlert('create-classroom-alert', `Аудитория "${escapeHtml(data.name)}" добавлена`, 'success');
|
||||
createClassroomForm.reset();
|
||||
updateSelectText('equipment-checkboxes', 'equipment-select-text');
|
||||
loadClassrooms();
|
||||
} catch (e) {
|
||||
showAlert('create-classroom-alert', e.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
classroomsTbody.addEventListener('click', async (e) => {
|
||||
const btnDelete = e.target.closest('.btn-delete');
|
||||
const btnToggleStatus = e.target.closest('.btn-icon-toggle');
|
||||
const btnEdit = e.target.closest('.btn-edit-classroom');
|
||||
|
||||
if (btnDelete) {
|
||||
if (!confirm('Удалить аудиторию?')) return;
|
||||
try {
|
||||
await api.delete('/api/classrooms/' + btnDelete.dataset.id);
|
||||
loadClassrooms();
|
||||
} catch (err) { alert('Ошибка удаления'); }
|
||||
}
|
||||
|
||||
if (btnToggleStatus) {
|
||||
const id = btnToggleStatus.dataset.id;
|
||||
const currentStatus = btnToggleStatus.dataset.currentStatus === 'true';
|
||||
try {
|
||||
await api.put('/api/classrooms/' + id, { isAvailable: !currentStatus });
|
||||
loadClassrooms();
|
||||
} catch (err) { alert('Ошибка изменения статуса'); }
|
||||
}
|
||||
|
||||
if (btnEdit) {
|
||||
openEditClassroomModal(btnEdit.dataset.id);
|
||||
}
|
||||
});
|
||||
|
||||
async function openEditClassroomModal(id) {
|
||||
try {
|
||||
// Can optimize by using already loaded classrooms, but fetch is safer to get fresh data
|
||||
const classrooms = await api.get('/api/classrooms');
|
||||
editingClassroomData = classrooms.find(c => c.id == id);
|
||||
|
||||
if (!editingClassroomData) return;
|
||||
|
||||
document.getElementById('edit-classroom-id').value = editingClassroomData.id;
|
||||
document.getElementById('edit-classroom-name').value = editingClassroomData.name;
|
||||
document.getElementById('edit-classroom-capacity').value = editingClassroomData.capacity;
|
||||
|
||||
const existingEquipIds = editingClassroomData.equipments.map(e => e.id);
|
||||
renderEquipmentCheckboxes(allEquipments, 'edit-equipment-checkboxes', 'edit-equipment-select-text', existingEquipIds);
|
||||
|
||||
hideAlert('edit-classroom-alert');
|
||||
modalEditClassroom.classList.add('open');
|
||||
} catch (e) {
|
||||
alert('Ошибка загрузки данных аудитории');
|
||||
}
|
||||
}
|
||||
|
||||
modalEditClassroomClose.addEventListener('click', () => {
|
||||
modalEditClassroom.classList.remove('open');
|
||||
});
|
||||
|
||||
modalEditClassroom.addEventListener('click', (e) => {
|
||||
if (e.target === modalEditClassroom) {
|
||||
modalEditClassroom.classList.remove('open');
|
||||
}
|
||||
});
|
||||
|
||||
editClassroomForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('edit-classroom-alert');
|
||||
const id = document.getElementById('edit-classroom-id').value;
|
||||
const name = document.getElementById('edit-classroom-name').value.trim();
|
||||
const capacity = parseInt(document.getElementById('edit-classroom-capacity').value, 10);
|
||||
|
||||
const checkedBoxes = Array.from(editEquipmentCheckboxes.querySelectorAll('input:checked'));
|
||||
const equipmentIds = checkedBoxes.map(chk => parseInt(chk.value, 10));
|
||||
|
||||
if (!name || isNaN(capacity)) { showAlert('edit-classroom-alert', 'Заполните обязательные поля', 'error'); return; }
|
||||
|
||||
try {
|
||||
const data = await api.put('/api/classrooms/' + id, {
|
||||
name,
|
||||
capacity,
|
||||
equipmentIds,
|
||||
isAvailable: editingClassroomData.isAvailable
|
||||
});
|
||||
modalEditClassroom.classList.remove('open');
|
||||
// We show alert on the main create form area or we could use toast
|
||||
showAlert('create-classroom-alert', `Аудитория "${escapeHtml(data.name)}" обновлена`, 'success');
|
||||
loadClassrooms();
|
||||
} catch (e) {
|
||||
showAlert('edit-classroom-alert', e.message || 'Ошибка обновления', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
loadInitialData();
|
||||
}
|
||||
71
frontend/admin/js/views/edu-forms.js
Executable file
71
frontend/admin/js/views/edu-forms.js
Executable file
@@ -0,0 +1,71 @@
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
|
||||
export let allEducationForms = [];
|
||||
|
||||
export async function fetchEducationForms() {
|
||||
try {
|
||||
allEducationForms = await api.get('/api/education-forms');
|
||||
return allEducationForms;
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch education forms", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function initEduForms() {
|
||||
const efTbody = document.getElementById('ef-tbody');
|
||||
const createEfForm = document.getElementById('create-ef-form');
|
||||
|
||||
async function loadEF() {
|
||||
try {
|
||||
const forms = await fetchEducationForms();
|
||||
renderEfTable(forms);
|
||||
} catch (e) {
|
||||
efTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderEfTable(forms) {
|
||||
if (!forms || !forms.length) {
|
||||
efTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет форм обучения</td></tr>';
|
||||
return;
|
||||
}
|
||||
efTbody.innerHTML = forms.map(ef => `
|
||||
<tr>
|
||||
<td>${ef.id}</td>
|
||||
<td>${escapeHtml(ef.name)}</td>
|
||||
<td><button class="btn-delete" data-id="${ef.id}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
createEfForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-ef-alert');
|
||||
const name = document.getElementById('new-ef-name').value.trim();
|
||||
if (!name) { showAlert('create-ef-alert', 'Введите название', 'error'); return; }
|
||||
|
||||
try {
|
||||
const data = await api.post('/api/education-forms', { name });
|
||||
showAlert('create-ef-alert', `Форма "${escapeHtml(data.name)}" создана`, 'success');
|
||||
createEfForm.reset();
|
||||
loadEF();
|
||||
} catch (e) {
|
||||
showAlert('create-ef-alert', e.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
efTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить форму обучения?')) return;
|
||||
try {
|
||||
await api.delete('/api/education-forms/' + btn.dataset.id);
|
||||
loadEF();
|
||||
} catch (e) {
|
||||
alert(e.message || 'Ошибка удаления');
|
||||
}
|
||||
});
|
||||
|
||||
loadEF();
|
||||
}
|
||||
88
frontend/admin/js/views/equipments.js
Executable file
88
frontend/admin/js/views/equipments.js
Executable file
@@ -0,0 +1,88 @@
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert, updateSelectText } from '../utils.js';
|
||||
|
||||
export let allEquipments = [];
|
||||
|
||||
export async function fetchEquipments() {
|
||||
try {
|
||||
allEquipments = await api.get('/api/equipments');
|
||||
return allEquipments;
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch equipments", e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function renderEquipmentCheckboxes(equipments, containerId, textId, checkedIds = []) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
if (!equipments.length) {
|
||||
container.innerHTML = '<p class="text-muted"><small>Нет доступного оборудования</small></p>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = equipments.map(eq => {
|
||||
const isChecked = checkedIds.includes(eq.id) ? 'checked' : '';
|
||||
return `
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="${eq.id}" ${isChecked}> ${escapeHtml(eq.name)}
|
||||
</label>
|
||||
`}).join('');
|
||||
updateSelectText(containerId, textId);
|
||||
}
|
||||
|
||||
export async function initEquipments() {
|
||||
const equipmentsTbody = document.getElementById('equipments-tbody');
|
||||
const createEquipmentForm = document.getElementById('create-equipment-form');
|
||||
|
||||
async function loadEquipments() {
|
||||
try {
|
||||
const equipments = await fetchEquipments();
|
||||
renderEquipments(equipments);
|
||||
} catch (e) {
|
||||
if (equipmentsTbody) equipmentsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderEquipments(equipments) {
|
||||
if (!equipments || !equipments.length) {
|
||||
equipmentsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет оборудования</td></tr>';
|
||||
return;
|
||||
}
|
||||
equipmentsTbody.innerHTML = equipments.map(eq => `
|
||||
<tr>
|
||||
<td>${eq.id}</td>
|
||||
<td>${escapeHtml(eq.name)}</td>
|
||||
<td><button class="btn-delete" data-id="${eq.id}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
createEquipmentForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-equipment-alert');
|
||||
const name = document.getElementById('new-equipment-name').value.trim();
|
||||
if (!name) { showAlert('create-equipment-alert', 'Введите название', 'error'); return; }
|
||||
|
||||
try {
|
||||
const data = await api.post('/api/equipments', { name });
|
||||
showAlert('create-equipment-alert', `Оборудование "${escapeHtml(data.name)}" добавлено`, 'success');
|
||||
createEquipmentForm.reset();
|
||||
loadEquipments();
|
||||
} catch (e) {
|
||||
showAlert('create-equipment-alert', e.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
equipmentsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить оборудование?')) return;
|
||||
try {
|
||||
await api.delete('/api/equipments/' + btn.dataset.id);
|
||||
loadEquipments();
|
||||
} catch (e) {
|
||||
alert(e.message || 'Ошибка удаления');
|
||||
}
|
||||
});
|
||||
|
||||
loadEquipments();
|
||||
}
|
||||
108
frontend/admin/js/views/groups.js
Executable file
108
frontend/admin/js/views/groups.js
Executable file
@@ -0,0 +1,108 @@
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
import { fetchEducationForms } from './edu-forms.js';
|
||||
|
||||
export async function initGroups() {
|
||||
const groupsTbody = document.getElementById('groups-tbody');
|
||||
const createGroupForm = document.getElementById('create-group-form');
|
||||
const newGroupEfSelect = document.getElementById('new-group-ef');
|
||||
const filterEfSelect = document.getElementById('filter-ef');
|
||||
|
||||
let allGroups = [];
|
||||
let educationForms = [];
|
||||
|
||||
async function loadInitialData() {
|
||||
try {
|
||||
educationForms = await fetchEducationForms();
|
||||
populateEfSelects(educationForms);
|
||||
await loadGroups();
|
||||
} catch (e) {
|
||||
groupsTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Ошибка загрузки данных</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
try {
|
||||
allGroups = await api.get('/api/groups');
|
||||
applyGroupFilter();
|
||||
} catch (e) {
|
||||
groupsTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function applyGroupFilter() {
|
||||
const filterId = filterEfSelect.value;
|
||||
const filtered = filterId
|
||||
? allGroups.filter(g => g.educationFormId == filterId)
|
||||
: allGroups;
|
||||
renderGroups(filtered);
|
||||
}
|
||||
|
||||
filterEfSelect.addEventListener('change', applyGroupFilter);
|
||||
|
||||
function populateEfSelects(forms) {
|
||||
// Group creation select
|
||||
const currentVal = newGroupEfSelect.value;
|
||||
newGroupEfSelect.innerHTML = forms.map(ef =>
|
||||
`<option value="${ef.id}">${escapeHtml(ef.name)}</option>`
|
||||
).join('');
|
||||
if (currentVal && forms.find(f => f.id == currentVal)) {
|
||||
newGroupEfSelect.value = currentVal;
|
||||
}
|
||||
|
||||
// Filter select
|
||||
const currentFilter = filterEfSelect.value;
|
||||
filterEfSelect.innerHTML = '<option value="">Все формы</option>' +
|
||||
forms.map(ef =>
|
||||
`<option value="${ef.id}">${escapeHtml(ef.name)}</option>`
|
||||
).join('');
|
||||
if (currentFilter) filterEfSelect.value = currentFilter;
|
||||
}
|
||||
|
||||
function renderGroups(groups) {
|
||||
if (!groups || !groups.length) {
|
||||
groupsTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Нет групп</td></tr>';
|
||||
return;
|
||||
}
|
||||
groupsTbody.innerHTML = groups.map(g => `
|
||||
<tr>
|
||||
<td>${g.id}</td>
|
||||
<td>${escapeHtml(g.name)}</td>
|
||||
<td><span class="badge badge-ef">${escapeHtml(g.educationFormName)}</span></td>
|
||||
<td><button class="btn-delete" data-id="${g.id}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
createGroupForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-group-alert');
|
||||
const name = document.getElementById('new-group-name').value.trim();
|
||||
const educationFormId = newGroupEfSelect.value;
|
||||
|
||||
if (!name) { showAlert('create-group-alert', 'Введите название группы', 'error'); return; }
|
||||
if (!educationFormId) { showAlert('create-group-alert', 'Выберите форму обучения', 'error'); return; }
|
||||
|
||||
try {
|
||||
const data = await api.post('/api/groups', { name, educationFormId: Number(educationFormId) });
|
||||
showAlert('create-group-alert', `Группа "${escapeHtml(data.name)}" создана`, 'success');
|
||||
createGroupForm.reset();
|
||||
loadGroups();
|
||||
} catch (e) {
|
||||
showAlert('create-group-alert', e.message || 'Ошибка создания', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
groupsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить группу?')) return;
|
||||
try {
|
||||
await api.delete('/api/groups/' + btn.dataset.id);
|
||||
loadGroups();
|
||||
} catch (e) {
|
||||
alert(e.message || 'Ошибка удаления');
|
||||
}
|
||||
});
|
||||
|
||||
loadInitialData();
|
||||
}
|
||||
370
frontend/admin/js/views/schedule.js
Executable file
370
frontend/admin/js/views/schedule.js
Executable file
@@ -0,0 +1,370 @@
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml } from '../utils.js';
|
||||
|
||||
export async function initSchedule() {
|
||||
const tbody = document.getElementById('schedule-tbody');
|
||||
const table = document.getElementById('schedule-table');
|
||||
|
||||
let lessonsData = [];
|
||||
let sortKey = null;
|
||||
let sortDir = 'asc';
|
||||
|
||||
// Активные фильтры: { teacher: Set, group: Set, subject: Set, day: Set }
|
||||
const activeFilters = {};
|
||||
|
||||
// Маппинг дней недели для корректной сортировки
|
||||
const dayOrder = {
|
||||
'понедельник': 1, 'вторник': 2, 'среда': 3,
|
||||
'четверг': 4, 'пятница': 5, 'суббота': 6, 'воскресенье': 7
|
||||
};
|
||||
|
||||
// ===================== Фильтрация =====================
|
||||
|
||||
// Извлечение отображаемого значения поля для фильтрации
|
||||
function getDisplayValue(lesson, key) {
|
||||
switch (key) {
|
||||
case 'teacher':
|
||||
return lesson.teacher?.username || lesson.teacherName || '—';
|
||||
case 'group':
|
||||
return lesson.group?.name || lesson.groupName || '—';
|
||||
case 'subject':
|
||||
return lesson.subject?.name || lesson.subjectName || '—';
|
||||
case 'day':
|
||||
return lesson.day || '—';
|
||||
case 'educationForm':
|
||||
return lesson.educationForm?.name || lesson.educationFormName || '—';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Собрать уникальные значения из данных
|
||||
function getUniqueValues(key) {
|
||||
const vals = new Set();
|
||||
lessonsData.forEach(lesson => {
|
||||
vals.add(getDisplayValue(lesson, key));
|
||||
});
|
||||
// Для дней — сортируем по порядку
|
||||
if (key === 'day') {
|
||||
return [...vals].sort((a, b) => (dayOrder[a.toLowerCase()] ?? 99) - (dayOrder[b.toLowerCase()] ?? 99));
|
||||
}
|
||||
return [...vals].sort((a, b) => a.localeCompare(b, 'ru'));
|
||||
}
|
||||
|
||||
// Применить все фильтры
|
||||
function applyFilters(lessons) {
|
||||
return lessons.filter(lesson => {
|
||||
for (const key of Object.keys(activeFilters)) {
|
||||
const filterSet = activeFilters[key];
|
||||
if (filterSet && filterSet.size > 0) {
|
||||
const val = getDisplayValue(lesson, key);
|
||||
if (!filterSet.has(val)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// ===================== Попап фильтра =====================
|
||||
|
||||
let currentPopup = null;
|
||||
|
||||
function closePopup() {
|
||||
if (currentPopup) {
|
||||
currentPopup.remove();
|
||||
currentPopup = null;
|
||||
}
|
||||
document.removeEventListener('click', onDocumentClick, true);
|
||||
}
|
||||
|
||||
function onDocumentClick(e) {
|
||||
if (currentPopup && !currentPopup.contains(e.target)) {
|
||||
// Проверяем, не кликнули ли по иконке фильтра
|
||||
if (!e.target.closest('.filter-icon')) {
|
||||
closePopup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openFilterPopup(th, filterKey) {
|
||||
// Если уже открыт этот же — закрыть
|
||||
if (currentPopup && currentPopup.dataset.filterKey === filterKey) {
|
||||
closePopup();
|
||||
return;
|
||||
}
|
||||
closePopup();
|
||||
|
||||
const uniqueValues = getUniqueValues(filterKey);
|
||||
const currentFilter = activeFilters[filterKey];
|
||||
|
||||
// Создаём попап
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'filter-popup';
|
||||
popup.dataset.filterKey = filterKey;
|
||||
|
||||
// Поисковое поле
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'text';
|
||||
searchInput.className = 'filter-search';
|
||||
searchInput.placeholder = 'Поиск...';
|
||||
popup.appendChild(searchInput);
|
||||
|
||||
// Кнопки «Выбрать все» / «Сбросить»
|
||||
const btnRow = document.createElement('div');
|
||||
btnRow.className = 'filter-btn-row';
|
||||
|
||||
const btnAll = document.createElement('button');
|
||||
btnAll.className = 'filter-btn-action';
|
||||
btnAll.textContent = 'Все';
|
||||
btnAll.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
checkboxes.forEach(cb => { cb.checked = true; });
|
||||
});
|
||||
|
||||
const btnNone = document.createElement('button');
|
||||
btnNone.className = 'filter-btn-action';
|
||||
btnNone.textContent = 'Сбросить';
|
||||
btnNone.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
checkboxes.forEach(cb => { cb.checked = false; });
|
||||
});
|
||||
|
||||
btnRow.appendChild(btnAll);
|
||||
btnRow.appendChild(btnNone);
|
||||
popup.appendChild(btnRow);
|
||||
|
||||
// Список чекбоксов
|
||||
const listWrap = document.createElement('div');
|
||||
listWrap.className = 'filter-list';
|
||||
|
||||
const checkboxes = [];
|
||||
|
||||
uniqueValues.forEach(val => {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'filter-item';
|
||||
|
||||
const cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.value = val;
|
||||
// Если фильтр активен — отмечаем только выбранные; если нет — все отмечены
|
||||
cb.checked = currentFilter ? currentFilter.has(val) : true;
|
||||
|
||||
const span = document.createElement('span');
|
||||
span.textContent = val;
|
||||
|
||||
label.appendChild(cb);
|
||||
label.appendChild(span);
|
||||
listWrap.appendChild(label);
|
||||
checkboxes.push(cb);
|
||||
});
|
||||
|
||||
popup.appendChild(listWrap);
|
||||
|
||||
// Кнопка «Применить»
|
||||
const btnApply = document.createElement('button');
|
||||
btnApply.className = 'filter-btn-apply';
|
||||
btnApply.textContent = 'Применить';
|
||||
btnApply.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const selected = new Set();
|
||||
checkboxes.forEach(cb => {
|
||||
if (cb.checked) selected.add(cb.value);
|
||||
});
|
||||
|
||||
// Если все выбраны — снимаем фильтр
|
||||
if (selected.size === uniqueValues.length) {
|
||||
delete activeFilters[filterKey];
|
||||
th.classList.remove('filter-active');
|
||||
} else {
|
||||
activeFilters[filterKey] = selected;
|
||||
th.classList.add('filter-active');
|
||||
}
|
||||
|
||||
closePopup();
|
||||
renderSchedule(lessonsData);
|
||||
});
|
||||
popup.appendChild(btnApply);
|
||||
|
||||
// Поиск по чекбоксам
|
||||
searchInput.addEventListener('input', () => {
|
||||
const query = searchInput.value.toLowerCase();
|
||||
listWrap.querySelectorAll('.filter-item').forEach(item => {
|
||||
const text = item.querySelector('span').textContent.toLowerCase();
|
||||
item.style.display = text.includes(query) ? '' : 'none';
|
||||
});
|
||||
});
|
||||
|
||||
// Предотвращаем всплытие кликов внутри попапа (чтобы не срабатывала сортировка th)
|
||||
popup.addEventListener('click', (e) => e.stopPropagation());
|
||||
searchInput.addEventListener('click', (e) => e.stopPropagation());
|
||||
|
||||
// Позиционируем попап под th
|
||||
th.style.position = 'relative';
|
||||
th.appendChild(popup);
|
||||
currentPopup = popup;
|
||||
|
||||
// Фокус на поиск
|
||||
setTimeout(() => searchInput.focus(), 50);
|
||||
|
||||
// Закрытие по клику вне
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', onDocumentClick, true);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// Обработчики кликов по заголовкам с фильтрами (клик по всей ячейке)
|
||||
table.querySelectorAll('thead th.filterable').forEach(th => {
|
||||
th.addEventListener('click', (e) => {
|
||||
// Не открываем попап при клике внутри самого попапа
|
||||
if (e.target.closest('.filter-popup')) return;
|
||||
const filterKey = th.dataset.filterKey;
|
||||
openFilterPopup(th, filterKey);
|
||||
});
|
||||
});
|
||||
|
||||
// ===================== Сортировка =====================
|
||||
|
||||
function getSortValue(lesson, key) {
|
||||
switch (key) {
|
||||
case 'id':
|
||||
return lesson.id ?? 0;
|
||||
case 'teacher':
|
||||
return (lesson.teacher?.username || lesson.teacherName || '').toLowerCase();
|
||||
case 'group':
|
||||
return (lesson.group?.name || lesson.groupName || '').toLowerCase();
|
||||
case 'classroomName':
|
||||
return (lesson.classroomName?.name || lesson.classroomName || '').toLowerCase();
|
||||
case 'educationForm':
|
||||
return (lesson.educationForm?.name || lesson.educationFormName || '').toLowerCase();
|
||||
case 'subject':
|
||||
return (lesson.subject?.name || lesson.subjectName || '').toLowerCase();
|
||||
case 'lessonFormat':
|
||||
return (lesson.lessonFormat?.name || lesson.lessonFormat || '').toLowerCase();
|
||||
case 'typeLesson':
|
||||
return (lesson.typeLesson?.name || lesson.typeLesson || '').toLowerCase();
|
||||
case 'day': {
|
||||
const d = (lesson.day || '').toLowerCase();
|
||||
return dayOrder[d] ?? 99;
|
||||
}
|
||||
case 'week':
|
||||
return (lesson.week || '').toLowerCase();
|
||||
case 'time': {
|
||||
// Составной ключ: день + время для правильной сортировки
|
||||
const d = (lesson.day || '').toLowerCase();
|
||||
const dayNum = dayOrder[d] ?? 99;
|
||||
const t = lesson.time || '99:99';
|
||||
return String(dayNum).padStart(2, '0') + '_' + t;
|
||||
}
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function sortLessons(lessons) {
|
||||
if (!sortKey) return lessons;
|
||||
|
||||
return [...lessons].sort((a, b) => {
|
||||
let va = getSortValue(a, sortKey);
|
||||
let vb = getSortValue(b, sortKey);
|
||||
|
||||
if (typeof va === 'number' && typeof vb === 'number') {
|
||||
return sortDir === 'asc' ? va - vb : vb - va;
|
||||
}
|
||||
|
||||
va = String(va);
|
||||
vb = String(vb);
|
||||
const cmp = va.localeCompare(vb, 'ru');
|
||||
return sortDir === 'asc' ? cmp : -cmp;
|
||||
});
|
||||
}
|
||||
|
||||
function updateSortHeaders() {
|
||||
table.querySelectorAll('thead th.sortable').forEach(th => {
|
||||
th.classList.remove('sort-asc', 'sort-desc', 'sort-active');
|
||||
if (th.dataset.sortKey === sortKey) {
|
||||
th.classList.add('sort-active', sortDir === 'asc' ? 'sort-asc' : 'sort-desc');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Навешиваем обработчики клика на заголовки (сортировка)
|
||||
table.querySelectorAll('thead th.sortable').forEach(th => {
|
||||
th.addEventListener('click', (e) => {
|
||||
// Не сортируем, если кликнули по иконке фильтра или внутри попапа
|
||||
if (e.target.closest('.filter-icon') || e.target.closest('.filter-popup')) return;
|
||||
|
||||
const key = th.dataset.sortKey;
|
||||
if (sortKey === key) {
|
||||
if (sortDir === 'asc') {
|
||||
sortDir = 'desc';
|
||||
} else {
|
||||
sortKey = null;
|
||||
sortDir = 'asc';
|
||||
}
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortDir = 'asc';
|
||||
}
|
||||
updateSortHeaders();
|
||||
renderSchedule(lessonsData);
|
||||
});
|
||||
});
|
||||
|
||||
// ===================== Загрузка и рендер =====================
|
||||
|
||||
async function loadSchedule() {
|
||||
try {
|
||||
const lessons = await api.get('/api/users/lessons');
|
||||
lessonsData = lessons;
|
||||
renderSchedule(lessons);
|
||||
} catch (e) {
|
||||
tbody.innerHTML = `<tr><td colspan="8" class="loading-row">Ошибка загрузки: ${escapeHtml(e.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderSchedule(lessons) {
|
||||
if (!lessons || !lessons.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="loading-row">Нет занятий</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Сначала фильтруем, потом сортируем
|
||||
const filtered = applyFilters(lessons);
|
||||
|
||||
if (!filtered.length) {
|
||||
tbody.innerHTML = '<tr><td colspan="8" class="loading-row">Нет занятий по выбранным фильтрам</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
const sorted = sortLessons(filtered);
|
||||
|
||||
tbody.innerHTML = sorted.map(lesson => {
|
||||
const teacherName = lesson.teacher?.username || lesson.teacherName || '—';
|
||||
const groupName = lesson.group?.name || lesson.groupName || '—';
|
||||
const classroomName = lesson.classroom?.name || lesson.classroomName || '—';
|
||||
const educationForm = lesson.educationForm?.name || lesson.educationFormName || '-';
|
||||
const subjectName = lesson.subject?.name || lesson.subjectName || '—';
|
||||
const lessonFormat = lesson.lessonFormat?.name || lesson.lessonFormat || '—';
|
||||
const typeLesson = lesson.typeLesson?.name || lesson.typeLesson || '—';
|
||||
const day = lesson.day || '—';
|
||||
const week = lesson.week || '—';
|
||||
const time = lesson.time || '—';
|
||||
|
||||
return `<tr>
|
||||
<td>${escapeHtml(lesson.id)}</td>
|
||||
<td>${escapeHtml(teacherName)}</td>
|
||||
<td>${escapeHtml(groupName)}</td>
|
||||
<td>${escapeHtml(classroomName)}</td>
|
||||
<td>${escapeHtml(educationForm)}</td>
|
||||
<td>${escapeHtml(subjectName)}</td>
|
||||
<td>${escapeHtml(lessonFormat)}</td>
|
||||
<td>${escapeHtml(typeLesson)}</td>
|
||||
<td>${escapeHtml(day)}</td>
|
||||
<td>${escapeHtml(week)}</td>
|
||||
<td>${escapeHtml(time)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
await loadSchedule();
|
||||
}
|
||||
153
frontend/admin/js/views/subjects.js
Executable file
153
frontend/admin/js/views/subjects.js
Executable file
@@ -0,0 +1,153 @@
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
|
||||
export async function initSubjects() {
|
||||
const subjectsTbody = document.getElementById('subjects-tbody');
|
||||
const createSubjectForm = document.getElementById('create-subject-form');
|
||||
|
||||
const assignTeacherForm = document.getElementById('assign-teacher-form');
|
||||
const assignTeacherSelect = document.getElementById('assign-teacher-select');
|
||||
const assignSubjectSelect = document.getElementById('assign-subject-select');
|
||||
const teacherSubjectsTbody = document.getElementById('teacher-subjects-tbody');
|
||||
|
||||
let allSubjects = [];
|
||||
let allTeachers = [];
|
||||
|
||||
async function loadInitialData() {
|
||||
await Promise.all([loadSubjects(), loadTeachers()]);
|
||||
await loadTeacherSubjects();
|
||||
}
|
||||
|
||||
async function loadSubjects() {
|
||||
try {
|
||||
allSubjects = await api.get('/api/subjects');
|
||||
renderSubjects(allSubjects);
|
||||
populateSubjectSelect(allSubjects);
|
||||
} catch (e) {
|
||||
if (subjectsTbody) subjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderSubjects(subjects) {
|
||||
if (!subjects || !subjects.length) {
|
||||
subjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет дисциплин</td></tr>';
|
||||
return;
|
||||
}
|
||||
subjectsTbody.innerHTML = subjects.map(s => `
|
||||
<tr>
|
||||
<td>${s.id}</td>
|
||||
<td>${escapeHtml(s.name)}</td>
|
||||
<td><button class="btn-delete" data-id="${s.id}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
function populateSubjectSelect(subjects) {
|
||||
if (!assignSubjectSelect) return;
|
||||
const currentVal = assignSubjectSelect.value;
|
||||
assignSubjectSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
|
||||
subjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
|
||||
if (currentVal && subjects.find(s => s.id == currentVal)) {
|
||||
assignSubjectSelect.value = currentVal;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTeachers() {
|
||||
try {
|
||||
allTeachers = await api.get('/api/users/teachers');
|
||||
populateTeacherSelect(allTeachers);
|
||||
} catch (e) {
|
||||
if (assignTeacherSelect) assignTeacherSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
|
||||
}
|
||||
}
|
||||
|
||||
function populateTeacherSelect(teachers) {
|
||||
if (!assignTeacherSelect) return;
|
||||
const currentVal = assignTeacherSelect.value;
|
||||
if (!teachers || !teachers.length) {
|
||||
assignTeacherSelect.innerHTML = '<option value="">Нет преподавателей</option>';
|
||||
return;
|
||||
}
|
||||
assignTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' +
|
||||
teachers.map(t => `<option value="${t.id}">${escapeHtml(t.username)}</option>`).join('');
|
||||
if (currentVal && teachers.find(t => t.id == currentVal)) {
|
||||
assignTeacherSelect.value = currentVal;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTeacherSubjects() {
|
||||
try {
|
||||
const tsData = await api.get('/api/teacher-subjects');
|
||||
renderTeacherSubjects(tsData);
|
||||
} catch (e) {
|
||||
if (teacherSubjectsTbody) teacherSubjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ошибка загрузки</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderTeacherSubjects(tsArray) {
|
||||
if (!tsArray || !tsArray.length) {
|
||||
teacherSubjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет привязок</td></tr>';
|
||||
return;
|
||||
}
|
||||
teacherSubjectsTbody.innerHTML = tsArray.map(ts => `
|
||||
<tr>
|
||||
<td>${escapeHtml(ts.username)}</td>
|
||||
<td>${escapeHtml(ts.subjectName)}</td>
|
||||
<td><button class="btn-delete" data-user-id="${ts.userId}" data-subject-id="${ts.subjectId}">Удалить</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
createSubjectForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-subject-alert');
|
||||
const name = document.getElementById('new-subject-name').value.trim();
|
||||
if (!name) { showAlert('create-subject-alert', 'Введите название', 'error'); return; }
|
||||
|
||||
try {
|
||||
const data = await api.post('/api/subjects', { name });
|
||||
showAlert('create-subject-alert', `Дисциплина "${escapeHtml(data.name)}" добавлена`, 'success');
|
||||
createSubjectForm.reset();
|
||||
loadSubjects();
|
||||
} catch (e) { showAlert('create-subject-alert', e.message || 'Ошибка создания', 'error'); }
|
||||
});
|
||||
|
||||
subjectsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить дисциплину?')) return;
|
||||
try {
|
||||
await api.delete('/api/subjects/' + btn.dataset.id);
|
||||
loadSubjects();
|
||||
loadTeacherSubjects(); // May have deleted a subject that was assigned
|
||||
} catch (e) { alert(e.message || 'Ошибка удаления'); }
|
||||
});
|
||||
|
||||
assignTeacherForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('assign-teacher-alert');
|
||||
const userId = assignTeacherSelect.value;
|
||||
const subjectId = assignSubjectSelect.value;
|
||||
if (!userId) { showAlert('assign-teacher-alert', 'Выберите преподавателя', 'error'); return; }
|
||||
if (!subjectId) { showAlert('assign-teacher-alert', 'Выберите дисциплину', 'error'); return; }
|
||||
|
||||
try {
|
||||
await api.post('/api/teacher-subjects', { userId: Number(userId), subjectId: Number(subjectId) });
|
||||
showAlert('assign-teacher-alert', 'Привязка создана', 'success');
|
||||
loadTeacherSubjects();
|
||||
} catch (e) { showAlert('assign-teacher-alert', e.message || 'Ошибка привязки', 'error'); }
|
||||
});
|
||||
|
||||
teacherSubjectsTbody.addEventListener('click', async (e) => {
|
||||
const btn = e.target.closest('.btn-delete');
|
||||
if (!btn) return;
|
||||
if (!confirm('Удалить привязку?')) return;
|
||||
try {
|
||||
await api.delete('/api/teacher-subjects', {
|
||||
userId: Number(btn.dataset.userId),
|
||||
subjectId: Number(btn.dataset.subjectId)
|
||||
});
|
||||
loadTeacherSubjects();
|
||||
} catch (e) { alert(e.message || 'Ошибка удаления'); }
|
||||
});
|
||||
|
||||
loadInitialData();
|
||||
}
|
||||
269
frontend/admin/js/views/users.js
Executable file
269
frontend/admin/js/views/users.js
Executable file
@@ -0,0 +1,269 @@
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
|
||||
const ROLE_LABELS = { ADMIN: 'Администратор', TEACHER: 'Преподаватель', STUDENT: 'Студент' };
|
||||
const ROLE_BADGE = { ADMIN: 'badge-admin', TEACHER: 'badge-teacher', STUDENT: 'badge-student' };
|
||||
|
||||
export async function initUsers() {
|
||||
const usersTbody = document.getElementById('users-tbody');
|
||||
const createForm = document.getElementById('create-form');
|
||||
|
||||
// Элементы модального окна добавления занятия
|
||||
const modalAddLesson = document.getElementById('modal-add-lesson');
|
||||
const modalAddLessonClose = document.getElementById('modal-add-lesson-close');
|
||||
const addLessonForm = document.getElementById('add-lesson-form');
|
||||
const lessonGroupSelect = document.getElementById('lesson-group');
|
||||
const lessonDisciplineSelect = document.getElementById('lesson-discipline');
|
||||
const lessonUserId = document.getElementById('lesson-user-id');
|
||||
const lessonDaySelect = document.getElementById('lesson-day');
|
||||
const weekUpper = document.getElementById('week-upper');
|
||||
const weekLower = document.getElementById('week-lower');
|
||||
// NEW: получаем элемент выбора времени
|
||||
const lessonTimeSelect = document.getElementById('lesson-time');
|
||||
|
||||
// Переменные для хранения загруженных данных
|
||||
let groups = [];
|
||||
let subjects = [];
|
||||
|
||||
// NEW: массивы с временными слотами
|
||||
const weekdaysTimes = [
|
||||
"8:00-9:30",
|
||||
"9:40-11:10",
|
||||
"11:40-13:10",
|
||||
"13:20-14:50",
|
||||
"15:00-16:30",
|
||||
"16:50-18:20",
|
||||
"18:30-19:00"
|
||||
];
|
||||
|
||||
const saturdayTimes = [
|
||||
"8:20-9:50",
|
||||
"10:00-11:30",
|
||||
"11:40-13:10",
|
||||
"13:20-14:50"
|
||||
];
|
||||
|
||||
// Загрузка групп с сервера
|
||||
async function loadGroups() {
|
||||
try {
|
||||
groups = await api.get('/api/groups');
|
||||
renderGroupOptions();
|
||||
} catch (e) {
|
||||
console.error('Ошибка загрузки групп:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Загрузка дисциплин
|
||||
async function loadSubjects() {
|
||||
try {
|
||||
subjects = await api.get('/api/subjects');
|
||||
renderSubjectOptions();
|
||||
} catch (e) {
|
||||
console.error('Ошибка загрузки дисциплин:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Заполнение select группами
|
||||
function renderGroupOptions() {
|
||||
lessonGroupSelect.innerHTML = '<option value="">Выберите группу</option>' +
|
||||
groups.map(g => `<option value="${g.id}">${escapeHtml(g.name)}</option>`).join('');
|
||||
}
|
||||
|
||||
// Заполнение select дисциплинами
|
||||
function renderSubjectOptions() {
|
||||
lessonDisciplineSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
|
||||
subjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
|
||||
}
|
||||
|
||||
// NEW: функция обновления списка времени в зависимости от дня
|
||||
function updateTimeOptions(dayValue) {
|
||||
let times = [];
|
||||
if (dayValue === "Суббота") {
|
||||
times = saturdayTimes;
|
||||
} else if (dayValue && dayValue !== '') {
|
||||
times = weekdaysTimes;
|
||||
} else {
|
||||
lessonTimeSelect.innerHTML = '<option value="">Сначала выберите день</option>';
|
||||
lessonTimeSelect.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
lessonTimeSelect.innerHTML = '<option value="">Выберите время</option>' +
|
||||
times.map(t => `<option value="${t}">${t}</option>`).join('');
|
||||
lessonTimeSelect.disabled = false;
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
try {
|
||||
const users = await api.get('/api/users');
|
||||
renderUsers(users);
|
||||
} catch (e) {
|
||||
usersTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Ошибка загрузки: ' + escapeHtml(e.message) + '</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderUsers(users) {
|
||||
if (!users || !users.length) {
|
||||
usersTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Нет пользователей</td></tr>';
|
||||
return;
|
||||
}
|
||||
usersTbody.innerHTML = users.map(u => `
|
||||
<tr>
|
||||
<td>${u.id}</td>
|
||||
<td>${escapeHtml(u.username)}</td>
|
||||
<td><span class="badge ${ROLE_BADGE[u.role] || ''}">${ROLE_LABELS[u.role] || escapeHtml(u.role)}</span></td>
|
||||
<td><button class="btn-delete" data-id="${u.id}">Удалить</button></td>
|
||||
<td><button class="btn-add-lesson" data-id="${u.id}">Добавить занятие</button></td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
|
||||
// Сброс формы модального окна
|
||||
function resetLessonForm() {
|
||||
addLessonForm.reset();
|
||||
lessonUserId.value = '';
|
||||
if (weekUpper) weekUpper.checked = false;
|
||||
if (weekLower) weekLower.checked = false;
|
||||
// NEW: сбрасываем селект времени
|
||||
lessonTimeSelect.innerHTML = '<option value="">Сначала выберите день</option>';
|
||||
lessonTimeSelect.disabled = true;
|
||||
hideAlert('add-lesson-alert');
|
||||
}
|
||||
|
||||
// Открытие модалки с установкой userId
|
||||
function openAddLessonModal(userId) {
|
||||
lessonUserId.value = userId;
|
||||
// NEW: сбрасываем выбранный день и время
|
||||
lessonDaySelect.value = '';
|
||||
updateTimeOptions('');
|
||||
modalAddLesson.classList.add('open');
|
||||
}
|
||||
|
||||
// Обработчик отправки формы добавления занятия
|
||||
addLessonForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('add-lesson-alert');
|
||||
|
||||
const userId = lessonUserId.value;
|
||||
const groupId = lessonGroupSelect.value;
|
||||
const subjectId = lessonDisciplineSelect.value;
|
||||
const dayOfWeek = lessonDaySelect.value;
|
||||
const timeSlot = lessonTimeSelect.value; // NEW: получаем выбранное время
|
||||
|
||||
// Проверка обязательных полей
|
||||
if (!groupId) {
|
||||
showAlert('add-lesson-alert', 'Выберите группу', 'error');
|
||||
return;
|
||||
}
|
||||
if (!subjectId) {
|
||||
showAlert('add-lesson-alert', 'Выберите дисциплину', 'error');
|
||||
return;
|
||||
}
|
||||
if (!dayOfWeek) {
|
||||
showAlert('add-lesson-alert', 'Выберите день недели', 'error');
|
||||
return;
|
||||
}
|
||||
// NEW: проверка времени
|
||||
if (!timeSlot) {
|
||||
showAlert('add-lesson-alert', 'Выберите время', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Определяем выбранный тип недели
|
||||
const weekUpperChecked = weekUpper?.checked || false;
|
||||
const weekLowerChecked = weekLower?.checked || false;
|
||||
let weekType = null;
|
||||
if (weekUpperChecked && weekLowerChecked) {
|
||||
weekType = 'Обе';
|
||||
} else if (weekUpperChecked) {
|
||||
weekType = 'Верхняя';
|
||||
} else if (weekLowerChecked) {
|
||||
weekType = 'Нижняя';
|
||||
}
|
||||
|
||||
try {
|
||||
// Отправляем данные на сервер
|
||||
const response = await api.post('/api/users/lessons/create', {
|
||||
teacherId: parseInt(userId),
|
||||
groupId: parseInt(groupId),
|
||||
subjectId: parseInt(subjectId),
|
||||
day: dayOfWeek,
|
||||
week: weekType,
|
||||
time: timeSlot // передаём время
|
||||
});
|
||||
showAlert('add-lesson-alert', 'Занятие добавлено', 'success');
|
||||
setTimeout(() => {
|
||||
modalAddLesson.classList.remove('open');
|
||||
resetLessonForm();
|
||||
}, 1500);
|
||||
} catch (e) {
|
||||
showAlert('add-lesson-alert', e.message || 'Ошибка добавления занятия', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
createForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-alert');
|
||||
const username = document.getElementById('new-username').value.trim();
|
||||
const password = document.getElementById('new-password').value;
|
||||
const role = document.getElementById('new-role').value;
|
||||
if (!username || !password) { showAlert('create-alert', 'Заполните все поля', 'error'); return; }
|
||||
|
||||
try {
|
||||
const data = await api.post('/api/users', { username, password, role });
|
||||
showAlert('create-alert', `Пользователь "${escapeHtml(data.username)}" создан`, 'success');
|
||||
createForm.reset();
|
||||
loadUsers();
|
||||
} catch (e) {
|
||||
showAlert('create-alert', e.message || 'Ошибка соединения', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Обработчик кликов по таблице
|
||||
usersTbody.addEventListener('click', async (e) => {
|
||||
const deleteBtn = e.target.closest('.btn-delete');
|
||||
if (deleteBtn) {
|
||||
if (!confirm('Удалить пользователя?')) return;
|
||||
try {
|
||||
await api.delete('/api/users/' + deleteBtn.dataset.id);
|
||||
loadUsers();
|
||||
} catch (e) {
|
||||
alert(e.message || 'Ошибка удаления');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const addLessonBtn = e.target.closest('.btn-add-lesson');
|
||||
if (addLessonBtn) {
|
||||
e.preventDefault();
|
||||
if (modalAddLesson) {
|
||||
openAddLessonModal(addLessonBtn.dataset.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// NEW: обработчик изменения дня недели для обновления списка времени
|
||||
lessonDaySelect.addEventListener('change', function() {
|
||||
updateTimeOptions(this.value);
|
||||
});
|
||||
|
||||
// Закрытие модалки по крестику
|
||||
if (modalAddLessonClose) {
|
||||
modalAddLessonClose.addEventListener('click', () => {
|
||||
modalAddLesson.classList.remove('open');
|
||||
resetLessonForm();
|
||||
});
|
||||
}
|
||||
|
||||
// Закрытие по клику на overlay
|
||||
if (modalAddLesson) {
|
||||
modalAddLesson.addEventListener('click', (e) => {
|
||||
if (e.target === modalAddLesson) {
|
||||
modalAddLesson.classList.remove('open');
|
||||
resetLessonForm();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Загружаем все данные при инициализации
|
||||
await Promise.all([loadUsers(), loadGroups(), loadSubjects()]);
|
||||
}
|
||||
109
frontend/admin/views/classrooms.html
Executable file
109
frontend/admin/views/classrooms.html
Executable file
@@ -0,0 +1,109 @@
|
||||
<!-- ===== Classrooms Tab ===== -->
|
||||
<div class="card create-card">
|
||||
<h2>Новая аудитория</h2>
|
||||
<form id="create-classroom-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-classroom-name">Номер / Название</label>
|
||||
<input type="text" id="new-classroom-name" placeholder="101 Ленинская" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-classroom-capacity">Вместимость (чел.)</label>
|
||||
<input type="number" id="new-classroom-capacity" placeholder="30" min="1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top: 1rem;">
|
||||
<div class="form-group" style="flex: 2;">
|
||||
<label>Оборудование</label>
|
||||
<div class="custom-multi-select">
|
||||
<div class="select-box" id="equipment-select-box">
|
||||
<span class="select-text" id="equipment-select-text">Выберите оборудование...</span>
|
||||
<svg class="dropdown-icon" width="12" height="8" viewBox="0 0 12 8" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="#9ca3af" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="equipment-dropdown-menu">
|
||||
<div id="equipment-checkboxes" class="checkbox-group-vertical">
|
||||
<!-- Подгружается через JS -->
|
||||
<p class="text-muted"><small>Загрузка...</small></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: flex-end;">
|
||||
<button type="submit" class="btn-primary">Добавить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-alert" id="create-classroom-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Список аудиторий</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="classrooms-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Вместимость</th>
|
||||
<th>Оборудование</th>
|
||||
<th>Статус</th>
|
||||
<th style="text-align: right;">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="classrooms-tbody">
|
||||
<tr>
|
||||
<td colspan="6" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Classroom Modal -->
|
||||
<div class="modal-overlay" id="modal-edit-classroom">
|
||||
<div class="modal-content card">
|
||||
<h2>Редактировать аудиторию</h2>
|
||||
<button class="modal-close" id="modal-edit-classroom-close">×</button>
|
||||
<form id="edit-classroom-form">
|
||||
<input type="hidden" id="edit-classroom-id">
|
||||
<div class="form-row" style="margin-top: 1rem;">
|
||||
<div class="form-group">
|
||||
<label for="edit-classroom-name">Номер / Название</label>
|
||||
<input type="text" id="edit-classroom-name" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="edit-classroom-capacity">Вместимость (чел.)</label>
|
||||
<input type="number" id="edit-classroom-capacity" min="1" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row" style="margin-top: 1rem;">
|
||||
<div class="form-group" style="flex: 2;">
|
||||
<label>Оборудование</label>
|
||||
<div class="custom-multi-select">
|
||||
<div class="select-box" id="edit-equipment-select-box">
|
||||
<span class="select-text" id="edit-equipment-select-text">Выберите оборудование...</span>
|
||||
<svg class="dropdown-icon" width="12" height="8" viewBox="0 0 12 8" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="#9ca3af" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dropdown-menu" id="edit-equipment-dropdown-menu">
|
||||
<div id="edit-equipment-checkboxes" class="checkbox-group-vertical">
|
||||
<!-- Подгружается через JS -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" style="display: flex; align-items: flex-end;">
|
||||
<button type="submit" class="btn-primary" style="width: 100%;">Сохранить</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-alert" id="edit-classroom-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
34
frontend/admin/views/edu-forms.html
Executable file
34
frontend/admin/views/edu-forms.html
Executable file
@@ -0,0 +1,34 @@
|
||||
<!-- ===== Education Forms Tab ===== -->
|
||||
<div class="card create-card">
|
||||
<h2>Новая форма обучения</h2>
|
||||
<form id="create-ef-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-ef-name">Название</label>
|
||||
<input type="text" id="new-ef-name" placeholder="Бакалавриат" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-ef-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Все формы обучения</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="ef-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="ef-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
34
frontend/admin/views/equipments.html
Executable file
34
frontend/admin/views/equipments.html
Executable file
@@ -0,0 +1,34 @@
|
||||
<!-- ===== Equipments Tab ===== -->
|
||||
<div class="card create-card">
|
||||
<h2>Новое оборудование</h2>
|
||||
<form id="create-equipment-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-equipment-name">Название оборудования</label>
|
||||
<input type="text" id="new-equipment-name" placeholder="Проектор" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Добавить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-equipment-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Справочник оборудования</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="equipments-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="equipments-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
49
frontend/admin/views/groups.html
Executable file
49
frontend/admin/views/groups.html
Executable file
@@ -0,0 +1,49 @@
|
||||
<!-- ===== Groups Tab ===== -->
|
||||
<div class="card create-card">
|
||||
<h2>Новая группа</h2>
|
||||
<form id="create-group-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-group-name">Название группы</label>
|
||||
<input type="text" id="new-group-name" placeholder="ИВТ-21-1" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-group-ef">Форма обучения</label>
|
||||
<select id="new-group-ef">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-group-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Все группы</h2>
|
||||
<div class="filter-row">
|
||||
<label for="filter-ef">Фильтр:</label>
|
||||
<select id="filter-ef">
|
||||
<option value="">Все формы</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="groups-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Форма обучения</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="groups-tbody">
|
||||
<tr>
|
||||
<td colspan="4" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
43
frontend/admin/views/schedule.html
Executable file
43
frontend/admin/views/schedule.html
Executable file
@@ -0,0 +1,43 @@
|
||||
<div class="card">
|
||||
<h2>Расписание занятий</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="schedule-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="sortable" data-sort-key="id">ID <span class="sort-arrow"></span></th>
|
||||
<th class="filterable" data-filter-key="teacher">
|
||||
Преподаватель <span class="filter-icon">▾</span>
|
||||
</th>
|
||||
<th class="filterable" data-filter-key="group">
|
||||
Группа <span class="filter-icon">▾</span>
|
||||
</th>
|
||||
<th class="filterable" data-filter-key="classroomName">
|
||||
Аудитория <span class="filter-icon">▾</span>
|
||||
</th>
|
||||
<th class="filterable" data-filter-key="educationForm">
|
||||
Форма обучения <span class="filter-icon">▾</span>
|
||||
</th>
|
||||
<th class="filterable" data-filter-key="subject">
|
||||
Дисциплина <span class="filter-icon">▾</span>
|
||||
</th>
|
||||
<th class="filterable" data-filter-key="lessonFormat">
|
||||
Формат занятия <span class="filter-icon">▾</span>
|
||||
</th>
|
||||
<th class="filterable" data-filter-key="typeLesson">
|
||||
Тип занятия <span class="filter-icon">▾</span>
|
||||
</th>
|
||||
<th class="filterable" data-filter-key="day">
|
||||
День недели <span class="filter-icon">▾</span>
|
||||
</th>
|
||||
<th>Неделя</th>
|
||||
<th>Время</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="schedule-tbody">
|
||||
<tr>
|
||||
<td colspan="8" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
76
frontend/admin/views/subjects.html
Executable file
76
frontend/admin/views/subjects.html
Executable file
@@ -0,0 +1,76 @@
|
||||
<!-- ===== Subjects Tab ===== -->
|
||||
<div class="card create-card">
|
||||
<h2>Новая дисциплина</h2>
|
||||
<form id="create-subject-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-subject-name">Название дисциплины</label>
|
||||
<input type="text" id="new-subject-name" placeholder="Высшая математика" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Добавить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-subject-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card create-card">
|
||||
<h2>Привязка преподавателя к дисциплине</h2>
|
||||
<form id="assign-teacher-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="assign-teacher-select">Преподаватель</label>
|
||||
<select id="assign-teacher-select">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="assign-subject-select">Дисциплина</label>
|
||||
<select id="assign-subject-select">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Привязать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="assign-teacher-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Все дисциплины</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="subjects-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="subjects-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Привязки преподавателей</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="teacher-subjects-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Преподаватель</th>
|
||||
<th>Дисциплина</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="teacher-subjects-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
110
frontend/admin/views/users.html
Executable file
110
frontend/admin/views/users.html
Executable file
@@ -0,0 +1,110 @@
|
||||
<!-- ===== Users Tab ===== -->
|
||||
<div class="card create-card">
|
||||
<h2>Новый пользователь</h2>
|
||||
<form id="create-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-username">Имя пользователя</label>
|
||||
<input type="text" id="new-username" placeholder="username" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-password">Пароль</label>
|
||||
<input type="text" id="new-password" placeholder="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="new-role">Роль</label>
|
||||
<select id="new-role">
|
||||
<option value="STUDENT">Студент</option>
|
||||
<option value="TEACHER">Преподаватель</option>
|
||||
<option value="ADMIN">Администратор</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Все пользователи</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="users-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Имя пользователя</th>
|
||||
<th>Роль</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="users-tbody">
|
||||
<tr>
|
||||
<td colspan="4" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Lesson Modal -->
|
||||
<div class="modal-overlay" id="modal-add-lesson">
|
||||
<div class="modal-content card">
|
||||
<h2>Добавить занятие</h2>
|
||||
<button class="modal-close" id="modal-add-lesson-close">×</button>
|
||||
<form id="add-lesson-form">
|
||||
<input type="hidden" id="lesson-user-id">
|
||||
|
||||
<div class="form-group" style="margin-top: 1rem;">
|
||||
<label for="lesson-group">Группа</label>
|
||||
<select id="lesson-group" required>
|
||||
<option value="">Выберите группу</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 1rem;">
|
||||
<label for="lesson-discipline">Дисциплина</label>
|
||||
<select id="lesson-discipline" required>
|
||||
<option value="">Выберите дисциплину</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-row" style="margin-top: 1rem;">
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label for="lesson-day">День недели</label>
|
||||
<select id="lesson-day" required>
|
||||
<option value="">Выберите день</option>
|
||||
<option value="Понедельник">Понедельник</option>
|
||||
<option value="Вторник">Вторник</option>
|
||||
<option value="Среда">Среда</option>
|
||||
<option value="Четверг">Четверг</option>
|
||||
<option value="Пятница">Пятница</option>
|
||||
<option value="Суббота">Суббота</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label>Неделя</label>
|
||||
<div style="display: flex; gap: 0.5rem;">
|
||||
<label class="btn-checkbox">
|
||||
<input type="checkbox" name="weekType" value="Верхняя" id="week-upper">
|
||||
<span class="checkbox-btn">Верхняя</span>
|
||||
</label>
|
||||
<label class="btn-checkbox">
|
||||
<input type="checkbox" name="weekType" value="Нижняя" id="week-lower">
|
||||
<span class="checkbox-btn">Нижняя</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 1rem;">
|
||||
<label for="lesson-time">Время занятия</label>
|
||||
<select id="lesson-time" required disabled>
|
||||
<option value="">Сначала выберите день</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn-primary" style="width: 100%; margin-top: 1rem;">Сохранить</button>
|
||||
<div class="form-alert" id="add-lesson-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
0
frontend/index.html
Normal file → Executable file
0
frontend/index.html
Normal file → Executable file
0
frontend/script.js
Normal file → Executable file
0
frontend/script.js
Normal file → Executable file
0
frontend/student/index.html
Normal file → Executable file
0
frontend/student/index.html
Normal file → Executable file
0
frontend/style.css
Normal file → Executable file
0
frontend/style.css
Normal file → Executable file
0
frontend/teacher/index.html
Normal file → Executable file
0
frontend/teacher/index.html
Normal file → Executable file
0
frontend/theme-toggle.js
Normal file → Executable file
0
frontend/theme-toggle.js
Normal file → Executable file
Reference in New Issue
Block a user