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();
|
||||
}
|
||||
|
||||
@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
|
||||
public ResponseEntity<?> createUser(@RequestBody CreateUserRequest request) {
|
||||
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;
|
||||
|
||||
import com.magistr.app.model.Role;
|
||||
import com.magistr.app.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
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
|
||||
(1, 1), (1, 4),
|
||||
-- '202 IT Lab' -> ПК (2), Проектор (1), Лаборатория (3)
|
||||
(2, 2), (2, 1), (2, 3),
|
||||
(2, 2), (2, 1), (2, 3)
|
||||
-- '303 Обычная' -> ничего
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
|
||||
@@ -73,6 +73,16 @@
|
||||
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);
|
||||
@@ -130,6 +140,8 @@
|
||||
let allGroups = [];
|
||||
let allEducationForms = [];
|
||||
let allEquipments = [];
|
||||
let allSubjects = [];
|
||||
let allTeachers = [];
|
||||
|
||||
// ---- Tab Switching ----
|
||||
const TAB_TITLES = {
|
||||
@@ -137,7 +149,8 @@
|
||||
groups: 'Управление группами',
|
||||
'edu-forms': 'Формы обучения',
|
||||
equipments: 'Оборудование',
|
||||
classrooms: 'Аудитории'
|
||||
classrooms: 'Аудитории',
|
||||
subjects: 'Дисциплины и преподаватели'
|
||||
};
|
||||
|
||||
navItems.forEach(item => {
|
||||
@@ -162,6 +175,7 @@
|
||||
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');
|
||||
@@ -711,6 +725,178 @@
|
||||
} 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
|
||||
// ============================================================
|
||||
|
||||
@@ -74,6 +74,14 @@
|
||||
</svg>
|
||||
Аудитории
|
||||
</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>
|
||||
<div class="sidebar-footer">
|
||||
<button class="btn-logout" id="btn-logout">
|
||||
@@ -343,6 +351,82 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Subjects Tab ===== -->
|
||||
<section class="content tab-content" id="tab-subjects" style="display:none;">
|
||||
<div class="card create-card">
|
||||
<h2>Новая дисциплина</h2>
|
||||
<form id="create-subject-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="new-subject-name">Название дисциплины</label>
|
||||
<input type="text" id="new-subject-name" placeholder="Высшая математика" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-create">Добавить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-subject-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card create-card">
|
||||
<h2>Привязка преподавателя к дисциплине</h2>
|
||||
<form id="assign-teacher-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="assign-teacher-select">Преподаватель</label>
|
||||
<select id="assign-teacher-select">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="assign-subject-select">Дисциплина</label>
|
||||
<select id="assign-subject-select">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-create">Привязать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="assign-teacher-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Все дисциплины</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="subjects-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="subjects-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>Привязки преподавателей</h2>
|
||||
<div class="table-wrap">
|
||||
<table id="teacher-subjects-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Преподаватель</th>
|
||||
<th>Дисциплина</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="teacher-subjects-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ===== Edit Classroom Modal ===== -->
|
||||
<div class="modal-overlay" id="modal-edit-classroom">
|
||||
<div class="modal-content">
|
||||
|
||||
Reference in New Issue
Block a user