diff --git a/.gitea/workflows/docker-build.yaml b/.gitea/workflows/docker-build.yaml new file mode 100644 index 0000000..32e7b72 --- /dev/null +++ b/.gitea/workflows/docker-build.yaml @@ -0,0 +1,68 @@ +name: Build and Push Docker Images + +on: + push: + branches: + - main + tags: + - 'v*' + +env: + REGISTRY: gitea.zuev.company # Замените на реальный домен вашего Gitea + BACKEND_IMAGE: zuev/magistr-backend + FRONTEND_IMAGE: zuev/magistr-frontend + +jobs: + build-and-push-backend: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.ZUEV_TOKEN }} # Нужно создать секрет ZUEV_TOKEN в настройках репозитория (Personal Access Token) + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: ./backend + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + build-and-push-frontend: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.ZUEV_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE }} + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + context: ./frontend + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitignore b/.gitignore index 95e33b9..6b4c2cd 100755 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,15 @@ -# Игнорируем данные БД (но не init-скрипты) db/data/ - -# Игнорируем секреты .env -!GEMINI.md -!AGENTS.md -# Игнорируем системные папки IDE (если редактируете с ПК) -.idea/ -.vscode/ -*.DS_Store -.agent/ # Игнорируем временные файлы сборки (на будущее) backend/target/ backend/build/ frontend/node_modules/ frontend/dist/ + +.agents +.idea/ +.vscode/ +*.DS_Store +AGENTS.md +GEMINI.md \ No newline at end of file diff --git a/backend/Dockerfile b/backend/Dockerfile old mode 100644 new mode 100755 diff --git a/backend/pom.xml b/backend/pom.xml old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/Application.java b/backend/src/main/java/com/magistr/app/Application.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/README.md b/backend/src/main/java/com/magistr/app/README.md old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/config/AppConfig.java b/backend/src/main/java/com/magistr/app/config/AppConfig.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/config/DataInitializer.java b/backend/src/main/java/com/magistr/app/config/DataInitializer.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/controller/AuthController.java b/backend/src/main/java/com/magistr/app/controller/AuthController.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/controller/ClassroomController.java b/backend/src/main/java/com/magistr/app/controller/ClassroomController.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/controller/EducationFormController.java b/backend/src/main/java/com/magistr/app/controller/EducationFormController.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/controller/EquipmentController.java b/backend/src/main/java/com/magistr/app/controller/EquipmentController.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/controller/GroupController.java b/backend/src/main/java/com/magistr/app/controller/GroupController.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/controller/LessonsController.java b/backend/src/main/java/com/magistr/app/controller/LessonsController.java new file mode 100755 index 0000000..f124e97 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/controller/LessonsController.java @@ -0,0 +1,240 @@ +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 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; + + public LessonsController(LessonRepository lessonRepository, UserRepository teacherRepository, GroupRepository groupRepository, SubjectRepository subjectRepository, EducationFormRepository educationForm) { + this.lessonRepository = lessonRepository; + this.teacherRepository = teacherRepository; + this.groupRepository = groupRepository; + this.subjectRepository = subjectRepository; + this.educationFormRepository = educationForm; + } + + @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)); + } + + //Проверка lessonTypeId + if (request.getSubjectId() == null || request.getSubjectId() == 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.setDay(request.getDay()); + lesson.setWeek(request.getWeek()); + lesson.setTime(request.getTime()); + + Lesson savedLesson = lessonRepository.save(lesson); + + Map 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("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 getAllLessons() { + logger.info("Запрос на получение всех занятий"); + + try { + List lessons = lessonRepository.findAll(); + + List 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("Неизвестно"); + + return new LessonResponse( + lesson.getId(), + teacherName, + groupName, + educationFormName, + subjectName, + 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 lessons = lessonRepository.findByTeacherId(teacherId); + + if(lessons.isEmpty()) { + logger.info("У преподавателя с ID {} нет занятий", teacherId); + return ResponseEntity.ok(Map.of( + "message", "У преподавателя с ID " + teacherId +" нет занятий.", + "lessons", Collections.emptyList() + )); + } + + List 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 result = new HashMap<>(); + + // Через JPA репозиторий + List 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 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; + } +} \ No newline at end of file diff --git a/backend/src/main/java/com/magistr/app/controller/SubjectController.java b/backend/src/main/java/com/magistr/app/controller/SubjectController.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/controller/TeacherSubjectController.java b/backend/src/main/java/com/magistr/app/controller/TeacherSubjectController.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/controller/TestController.java b/backend/src/main/java/com/magistr/app/controller/TestController.java deleted file mode 100644 index 1f7bde5..0000000 --- a/backend/src/main/java/com/magistr/app/controller/TestController.java +++ /dev/null @@ -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.getLessonTypeId() == null || request.getLessonTypeId() == 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.setLessonTypeId(request.getLessonTypeId()); - 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 getAllLessons() { - return lessonRepository.findAll().stream() - .map(l -> new LessonResponse(l.getId(), l.getTeacherId(), l.getLessonTypeId(), l.getDay(), l.getWeek(), l.getTime())) - .toList(); - } - - @GetMapping("/ping") - public String ping() { - return "pong"; - } -} \ No newline at end of file diff --git a/backend/src/main/java/com/magistr/app/controller/UserController.java b/backend/src/main/java/com/magistr/app/controller/UserController.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/dto/ClassroomRequest.java b/backend/src/main/java/com/magistr/app/dto/ClassroomRequest.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/dto/ClassroomResponse.java b/backend/src/main/java/com/magistr/app/dto/ClassroomResponse.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/dto/CreateGroupRequest.java b/backend/src/main/java/com/magistr/app/dto/CreateGroupRequest.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/dto/CreateLessonRequest.java b/backend/src/main/java/com/magistr/app/dto/CreateLessonRequest.java old mode 100644 new mode 100755 index 8affb79..bb9a3ed --- a/backend/src/main/java/com/magistr/app/dto/CreateLessonRequest.java +++ b/backend/src/main/java/com/magistr/app/dto/CreateLessonRequest.java @@ -4,7 +4,7 @@ public class CreateLessonRequest { private Long teacherId; private Long groupId; - private Long lessonTypeId; + private Long subjectId; private String day; private String week; private String time; @@ -28,12 +28,12 @@ public class CreateLessonRequest { this.groupId = groupId; } - public Long getLessonTypeId() { - return lessonTypeId; + public Long getSubjectId() { + return subjectId; } - public void setLessonTypeId(Long lessonTypeId) { - this.lessonTypeId= lessonTypeId; + public void setSubjectId(Long subjectId) { + this.subjectId = subjectId; } public String getDay() { diff --git a/backend/src/main/java/com/magistr/app/dto/CreateUserRequest.java b/backend/src/main/java/com/magistr/app/dto/CreateUserRequest.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/dto/GroupResponse.java b/backend/src/main/java/com/magistr/app/dto/GroupResponse.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/dto/LessonResponse.java b/backend/src/main/java/com/magistr/app/dto/LessonResponse.java old mode 100644 new mode 100755 index 873de27..b718edc --- a/backend/src/main/java/com/magistr/app/dto/LessonResponse.java +++ b/backend/src/main/java/com/magistr/app/dto/LessonResponse.java @@ -1,11 +1,19 @@ 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 lessonTypeId; + private String groupName; + private String educationFormName; + private Long subjectId; + private String subjectName; private String day; private String week; private String time; @@ -13,17 +21,22 @@ public class LessonResponse { public LessonResponse() { } - public LessonResponse(Long lessonTypeId, String day, String week, String time) { - this.lessonTypeId = lessonTypeId; + 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 lessonTypeId, String day, String week, String time) { + public LessonResponse(Long id, String teacherName, String groupName, String educationFormName, String subjectName, String day, String week, String time) { this.id = id; - this.teacherId = teacherId; - this.lessonTypeId = lessonTypeId; + this.teacherName = teacherName; + this.groupName = groupName; + this.educationFormName = educationFormName; + this.subjectName = subjectName; this.day = day; this.week = week; this.time = time; @@ -45,6 +58,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 +74,36 @@ public class LessonResponse { this.groupId = groupId; } - public Long getLessonTypeId() { - return lessonTypeId; + public String getGroupName() { + return groupName; } - public void setLessonTypeId(Long lessonTypeId) { - this.lessonTypeId = lessonTypeId; + public void setGroupName(String groupName) { + this.groupName = groupName; + } + + 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() { diff --git a/backend/src/main/java/com/magistr/app/dto/LoginRequest.java b/backend/src/main/java/com/magistr/app/dto/LoginRequest.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/dto/LoginResponse.java b/backend/src/main/java/com/magistr/app/dto/LoginResponse.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/dto/TeacherSubjectResponse.java b/backend/src/main/java/com/magistr/app/dto/TeacherSubjectResponse.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/dto/UserResponse.java b/backend/src/main/java/com/magistr/app/dto/UserResponse.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/model/Classroom.java b/backend/src/main/java/com/magistr/app/model/Classroom.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/model/EducationForm.java b/backend/src/main/java/com/magistr/app/model/EducationForm.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/model/Equipment.java b/backend/src/main/java/com/magistr/app/model/Equipment.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/model/Lesson.java b/backend/src/main/java/com/magistr/app/model/Lesson.java old mode 100644 new mode 100755 index b5bd559..5d89202 --- a/backend/src/main/java/com/magistr/app/model/Lesson.java +++ b/backend/src/main/java/com/magistr/app/model/Lesson.java @@ -16,8 +16,8 @@ public class Lesson { @Column(name = "group_id", nullable = false) private Long groupId; - @Column(name = "lesson_type_id", nullable = false) - private Long lessonTypeId; + @Column(name = "subject_id", nullable = false) + private Long subjectId; @Column(name = "day", nullable = false, length = 255) private String day; @@ -55,12 +55,12 @@ public class Lesson { this.groupId = groupId; } - public Long getLessonTypeId() { - return lessonTypeId; + public Long getSubjectId() { + return subjectId; } - public void setLessonTypeId(Long lessonTypeId) { - this.lessonTypeId = lessonTypeId; + public void setSubjectId(Long subjectId) { + this.subjectId = subjectId; } public String getDay() { diff --git a/backend/src/main/java/com/magistr/app/model/Role.java b/backend/src/main/java/com/magistr/app/model/Role.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/model/StudentGroup.java b/backend/src/main/java/com/magistr/app/model/StudentGroup.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/model/Subject.java b/backend/src/main/java/com/magistr/app/model/Subject.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/model/TeacherSubject.java b/backend/src/main/java/com/magistr/app/model/TeacherSubject.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/model/TeacherSubjectId.java b/backend/src/main/java/com/magistr/app/model/TeacherSubjectId.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/model/User.java b/backend/src/main/java/com/magistr/app/model/User.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/repository/ClassroomRepository.java b/backend/src/main/java/com/magistr/app/repository/ClassroomRepository.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/repository/EducationFormRepository.java b/backend/src/main/java/com/magistr/app/repository/EducationFormRepository.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/repository/EquipmentRepository.java b/backend/src/main/java/com/magistr/app/repository/EquipmentRepository.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/repository/GroupRepository.java b/backend/src/main/java/com/magistr/app/repository/GroupRepository.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/repository/LessonRepository.java b/backend/src/main/java/com/magistr/app/repository/LessonRepository.java old mode 100644 new mode 100755 index 3af1d8f..f02e9b5 --- a/backend/src/main/java/com/magistr/app/repository/LessonRepository.java +++ b/backend/src/main/java/com/magistr/app/repository/LessonRepository.java @@ -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 { - Optional findByLessonTypeId(Long lessonTypeId); + Optional findBySubjectId(Long subjectId); + + List findByTeacherId(Long teacherId); } diff --git a/backend/src/main/java/com/magistr/app/repository/SubjectRepository.java b/backend/src/main/java/com/magistr/app/repository/SubjectRepository.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/repository/TeacherSubjectRepository.java b/backend/src/main/java/com/magistr/app/repository/TeacherSubjectRepository.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/repository/UserRepository.java b/backend/src/main/java/com/magistr/app/repository/UserRepository.java old mode 100644 new mode 100755 diff --git a/backend/src/main/java/com/magistr/app/utils/DayAndWeekValidator.java b/backend/src/main/java/com/magistr/app/utils/DayAndWeekValidator.java new file mode 100755 index 0000000..b4461f9 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/utils/DayAndWeekValidator.java @@ -0,0 +1,30 @@ +package com.magistr.app.utils; + +import java.util.Set; + +public class DayAndWeekValidator { + + private static final Set VALID_DAYS = Set.of( + "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота" + ); + + private static final Set 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); + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties old mode 100644 new mode 100755 index 35a3635..05625aa --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -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 diff --git a/compose.yaml b/compose.yaml old mode 100644 new mode 100755 diff --git a/db/init/init.sql b/db/init/init.sql old mode 100644 new mode 100755 index 87f4c46..eb434fb --- a/db/init/init.sql +++ b/db/init/init.sql @@ -185,7 +185,7 @@ 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), - lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id), + subject_id BIGINT NOT NULL REFERENCES subjects(id), day VARCHAR(255) NOT NULL, week VARCHAR(255) NOT NULL, time VARCHAR(255) NOT NULL diff --git a/frontend/.dockerignore b/frontend/.dockerignore old mode 100644 new mode 100755 diff --git a/frontend/Dockerfile b/frontend/Dockerfile old mode 100644 new mode 100755 diff --git a/frontend/admin/admin.js b/frontend/admin/admin.js deleted file mode 100644 index 9d0c01b..0000000 --- a/frontend/admin/admin.js +++ /dev/null @@ -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 = 'Ошибка загрузки'; - } - } - - function renderUsers(users) { - if (!users.length) { - usersTbody.innerHTML = 'Нет пользователей'; - return; - } - usersTbody.innerHTML = users.map(u => ` - - ${u.id} - ${escapeHtml(u.username)} - ${ROLE_LABELS[u.role] || u.role} - - - `).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 = 'Ошибка загрузки'; - } - } - - function renderEfTable(forms) { - if (!forms.length) { - efTbody.innerHTML = 'Нет форм обучения'; - return; - } - efTbody.innerHTML = forms.map(ef => ` - - ${ef.id} - ${escapeHtml(ef.name)} - - `).join(''); - } - - function populateEfSelects(forms) { - // Group creation select - const currentVal = newGroupEfSelect.value; - newGroupEfSelect.innerHTML = forms.map(ef => - `` - ).join(''); - if (currentVal && forms.find(f => f.id == currentVal)) { - newGroupEfSelect.value = currentVal; - } - - // Filter select - const currentFilter = filterEfSelect.value; - filterEfSelect.innerHTML = '' + - forms.map(ef => - `` - ).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 = 'Ошибка загрузки'; - } - } - - 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 = 'Нет групп'; - return; - } - groupsTbody.innerHTML = groups.map(g => ` - - ${g.id} - ${escapeHtml(g.name)} - ${escapeHtml(g.educationFormName)} - - `).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 = 'Ошибка загрузки'; - if (equipmentCheckboxes) equipmentCheckboxes.innerHTML = '

Ошибка загрузки

'; - } - } - - function renderEquipments(equipments) { - if (!equipments.length) { - equipmentsTbody.innerHTML = 'Нет оборудования'; - return; - } - equipmentsTbody.innerHTML = equipments.map(eq => ` - - ${eq.id} - ${escapeHtml(eq.name)} - - `).join(''); - } - - function renderEquipmentCheckboxes(equipments) { - if (!equipments.length) { - equipmentCheckboxes.innerHTML = '

Нет доступного оборудования

'; - return; - } - equipmentCheckboxes.innerHTML = equipments.map(eq => ` - - `).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 = 'Ошибка загрузки'; - } - } - - function renderClassrooms(classrooms) { - if (!classrooms.length) { - classroomsTbody.innerHTML = 'Нет аудиторий'; - return; - } - classroomsTbody.innerHTML = classrooms.map(c => { - const equipHtml = c.equipments && c.equipments.length - ? c.equipments.map(eq => escapeHtml(eq.name)).join(', ') - : '—'; - - return ` - - ${c.id} - ${escapeHtml(c.name)} - ${c.capacity} чел. - ${equipHtml} - -
- - ${c.isAvailable ? 'Доступна' : 'Не доступна'} - - -
- - - - - - `; - }).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 ` - - `; - }).join(''); - } else { - editEquipmentCheckboxes.innerHTML = '

Нет доступного оборудования

'; - } - 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 = 'Ошибка загрузки'; - } - } - - function renderSubjects(subjects) { - if (!subjects.length) { - subjectsTbody.innerHTML = 'Нет дисциплин'; - return; - } - subjectsTbody.innerHTML = subjects.map(s => ` - - ${s.id} - ${escapeHtml(s.name)} - - `).join(''); - } - - function populateSubjectSelect(subjects) { - if (!assignSubjectSelect) return; - const currentVal = assignSubjectSelect.value; - assignSubjectSelect.innerHTML = '' + - subjects.map(s => ``).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 = ''; - } - } - - function populateTeacherSelect(teachers) { - if (!assignTeacherSelect) return; - const currentVal = assignTeacherSelect.value; - if (!teachers.length) { - assignTeacherSelect.innerHTML = ''; - return; - } - assignTeacherSelect.innerHTML = '' + - teachers.map(t => ``).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 = 'Ошибка загрузки'; - } - } - - function renderTeacherSubjects(tsArray) { - if (!tsArray.length) { - teacherSubjectsTbody.innerHTML = 'Нет привязок'; - return; - } - teacherSubjectsTbody.innerHTML = tsArray.map(ts => ` - - ${escapeHtml(ts.username)} - ${escapeHtml(ts.subjectName)} - - `).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(); -})(); diff --git a/frontend/admin/admin.css b/frontend/admin/css/components.css old mode 100644 new mode 100755 similarity index 52% rename from frontend/admin/admin.css rename to frontend/admin/css/components.css index f178ab8..e579611 --- a/frontend/admin/admin.css +++ b/frontend/admin/css/components.css @@ -1,338 +1,4 @@ -/* ===== 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.7); - --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; -} - -[data-theme="light"] .form-group select option, -[data-theme="light"] .filter-row select option { - background: #fff; - color: #1a1a2e; -} - -[data-theme="light"] .nav-item.active { - background: rgba(99, 102, 241, 0.18); -} - -[data-theme="light"] .custom-multi-select .dropdown-menu { - background: rgba(255, 255, 255, 0.98); -} - -[data-theme="light"] .form-group input, -[data-theme="light"] .form-group select, -[data-theme="light"] .filter-row select { - border-color: rgba(0, 0, 0, 0.15); -} - -[data-theme="light"] tbody td { - border-bottom-color: rgba(0, 0, 0, 0.08); -} - -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; -} - -/* ===== 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); -} - -.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); -} - -/* Checkbox list styling */ -.checkbox-group { - display: flex; - flex-wrap: wrap; - gap: 12px; - padding: 8px 0; -} - -.checkbox-item { - display: flex; - align-items: center; - gap: 6px; - font-size: 0.9rem; - color: var(--text-base); - cursor: pointer; -} - -.checkbox-item input[type="checkbox"] { - cursor: pointer; - width: 16px; - height: 16px; - accent-color: var(--primary-color); -} - -/* Classroom Status Badges */ -.badge-available { - background-color: var(--success-bg); - color: var(--success-color); -} - -.badge-unavailable { - background-color: var(--error-bg); - color: var(--error-color); -} - -.status-cell { - display: flex; - align-items: center; - gap: 8px; -} - -.btn-icon-toggle { - background: var(--bg-body); - border: 1px solid var(--border-color); - color: var(--text-muted); - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 6px; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - padding: 0; -} - -.btn-icon-toggle:hover { - background: var(--card-bg); - border-color: var(--primary-color); - color: var(--primary-color); - transform: rotate(45deg); - box-shadow: 0 0 10px rgba(99, 102, 241, 0.2); -} - -.btn-icon-toggle svg { - display: block; -} - -.nav-item.active { - background: rgba(99, 102, 241, 0.12); - color: var(--accent-hover); -} - -.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); -} - -.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; -} - /* ===== Cards ===== */ -@keyframes slideUpCard { - from { - opacity: 0; - transform: translateY(15px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - .card { background: var(--bg-card); backdrop-filter: blur(20px); @@ -369,20 +35,6 @@ body { opacity: 1; } -/* Staggered cards */ -.card:nth-child(1) { - animation-delay: 0.1s; -} - -.card:nth-child(2) { - animation-delay: 0.2s; -} - -.card:nth-child(3) { - animation-delay: 0.3s; -} - -/* Specific Cards */ .create-card { z-index: 10; } @@ -396,7 +48,7 @@ body { letter-spacing: 0.04em; } -/* ===== Create Form ===== */ +/* ===== Form Structure ===== */ .form-row { display: flex; gap: 1rem; @@ -404,7 +56,7 @@ body { flex-wrap: wrap; } -.form-row .form-group { +.form-group { flex: 1; min-width: 160px; } @@ -459,9 +111,12 @@ input[type="number"]::-webkit-inner-spin-button { input[type="number"] { -moz-appearance: textfield; + appearance: textfield; } -.form-group select { +/* Select Base Style */ +.form-group select, +.filter-row select { cursor: pointer; appearance: none; background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); @@ -470,12 +125,166 @@ input[type="number"] { padding-right: 2.25rem; } -.form-group select option { +.form-group select option, +.filter-row select option { background: #1a1a2e; color: var(--text-primary); } -.btn-create { +/* Light theme selects */ +[data-theme="light"] .form-group input, +[data-theme="light"] .form-group select, +[data-theme="light"] .filter-row select { + border-color: rgba(0, 0, 0, 0.15); +} + +[data-theme="light"] .form-group select option, +[data-theme="light"] .filter-row select option { + background: #fff; + color: #1a1a2e; +} + +/* Filter Row */ +.card-header-row { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 0.75rem; +} + +.card-header-row h2 { + margin-bottom: 0; +} + +.filter-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.filter-row label { + font-size: 0.78rem; + font-weight: 500; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.04em; + white-space: nowrap; +} + +.filter-row select { + padding: 0.45rem 2rem 0.45rem 0.7rem; + background: var(--bg-input); + border: 1px solid transparent; + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 0.85rem; + transition: background var(--transition), border-color var(--transition), box-shadow var(--transition); +} + +.filter-row select:focus { + background-color: var(--bg-input-focus); + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} + + +/* ===== Custom Multi-Select ===== */ +.custom-multi-select { + position: relative; + user-select: none; + width: 100%; +} + +.select-box { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0.75rem 1rem; + background: var(--bg-input); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); + color: var(--text-primary); + font-size: 0.95rem; + cursor: pointer; + transition: all var(--transition); +} + +.select-box:hover { + background: var(--bg-hover); +} + +.select-box.active { + border-color: var(--accent); + box-shadow: 0 0 0 4px var(--accent-glow); +} + +.dropdown-icon { + transition: transform var(--transition); +} + +.select-box.active .dropdown-icon { + transform: rotate(180deg); +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + width: 100%; + margin-top: 0.5rem; + background: rgba(15, 23, 42, 0.95); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); + box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3); + padding: 1rem; + z-index: 100; + opacity: 0; + visibility: hidden; + transform: translateY(-10px); + transition: all var(--transition); +} + +[data-theme="light"] .custom-multi-select .dropdown-menu { + background: rgba(255, 255, 255, 0.98); +} + +.dropdown-menu.open { + opacity: 1; + visibility: visible; + transform: translateY(0); +} + +.checkbox-group-vertical { + display: flex; + flex-direction: column; + gap: 0.75rem; + max-height: 200px; + overflow-y: auto; +} + +.checkbox-item { + display: flex; + align-items: center; + gap: 0.75rem; + cursor: pointer; + font-size: 0.9rem; + color: var(--text-primary); + padding: 0.25rem 0; +} + +.checkbox-item input[type="checkbox"] { + cursor: pointer; + width: 1.1rem; + height: 1.1rem; + accent-color: var(--accent); +} + +/* ===== Buttons ===== */ +.btn-primary { position: relative; overflow: hidden; padding: 0.75rem 1.75rem; @@ -493,7 +302,7 @@ input[type="number"] { box-shadow: 0 4px 15px var(--accent-glow); } -.btn-create::before { +.btn-primary::before { content: ''; position: absolute; top: 0; @@ -506,32 +315,78 @@ input[type="number"] { transition: opacity var(--transition); } -.btn-create:hover { +.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 8px 25px var(--accent-glow); } -.btn-create:hover::before { +.btn-primary:hover::before { opacity: 1; } -.btn-create:active { +.btn-primary:active { transform: translateY(1px); box-shadow: 0 2px 10px var(--accent-glow); } -@keyframes slideDownAlert { - from { - opacity: 0; - transform: translateY(-10px); - } - - to { - opacity: 1; - transform: translateY(0); - } +.btn-delete { + padding: 0.35rem 0.7rem; + background: rgba(248, 113, 113, 0.1); + border: 1px solid rgba(248, 113, 113, 0.2); + border-radius: var(--radius-sm); + color: var(--error); + font-family: inherit; + font-size: 0.8rem; + cursor: pointer; + transition: background var(--transition), transform var(--transition); + position: relative; + overflow: hidden; } +.btn-delete:hover { + background: rgba(248, 113, 113, 0.2); + transform: scale(1.05); +} + +.btn-icon-toggle { + background: transparent; + border: 1px solid var(--bg-card-border); + color: var(--text-secondary); + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + padding: 0; +} + +.btn-icon-toggle:hover { + background: var(--bg-card); + border-color: var(--accent); + color: var(--accent); + transform: rotate(45deg); + box-shadow: 0 0 10px var(--accent-glow); +} + +.btn-edit-classroom { + 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; + transition: all var(--transition); +} + +.btn-edit-classroom:hover { + background: rgba(99, 102, 241, 0.2); + transform: translateY(-1px); +} + +/* ===== Alerts ===== */ .form-alert { display: none; padding: 0.6rem 1rem; @@ -558,7 +413,7 @@ input[type="number"] { /* ===== Table ===== */ .table-wrap { - overflow-x: visible; + overflow-x: auto; } table { @@ -577,17 +432,113 @@ thead th { border-bottom: 1px solid var(--bg-card-border); } -tbody td { - padding: 0.85rem 1rem; - font-size: 0.95rem; - border-bottom: 1px solid var(--bg-card-border); - transition: background var(--transition); +/* Sortable columns */ +thead th.sortable { + cursor: pointer; + user-select: none; + white-space: nowrap; + transition: color 0.2s ease; } -@keyframes slideInRow { +thead th.sortable:hover { + color: var(--accent); +} + +thead th.sortable.sort-active { + color: var(--accent); +} + +.sort-arrow { + display: inline-block; + width: 0; + height: 0; + margin-left: 5px; + vertical-align: middle; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-bottom: 5px solid currentColor; + opacity: 0.3; + transition: transform 0.25s ease, opacity 0.25s ease; +} + +thead th.sortable:hover .sort-arrow { + opacity: 0.6; +} + +thead th.sortable.sort-asc .sort-arrow { + opacity: 1; + border-bottom: 5px solid var(--accent); + transform: rotate(0deg); +} + +thead th.sortable.sort-desc .sort-arrow { + opacity: 1; + border-bottom: 5px solid var(--accent); + transform: rotate(180deg); +} + +/* ===== Filter Icon & Popup ===== */ +thead th.filterable { + cursor: pointer; + user-select: none; + white-space: nowrap; + transition: color 0.2s ease; +} + +thead th.filterable:hover { + color: var(--accent); +} + +.filter-icon { + display: inline-block; + margin-left: 4px; + font-size: 0.65rem; + opacity: 0.4; + cursor: pointer; + transition: opacity 0.2s ease, color 0.2s ease; + vertical-align: middle; +} + +thead th.filterable:hover .filter-icon { + opacity: 0.8; +} + +thead th.filter-active .filter-icon { + opacity: 1; + color: var(--accent); +} + +thead th.filter-active { + color: var(--accent); +} + +.filter-popup { + position: absolute; + top: 100%; + left: 0; + min-width: 220px; + max-width: 280px; + background: rgba(15, 23, 42, 0.97); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45); + padding: 0.75rem; + z-index: 200; + animation: filterPopupIn 0.2s ease-out both; + text-transform: none; +} + +[data-theme="light"] .filter-popup { + background: rgba(255, 255, 255, 0.98); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15); +} + +@keyframes filterPopupIn { from { opacity: 0; - transform: translateY(10px); + transform: translateY(-6px); } to { @@ -596,16 +547,124 @@ tbody td { } } -@keyframes slideInRow { - from { - opacity: 0; - transform: translateX(-10px); - } +.filter-search { + width: 100%; + padding: 0.5rem 0.7rem; + background: var(--bg-input); + border: 1px solid var(--bg-card-border); + border-radius: 6px; + color: var(--text-primary); + font-family: inherit; + font-size: 0.85rem; + outline: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + margin-bottom: 0.5rem; +} - to { - opacity: 1; - transform: translateX(0); - } +.filter-search::placeholder { + color: var(--text-placeholder); +} + +.filter-search:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +.filter-btn-row { + display: flex; + gap: 0.4rem; + margin-bottom: 0.5rem; +} + +.filter-btn-action { + flex: 1; + padding: 0.3rem 0.5rem; + background: var(--bg-input); + border: 1px solid var(--bg-card-border); + border-radius: 6px; + color: var(--text-secondary); + font-family: inherit; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.filter-btn-action:hover { + background: var(--bg-hover); + color: var(--text-primary); + border-color: var(--accent); +} + +.filter-list { + max-height: 180px; + overflow-y: auto; + margin-bottom: 0.5rem; + scrollbar-width: thin; + scrollbar-color: var(--bg-card-border) transparent; +} + +.filter-list::-webkit-scrollbar { + width: 4px; +} + +.filter-list::-webkit-scrollbar-thumb { + background: var(--bg-card-border); + border-radius: 2px; +} + +.filter-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.35rem 0.3rem; + border-radius: 4px; + cursor: pointer; + font-size: 0.85rem; + color: var(--text-primary); + transition: background 0.15s ease; +} + +.filter-item:hover { + background: var(--bg-hover); +} + +.filter-item input[type="checkbox"] { + cursor: pointer; + width: 1rem; + height: 1rem; + accent-color: var(--accent); + flex-shrink: 0; +} + +.filter-btn-apply { + width: 100%; + padding: 0.5rem; + background: linear-gradient(135deg, var(--accent), var(--accent-secondary)); + border: none; + border-radius: 6px; + color: #fff; + font-family: inherit; + font-size: 0.82rem; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; + box-shadow: 0 3px 10px var(--accent-glow); +} + +.filter-btn-apply:hover { + transform: translateY(-1px); + box-shadow: 0 5px 15px var(--accent-glow); +} + +tbody td { + padding: 0.85rem 1rem; + font-size: 0.95rem; + border-bottom: 1px solid var(--bg-card-border); + transition: background var(--transition); +} + +[data-theme="light"] tbody td { + border-bottom-color: rgba(0, 0, 0, 0.08); } tbody tr { @@ -613,6 +672,7 @@ tbody tr { animation: slideInRow 0.3s ease-out both; } +/* Animation delays */ tbody tr:nth-child(1) { animation-delay: 0.05s; } @@ -647,7 +707,7 @@ tbody tr:hover { padding: 2rem !important; } -/* ===== Role Badges ===== */ +/* ===== Badges ===== */ .badge { display: inline-block; padding: 0.2rem 0.6rem; @@ -673,281 +733,48 @@ tbody tr:hover { color: var(--success); } -/* ===== Education Form Badge ===== */ .badge-ef { background: rgba(99, 102, 241, 0.15); color: var(--accent-hover); } -/* ===== Card Header Row ===== */ -.card-header-row { - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 0.75rem; - margin-bottom: 1rem; +/* Classroom Status */ +.badge-available { + background: rgba(16, 185, 129, 0.15); + color: var(--success); } -.card-header-row h2 { - margin-bottom: 0; -} - -.filter-row { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.filter-row label { - font-size: 0.78rem; - font-weight: 500; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 0.04em; - white-space: nowrap; -} - -.filter-row select { - padding: 0.45rem 2rem 0.45rem 0.7rem; - background: var(--bg-input); - border: 1px solid transparent; - border-radius: var(--radius-sm); - color: var(--text-primary); - font-family: inherit; - font-size: 0.85rem; - outline: none; - cursor: pointer; - appearance: none; - background-image: url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 1.5L6 6.5L11 1.5' stroke='%239ca3af' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.6rem center; - transition: background var(--transition), border-color var(--transition), box-shadow var(--transition); -} - -.filter-row select:focus { - background-color: var(--bg-input-focus); - border-color: var(--accent); - box-shadow: 0 0 0 3px var(--accent-glow); -} - -.filter-row select option { - background: #1a1a2e; - color: var(--text-primary); -} - -/* ===== Tab Content ===== */ -.tab-content { - animation: fadeIn 0.2s ease; -} - -@keyframes fadeIn { - from { - opacity: 0; - transform: translateY(4px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ===== Delete Button ===== */ -.btn-delete { - padding: 0.35rem 0.7rem; - background: rgba(248, 113, 113, 0.1); - border: 1px solid rgba(248, 113, 113, 0.2); - border-radius: var(--radius-sm); +.badge-unavailable { + background: rgba(248, 113, 113, 0.15); color: var(--error); - font-family: inherit; - font-size: 0.8rem; - cursor: pointer; - transition: background var(--transition), transform var(--transition); } -.btn-delete:hover { - background: rgba(248, 113, 113, 0.2); - transform: scale(1.05); -} - -/* ===== Ripple Effect ===== */ -.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; -} - -@keyframes admin-ripple { - to { - transform: scale(4); - opacity: 0; - } -} - -/* ===== 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 ===== */ -.sidebar-overlay { - display: none; - position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); - z-index: 9; - backdrop-filter: blur(2px); -} - -/* ===== Theme Toggle Button ===== */ -.theme-toggle { - width: 40px; - height: 40px; - border: none; - border-radius: 50%; - background: var(--bg-card); - border: 1px solid var(--bg-card-border); - color: var(--text-primary); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - transition: all var(--transition); - z-index: 100; - flex-shrink: 0; -} - -.theme-toggle svg { - width: 20px; - height: 20px; - transition: transform 0.4s ease; -} - -.theme-toggle:hover { - transform: scale(1.1); - box-shadow: 0 4px 16px var(--accent-glow); -} - -.theme-toggle:active { - transform: scale(0.95); -} - -/* ===== Custom Multi Select ===== */ -.custom-multi-select { - position: relative; - width: 100%; -} - -.custom-multi-select .select-box { - width: 100%; - padding: 0.75rem 1rem; - background: var(--bg-input); - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-sm); - color: var(--text-primary); - font-family: inherit; - font-size: 0.95rem; - cursor: pointer; - display: flex; - align-items: center; - justify-content: space-between; - transition: all var(--transition); -} - -.custom-multi-select .select-box.active { - background: var(--bg-input-focus); - border-color: var(--accent); - box-shadow: 0 0 0 4px var(--accent-glow); -} - -.custom-multi-select .dropdown-icon { - transition: transform var(--transition); -} - -.custom-multi-select .select-box.active .dropdown-icon { - transform: rotate(180deg); -} - -.custom-multi-select .dropdown-menu { - position: absolute; - top: calc(100% + 5px); - left: 0; - width: 100%; - background: rgba(15, 15, 26, 0.98); - backdrop-filter: blur(24px); - -webkit-backdrop-filter: blur(24px); - border: 1px solid var(--bg-card-border); - border-radius: var(--radius-md); - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); - padding: 0.75rem; - z-index: 9999; - opacity: 0; - visibility: hidden; - transform: translateY(-10px); - transition: all var(--transition); - max-height: 250px; - overflow-y: auto; -} - -.custom-multi-select .dropdown-menu.open { - opacity: 1; - visibility: visible; - transform: translateY(0); -} - -.checkbox-group-vertical { - display: flex; - flex-direction: column; - gap: 8px; -} - -.checkbox-group-vertical .checkbox-item { - padding: 6px 8px; - border-radius: 6px; - transition: background var(--transition); +.status-cell { display: flex; align-items: center; gap: 8px; - cursor: pointer; } -.checkbox-group-vertical .checkbox-item:hover { - background: var(--bg-hover); -} - -/* ===== Modals ===== */ +/* ===== Modal ===== */ .modal-overlay { display: none; position: fixed; - inset: 0; - background: rgba(0, 0, 0, 0.5); + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(4px); z-index: 1000; - backdrop-filter: blur(2px); align-items: center; justify-content: center; - padding: 1rem; + opacity: 0; + transition: opacity var(--transition); } .modal-overlay.open { display: flex; - animation: fadeIn 0.2s ease; + opacity: 1; } .modal-content { @@ -955,31 +782,27 @@ tbody tr:hover { border: 1px solid var(--bg-card-border); border-radius: var(--radius-md); padding: 2rem; - width: 100%; - max-width: 600px; + width: 90%; + max-width: 500px; position: relative; - box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); + transform: scale(0.95); + transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.5); } -.modal-content h2 { - font-size: 1.2rem; - font-weight: 600; - margin-bottom: 0.5rem; - color: var(--text-primary); - border-bottom: 1px solid var(--bg-card-border); - padding-bottom: 1rem; +.modal-overlay.open .modal-content { + transform: scale(1); } .modal-close { position: absolute; - top: 1.5rem; - right: 1.5rem; + top: 1rem; + right: 1rem; background: none; border: none; - color: var(--text-secondary); font-size: 1.5rem; + color: var(--text-secondary); cursor: pointer; - line-height: 1; transition: color var(--transition); } @@ -987,92 +810,52 @@ tbody tr:hover { color: var(--error); } -/* ===== Responsive ===== */ -@media (max-width: 768px) { - .sidebar { - width: 240px; - transform: translateX(-100%); - transition: transform 0.3s ease; - z-index: 20; - } - - .sidebar.open { - transform: translateX(0); - } - - .sidebar-overlay.open { - display: block; - } - - .menu-toggle { - display: flex; - } - - .main { - margin-left: 0; - } - - .topbar { - padding: 1rem; - display: flex; - align-items: center; - gap: 0.75rem; - } - - .topbar h1 { - font-size: 1.1rem; - } - - .content { - padding: 1rem; - } - - .card { - padding: 1rem; - } - - .form-row { - flex-direction: column; - align-items: stretch; - } - - .form-row .form-group { - min-width: 0; - } - - .btn-create { - width: 100%; - text-align: center; - } - - tbody td { - padding: 0.5rem 0.6rem; - font-size: 0.82rem; - } - - thead th { - padding: 0.5rem 0.6rem; - font-size: 0.72rem; - } - - .table-wrap { - margin: 0 -1rem; - padding: 0 1rem; - } +.btn-add-lesson { + padding: 0.35rem 0.7rem; + background: rgba(16, 185, 129, 0.1); + border: 1px solid rgba(16, 185, 129, 0.2); + border-radius: var(--radius-sm); + color: var(--success); + font-family: inherit; + font-size: 0.8rem; + cursor: pointer; + transition: background var(--transition), transform var(--transition); + position: relative; + overflow: hidden; } -@media (max-width: 480px) { - .topbar h1 { - font-size: 0.95rem; - } +.btn-add-lesson:hover { + background: rgba(16, 185, 129, 0.2); + transform: scale(1.05); +} - .badge { - font-size: 0.65rem; - padding: 0.15rem 0.45rem; - } +/* Кнопки-переключатели для недели */ +.btn-checkbox { + display: inline-block; + cursor: pointer; +} - .btn-delete { - padding: 0.25rem 0.5rem; - font-size: 0.72rem; - } +.btn-checkbox input { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.checkbox-btn { + display: inline-block; + padding: 0.5rem 1rem; + background: var(--bg-secondary); + border: 1px solid var(--bg-card-border); + border-radius: var(--radius-sm); + color: var(--text-primary); + transition: all var(--transition); + user-select: none; +} + +.btn-checkbox input:checked+.checkbox-btn { + background: var(--success, #10b981); + /* используем success или зелёный */ + border-color: var(--success, #10b981); + color: white; } \ No newline at end of file diff --git a/frontend/admin/css/layout.css b/frontend/admin/css/layout.css new file mode 100755 index 0000000..d6386ac --- /dev/null +++ b/frontend/admin/css/layout.css @@ -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; + } +} \ No newline at end of file diff --git a/frontend/admin/css/main.css b/frontend/admin/css/main.css new file mode 100755 index 0000000..a03d06e --- /dev/null +++ b/frontend/admin/css/main.css @@ -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; +} diff --git a/frontend/admin/index.html b/frontend/admin/index.html old mode 100644 new mode 100755 index c8fa44a..860acd3 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -8,7 +8,11 @@ - + + + + + @@ -31,7 +35,7 @@