feat: add subjects & teacher-subjects management tab in admin panel
This commit is contained in:
83
.agent/rules/database_schema.md
Normal file
83
.agent/rules/database_schema.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
description: Полное описание схемы базы данных проекта
|
||||||
|
---
|
||||||
|
|
||||||
|
# Описание базы данных: Система расписания университета
|
||||||
|
|
||||||
|
В этом правиле содержится полное описание структуры базы данных PostgreSQL, используемой в проекте.
|
||||||
|
|
||||||
|
## Основные сущности и таблицы
|
||||||
|
|
||||||
|
### 1. Пользователи и роли (`users`)
|
||||||
|
Хранит учетные записи пользователей системы.
|
||||||
|
- `id` (BIGSERIAL, РК)
|
||||||
|
- `username` (VARCHAR(50), UNIQUE, NOT NULL) - логин
|
||||||
|
- `password` (VARCHAR(255), NOT NULL) - хэш пароля (используется bcrypt через pgcrypto)
|
||||||
|
- `role` (VARCHAR(20), NOT NULL, DEFAULT 'STUDENT') - роль пользователя. Возможные значения: `ADMIN`, `TEACHER`, `STUDENT`.
|
||||||
|
|
||||||
|
### 2. Формы обучения (`education_forms`)
|
||||||
|
Справочник форм обучения.
|
||||||
|
- `id` (BIGSERIAL, РК)
|
||||||
|
- `name` (VARCHAR(100), UNIQUE, NOT NULL) - например: 'Бакалавриат', 'Магистратура', 'Специалитет'
|
||||||
|
|
||||||
|
### 3. Студенческие группы (`student_groups`)
|
||||||
|
Справочник учебных групп.
|
||||||
|
- `id` (BIGSERIAL, РК)
|
||||||
|
- `name` (VARCHAR(100), UNIQUE, NOT NULL) - название группы (например, 'ИВТ-21-1')
|
||||||
|
- `education_form_id` (BIGINT, FK -> education_forms.id) - привязка к форме обучения
|
||||||
|
|
||||||
|
### 4. Подгруппы (`subgroups`)
|
||||||
|
Разделение групп на подгруппы (для лабораторных и практик).
|
||||||
|
- `id` (BIGSERIAL, РК)
|
||||||
|
- `group_id` (BIGINT, FK -> student_groups.id)
|
||||||
|
- `name` (VARCHAR(100), NOT NULL) - название подгруппы (например, 'Подгруппа 1')
|
||||||
|
|
||||||
|
### 5. Дисциплины (`subjects`)
|
||||||
|
Справочник учебных предметов.
|
||||||
|
- `id` (BIGSERIAL, РК)
|
||||||
|
- `name` (VARCHAR(200), UNIQUE, NOT NULL) - название дисциплины (например, 'Высшая математика')
|
||||||
|
|
||||||
|
### 6. Типы занятий (`lesson_types`)
|
||||||
|
Справочник видов учебных занятий.
|
||||||
|
- `id` (BIGSERIAL, РК)
|
||||||
|
- `name` (VARCHAR(50), UNIQUE, NOT NULL) - вид занятия (Лекция, Практика, Лабораторная работа)
|
||||||
|
|
||||||
|
### 7. Оборудование (`equipments`)
|
||||||
|
Справочник доступного оборудования для аудиторий.
|
||||||
|
- `id` (BIGSERIAL, РК)
|
||||||
|
- `name` (VARCHAR(50), UNIQUE, NOT NULL) - например, 'Проектор', 'ПК', 'Лаборатория', 'Интерактивная доска'
|
||||||
|
|
||||||
|
### 8. Аудиторный фонд (`classrooms`)
|
||||||
|
Справочник аудиторий университета.
|
||||||
|
- `id` (BIGSERIAL, РК)
|
||||||
|
- `name` (VARCHAR(50), UNIQUE, NOT NULL) - номер/название аудитории
|
||||||
|
- `capacity` (INT, NOT NULL) - вместимость (количество посадочных мест)
|
||||||
|
- `is_available` (BOOLEAN, DEFAULT TRUE) - статус доступности аудитории для проведения пар
|
||||||
|
|
||||||
|
### 9. Привязка оборудования к аудиториям (`classroom_equipments`)
|
||||||
|
Связующая таблица (Many-to-Many) для указания, какое оборудование есть в аудитории.
|
||||||
|
- `classroom_id` (BIGINT, FK -> classrooms.id)
|
||||||
|
- `equipment_id` (BIGINT, FK -> equipments.id)
|
||||||
|
|
||||||
|
### 10. Привязка преподавателей к дисциплинам (`teacher_subjects`)
|
||||||
|
Связующая таблица (Many-to-Many). Определяет, какие предметы имеет право вести преподаватель.
|
||||||
|
- `user_id` (BIGINT, FK -> users.id)
|
||||||
|
- `subject_id` (BIGINT, FK -> subjects.id)
|
||||||
|
|
||||||
|
### 11. Расписание занятий (Пар) (`lessons`)
|
||||||
|
Главная таблица, хранящая сетку расписания. Связывает все основные сущности в рамках одного учебного занятия.
|
||||||
|
- `id` (BIGSERIAL, РК)
|
||||||
|
- `teacher_id` (BIGINT, FK -> users.id) - кто ведет пару
|
||||||
|
- `subject_id` (BIGINT, FK -> subjects.id) - какую дисциплину ведут
|
||||||
|
- `lesson_type_id` (BIGINT, FK -> lesson_types.id) - вид занятия (лекция/практика)
|
||||||
|
- `classroom_id` (BIGINT, FK -> classrooms.id) - где проходит
|
||||||
|
- `group_id` (BIGINT, FK -> student_groups.id) - у какой группы
|
||||||
|
- `subgroup_id` (BIGINT, FK -> subgroups.id, NULLABLE) - конкретная подгруппа (если пара не у всей группы)
|
||||||
|
- `day_of_week` (INT, NOT NULL) - день недели (1=Понедельник ... 7=Воскресенье)
|
||||||
|
- `is_even_week` (BOOLEAN, NOT NULL) - признак четности недели (TRUE = четная, FALSE = нечетная)
|
||||||
|
- `start_time` (TIME, NOT NULL) - время начала пары (например: '08:00:00')
|
||||||
|
- `end_time` (TIME, NOT NULL) - время окончания пары (например: '09:30:00')
|
||||||
|
|
||||||
|
## Особенности архитектуры БД
|
||||||
|
- Используется расширение `pgcrypto` для шифрования паролей пользователей (`bcrypt`).
|
||||||
|
- Все связи Many-to-Many и внешние ключи настроены с `ON DELETE CASCADE` там, где это необходимо, для поддержания целостности данных при удалении родительских сущностей.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.magistr.app.controller;
|
||||||
|
|
||||||
|
import com.magistr.app.model.Subject;
|
||||||
|
import com.magistr.app.repository.SubjectRepository;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/subjects")
|
||||||
|
public class SubjectController {
|
||||||
|
|
||||||
|
private final SubjectRepository subjectRepository;
|
||||||
|
|
||||||
|
public SubjectController(SubjectRepository subjectRepository) {
|
||||||
|
this.subjectRepository = subjectRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<Subject> getAllSubjects() {
|
||||||
|
return subjectRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<?> createSubject(@RequestBody Map<String, String> request) {
|
||||||
|
String name = request.get("name");
|
||||||
|
if (name == null || name.isBlank()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "Название обязательно"));
|
||||||
|
}
|
||||||
|
if (subjectRepository.findByName(name.trim()).isPresent()) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "Дисциплина с таким названием уже существует"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Subject subject = new Subject();
|
||||||
|
subject.setName(name.trim());
|
||||||
|
subjectRepository.save(subject);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<?> deleteSubject(@PathVariable Long id) {
|
||||||
|
if (!subjectRepository.existsById(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
subjectRepository.deleteById(id);
|
||||||
|
return ResponseEntity.ok(Map.of("message", "Дисциплина удалена"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package com.magistr.app.controller;
|
||||||
|
|
||||||
|
import com.magistr.app.dto.TeacherSubjectResponse;
|
||||||
|
import com.magistr.app.model.TeacherSubject;
|
||||||
|
import com.magistr.app.model.TeacherSubjectId;
|
||||||
|
import com.magistr.app.repository.SubjectRepository;
|
||||||
|
import com.magistr.app.repository.TeacherSubjectRepository;
|
||||||
|
import com.magistr.app.repository.UserRepository;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/teacher-subjects")
|
||||||
|
public class TeacherSubjectController {
|
||||||
|
|
||||||
|
private final TeacherSubjectRepository teacherSubjectRepository;
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final SubjectRepository subjectRepository;
|
||||||
|
|
||||||
|
public TeacherSubjectController(TeacherSubjectRepository teacherSubjectRepository,
|
||||||
|
UserRepository userRepository,
|
||||||
|
SubjectRepository subjectRepository) {
|
||||||
|
this.teacherSubjectRepository = teacherSubjectRepository;
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.subjectRepository = subjectRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public List<TeacherSubjectResponse> getAll() {
|
||||||
|
return teacherSubjectRepository.findAll().stream()
|
||||||
|
.map(ts -> new TeacherSubjectResponse(
|
||||||
|
ts.getUserId(),
|
||||||
|
ts.getUser().getUsername(),
|
||||||
|
ts.getSubjectId(),
|
||||||
|
ts.getSubject().getName()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<?> create(@RequestBody Map<String, Long> request) {
|
||||||
|
Long userId = request.get("userId");
|
||||||
|
Long subjectId = request.get("subjectId");
|
||||||
|
|
||||||
|
if (userId == null || subjectId == null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "userId и subjectId обязательны"));
|
||||||
|
}
|
||||||
|
if (!userRepository.existsById(userId)) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "Преподаватель не найден"));
|
||||||
|
}
|
||||||
|
if (!subjectRepository.existsById(subjectId)) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "Дисциплина не найдена"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TeacherSubjectId id = new TeacherSubjectId(userId, subjectId);
|
||||||
|
if (teacherSubjectRepository.existsById(id)) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "Привязка уже существует"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TeacherSubject ts = new TeacherSubject(userId, subjectId);
|
||||||
|
teacherSubjectRepository.save(ts);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(Map.of("message", "Привязка создана"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
public ResponseEntity<?> delete(@RequestBody Map<String, Long> request) {
|
||||||
|
Long userId = request.get("userId");
|
||||||
|
Long subjectId = request.get("subjectId");
|
||||||
|
|
||||||
|
if (userId == null || subjectId == null) {
|
||||||
|
return ResponseEntity.badRequest().body(Map.of("message", "userId и subjectId обязательны"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TeacherSubjectId id = new TeacherSubjectId(userId, subjectId);
|
||||||
|
if (!teacherSubjectRepository.existsById(id)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
teacherSubjectRepository.deleteById(id);
|
||||||
|
return ResponseEntity.ok(Map.of("message", "Привязка удалена"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,13 @@ public class UserController {
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/teachers")
|
||||||
|
public List<UserResponse> getTeachers() {
|
||||||
|
return userRepository.findByRole(Role.TEACHER).stream()
|
||||||
|
.map(u -> new UserResponse(u.getId(), u.getUsername(), u.getRole().name()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request) {
|
public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request) {
|
||||||
if (request.getUsername() == null || request.getUsername().isBlank()) {
|
if (request.getUsername() == null || request.getUsername().isBlank()) {
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.magistr.app.dto;
|
||||||
|
|
||||||
|
public class TeacherSubjectResponse {
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
private String username;
|
||||||
|
private Long subjectId;
|
||||||
|
private String subjectName;
|
||||||
|
|
||||||
|
public TeacherSubjectResponse(Long userId, String username, Long subjectId, String subjectName) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.username = username;
|
||||||
|
this.subjectId = subjectId;
|
||||||
|
this.subjectName = subjectName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSubjectId() {
|
||||||
|
return subjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getSubjectName() {
|
||||||
|
return subjectName;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
backend/src/main/java/com/magistr/app/model/Subject.java
Normal file
39
backend/src/main/java/com/magistr/app/model/Subject.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package com.magistr.app.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "subjects")
|
||||||
|
public class Subject {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(unique = true, nullable = false, length = 200)
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
public Subject() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public Subject(Long id, String name) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.magistr.app.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "teacher_subjects")
|
||||||
|
@IdClass(TeacherSubjectId.class)
|
||||||
|
public class TeacherSubject {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "user_id")
|
||||||
|
private Long userId;
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@Column(name = "subject_id")
|
||||||
|
private Long subjectId;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.EAGER)
|
||||||
|
@JoinColumn(name = "user_id", insertable = false, updatable = false)
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.EAGER)
|
||||||
|
@JoinColumn(name = "subject_id", insertable = false, updatable = false)
|
||||||
|
private Subject subject;
|
||||||
|
|
||||||
|
public TeacherSubject() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public TeacherSubject(Long userId, Long subjectId) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.subjectId = subjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(Long userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSubjectId() {
|
||||||
|
return subjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubjectId(Long subjectId) {
|
||||||
|
this.subjectId = subjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User getUser() {
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Subject getSubject() {
|
||||||
|
return subject;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.magistr.app.model;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class TeacherSubjectId implements Serializable {
|
||||||
|
|
||||||
|
private Long userId;
|
||||||
|
private Long subjectId;
|
||||||
|
|
||||||
|
public TeacherSubjectId() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public TeacherSubjectId(Long userId, Long subjectId) {
|
||||||
|
this.userId = userId;
|
||||||
|
this.subjectId = subjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getUserId() {
|
||||||
|
return userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserId(Long userId) {
|
||||||
|
this.userId = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getSubjectId() {
|
||||||
|
return subjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubjectId(Long subjectId) {
|
||||||
|
this.subjectId = subjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (!(o instanceof TeacherSubjectId that)) return false;
|
||||||
|
return Objects.equals(userId, that.userId) && Objects.equals(subjectId, that.subjectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(userId, subjectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.magistr.app.repository;
|
||||||
|
|
||||||
|
import com.magistr.app.model.Subject;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface SubjectRepository extends JpaRepository<Subject, Long> {
|
||||||
|
Optional<Subject> findByName(String name);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.magistr.app.repository;
|
||||||
|
|
||||||
|
import com.magistr.app.model.TeacherSubject;
|
||||||
|
import com.magistr.app.model.TeacherSubjectId;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface TeacherSubjectRepository extends JpaRepository<TeacherSubject, TeacherSubjectId> {
|
||||||
|
List<TeacherSubject> findByUserId(Long userId);
|
||||||
|
List<TeacherSubject> findBySubjectId(Long subjectId);
|
||||||
|
}
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
package com.magistr.app.repository;
|
package com.magistr.app.repository;
|
||||||
|
|
||||||
|
import com.magistr.app.model.Role;
|
||||||
import com.magistr.app.model.User;
|
import com.magistr.app.model.User;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
|
||||||
Optional<User> findByUsername(String username);
|
Optional<User> findByUsername(String username);
|
||||||
|
|
||||||
|
List<User> findByRole(Role role);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ CREATE TABLE IF NOT EXISTS classroom_equipments (
|
|||||||
INSERT INTO classroom_equipments (classroom_id, equipment_id) VALUES
|
INSERT INTO classroom_equipments (classroom_id, equipment_id) VALUES
|
||||||
(1, 1), (1, 4),
|
(1, 1), (1, 4),
|
||||||
-- '202 IT Lab' -> ПК (2), Проектор (1), Лаборатория (3)
|
-- '202 IT Lab' -> ПК (2), Проектор (1), Лаборатория (3)
|
||||||
(2, 2), (2, 1), (2, 3),
|
(2, 2), (2, 1), (2, 3)
|
||||||
-- '303 Обычная' -> ничего
|
-- '303 Обычная' -> ничего
|
||||||
ON CONFLICT DO NOTHING;
|
ON CONFLICT DO NOTHING;
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,16 @@
|
|||||||
const createEquipmentAlert = document.getElementById('create-equipment-alert');
|
const createEquipmentAlert = document.getElementById('create-equipment-alert');
|
||||||
const equipmentCheckboxes = document.getElementById('equipment-checkboxes');
|
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 ---
|
// --- Multi-select logic ---
|
||||||
function updateSelectText(containerId, textId) {
|
function updateSelectText(containerId, textId) {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
@@ -130,6 +140,8 @@
|
|||||||
let allGroups = [];
|
let allGroups = [];
|
||||||
let allEducationForms = [];
|
let allEducationForms = [];
|
||||||
let allEquipments = [];
|
let allEquipments = [];
|
||||||
|
let allSubjects = [];
|
||||||
|
let allTeachers = [];
|
||||||
|
|
||||||
// ---- Tab Switching ----
|
// ---- Tab Switching ----
|
||||||
const TAB_TITLES = {
|
const TAB_TITLES = {
|
||||||
@@ -137,7 +149,8 @@
|
|||||||
groups: 'Управление группами',
|
groups: 'Управление группами',
|
||||||
'edu-forms': 'Формы обучения',
|
'edu-forms': 'Формы обучения',
|
||||||
equipments: 'Оборудование',
|
equipments: 'Оборудование',
|
||||||
classrooms: 'Аудитории'
|
classrooms: 'Аудитории',
|
||||||
|
subjects: 'Дисциплины и преподаватели'
|
||||||
};
|
};
|
||||||
|
|
||||||
navItems.forEach(item => {
|
navItems.forEach(item => {
|
||||||
@@ -162,6 +175,7 @@
|
|||||||
if (tab === 'edu-forms') loadEducationForms();
|
if (tab === 'edu-forms') loadEducationForms();
|
||||||
if (tab === 'equipments') loadEquipments();
|
if (tab === 'equipments') loadEquipments();
|
||||||
if (tab === 'classrooms') { loadEquipments().then(() => loadClassrooms()); }
|
if (tab === 'classrooms') { loadEquipments().then(() => loadClassrooms()); }
|
||||||
|
if (tab === 'subjects') { Promise.all([loadSubjects(), loadTeachers()]).then(() => loadTeacherSubjects()); }
|
||||||
|
|
||||||
sidebar.classList.remove('open');
|
sidebar.classList.remove('open');
|
||||||
sidebarOverlay.classList.remove('open');
|
sidebarOverlay.classList.remove('open');
|
||||||
@@ -711,6 +725,178 @@
|
|||||||
} catch (e) { showAlert(editClassroomAlert, 'Ошибка соединения', 'error'); }
|
} catch (e) { showAlert(editClassroomAlert, 'Ошибка соединения', 'error'); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// SUBJECTS
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
async function loadSubjects() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/subjects', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token },
|
||||||
|
});
|
||||||
|
allSubjects = await res.json();
|
||||||
|
renderSubjects(allSubjects);
|
||||||
|
populateSubjectSelect(allSubjects);
|
||||||
|
} catch (e) {
|
||||||
|
if (subjectsTbody) subjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ошибка загрузки</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSubjects(subjects) {
|
||||||
|
if (!subjects.length) {
|
||||||
|
subjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет дисциплин</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
subjectsTbody.innerHTML = subjects.map(s => `
|
||||||
|
<tr>
|
||||||
|
<td>${s.id}</td>
|
||||||
|
<td>${escapeHtml(s.name)}</td>
|
||||||
|
<td><button class="btn-delete" data-id="${s.id}">Удалить</button></td>
|
||||||
|
</tr>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateSubjectSelect(subjects) {
|
||||||
|
if (!assignSubjectSelect) return;
|
||||||
|
const currentVal = assignSubjectSelect.value;
|
||||||
|
assignSubjectSelect.innerHTML = '<option value="">Выберите дисциплину</option>' +
|
||||||
|
subjects.map(s => `<option value="${s.id}">${escapeHtml(s.name)}</option>`).join('');
|
||||||
|
if (currentVal && subjects.find(s => s.id == currentVal)) {
|
||||||
|
assignSubjectSelect.value = currentVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTeachers() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/users/teachers', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token },
|
||||||
|
});
|
||||||
|
allTeachers = await res.json();
|
||||||
|
populateTeacherSelect(allTeachers);
|
||||||
|
} catch (e) {
|
||||||
|
if (assignTeacherSelect) assignTeacherSelect.innerHTML = '<option value="">Ошибка загрузки</option>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateTeacherSelect(teachers) {
|
||||||
|
if (!assignTeacherSelect) return;
|
||||||
|
const currentVal = assignTeacherSelect.value;
|
||||||
|
if (!teachers.length) {
|
||||||
|
assignTeacherSelect.innerHTML = '<option value="">Нет преподавателей</option>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assignTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' +
|
||||||
|
teachers.map(t => `<option value="${t.id}">${escapeHtml(t.username)}</option>`).join('');
|
||||||
|
if (currentVal && teachers.find(t => t.id == currentVal)) {
|
||||||
|
assignTeacherSelect.value = currentVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadTeacherSubjects() {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/teacher-subjects', {
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token },
|
||||||
|
});
|
||||||
|
const tsData = await res.json();
|
||||||
|
renderTeacherSubjects(tsData);
|
||||||
|
} catch (e) {
|
||||||
|
if (teacherSubjectsTbody) teacherSubjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ошибка загрузки</td></tr>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTeacherSubjects(tsArray) {
|
||||||
|
if (!tsArray.length) {
|
||||||
|
teacherSubjectsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет привязок</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
teacherSubjectsTbody.innerHTML = tsArray.map(ts => `
|
||||||
|
<tr>
|
||||||
|
<td>${escapeHtml(ts.username)}</td>
|
||||||
|
<td>${escapeHtml(ts.subjectName)}</td>
|
||||||
|
<td><button class="btn-delete" data-user-id="${ts.userId}" data-subject-id="${ts.subjectId}">Удалить</button></td>
|
||||||
|
</tr>`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
createSubjectForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
hideAlert(createSubjectAlert);
|
||||||
|
const name = document.getElementById('new-subject-name').value.trim();
|
||||||
|
if (!name) { showAlert(createSubjectAlert, 'Введите название', 'error'); return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/subjects', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
showAlert(createSubjectAlert, `Дисциплина "${data.name}" добавлена`, 'success');
|
||||||
|
createSubjectForm.reset();
|
||||||
|
loadSubjects();
|
||||||
|
} else {
|
||||||
|
showAlert(createSubjectAlert, data.message || 'Ошибка создания', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) { showAlert(createSubjectAlert, 'Ошибка соединения', 'error'); }
|
||||||
|
});
|
||||||
|
|
||||||
|
subjectsTbody.addEventListener('click', async (e) => {
|
||||||
|
const btn = e.target.closest('.btn-delete');
|
||||||
|
if (!btn) return;
|
||||||
|
if (!confirm('Удалить дисциплину?')) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/subjects/' + btn.dataset.id, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Authorization': 'Bearer ' + token },
|
||||||
|
});
|
||||||
|
if (res.ok) {
|
||||||
|
loadSubjects();
|
||||||
|
loadTeacherSubjects();
|
||||||
|
} else {
|
||||||
|
const data = await res.json();
|
||||||
|
alert(data.message || 'Ошибка удаления');
|
||||||
|
}
|
||||||
|
} catch (e) { alert('Ошибка соединения'); }
|
||||||
|
});
|
||||||
|
|
||||||
|
assignTeacherForm.addEventListener('submit', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
hideAlert(assignTeacherAlert);
|
||||||
|
const userId = assignTeacherSelect.value;
|
||||||
|
const subjectId = assignSubjectSelect.value;
|
||||||
|
if (!userId) { showAlert(assignTeacherAlert, 'Выберите преподавателя', 'error'); return; }
|
||||||
|
if (!subjectId) { showAlert(assignTeacherAlert, 'Выберите дисциплину', 'error'); return; }
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/teacher-subjects', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||||
|
body: JSON.stringify({ userId: Number(userId), subjectId: Number(subjectId) }),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok) {
|
||||||
|
showAlert(assignTeacherAlert, 'Привязка создана', 'success');
|
||||||
|
loadTeacherSubjects();
|
||||||
|
} else {
|
||||||
|
showAlert(assignTeacherAlert, data.message || 'Ошибка привязки', 'error');
|
||||||
|
}
|
||||||
|
} catch (e) { showAlert(assignTeacherAlert, 'Ошибка соединения', 'error'); }
|
||||||
|
});
|
||||||
|
|
||||||
|
teacherSubjectsTbody.addEventListener('click', async (e) => {
|
||||||
|
const btn = e.target.closest('.btn-delete');
|
||||||
|
if (!btn) return;
|
||||||
|
if (!confirm('Удалить привязку?')) return;
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/teacher-subjects', {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
||||||
|
body: JSON.stringify({ userId: Number(btn.dataset.userId), subjectId: Number(btn.dataset.subjectId) }),
|
||||||
|
});
|
||||||
|
if (res.ok) loadTeacherSubjects();
|
||||||
|
else alert('Ошибка удаления');
|
||||||
|
} catch (e) { alert('Ошибка соединения'); }
|
||||||
|
});
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// LOGOUT & INIT
|
// LOGOUT & INIT
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -74,6 +74,14 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Аудитории
|
Аудитории
|
||||||
</a>
|
</a>
|
||||||
|
<a href="#" class="nav-item" data-tab="subjects">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M12 20h9" />
|
||||||
|
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||||
|
</svg>
|
||||||
|
Дисциплины
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<button class="btn-logout" id="btn-logout">
|
<button class="btn-logout" id="btn-logout">
|
||||||
@@ -343,6 +351,82 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- ===== Subjects Tab ===== -->
|
||||||
|
<section class="content tab-content" id="tab-subjects" style="display:none;">
|
||||||
|
<div class="card create-card">
|
||||||
|
<h2>Новая дисциплина</h2>
|
||||||
|
<form id="create-subject-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new-subject-name">Название дисциплины</label>
|
||||||
|
<input type="text" id="new-subject-name" placeholder="Высшая математика" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-create">Добавить</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-alert" id="create-subject-alert" role="alert"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card create-card">
|
||||||
|
<h2>Привязка преподавателя к дисциплине</h2>
|
||||||
|
<form id="assign-teacher-form">
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="assign-teacher-select">Преподаватель</label>
|
||||||
|
<select id="assign-teacher-select">
|
||||||
|
<option value="">Загрузка...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="assign-subject-select">Дисциплина</label>
|
||||||
|
<select id="assign-subject-select">
|
||||||
|
<option value="">Загрузка...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn-create">Привязать</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-alert" id="assign-teacher-alert" role="alert"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Все дисциплины</h2>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table id="subjects-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Название</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="subjects-tbody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Привязки преподавателей</h2>
|
||||||
|
<div class="table-wrap">
|
||||||
|
<table id="teacher-subjects-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Преподаватель</th>
|
||||||
|
<th>Дисциплина</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="teacher-subjects-tbody">
|
||||||
|
<tr>
|
||||||
|
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- ===== Edit Classroom Modal ===== -->
|
<!-- ===== Edit Classroom Modal ===== -->
|
||||||
<div class="modal-overlay" id="modal-edit-classroom">
|
<div class="modal-overlay" id="modal-edit-classroom">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
|
|||||||
Reference in New Issue
Block a user