diff --git a/.agent/rules/database_schema.md b/.agent/rules/database_schema.md new file mode 100644 index 0000000..a423c39 --- /dev/null +++ b/.agent/rules/database_schema.md @@ -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` там, где это необходимо, для поддержания целостности данных при удалении родительских сущностей. diff --git a/backend/src/main/java/com/magistr/app/controller/SubjectController.java b/backend/src/main/java/com/magistr/app/controller/SubjectController.java new file mode 100644 index 0000000..fb9e64f --- /dev/null +++ b/backend/src/main/java/com/magistr/app/controller/SubjectController.java @@ -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 getAllSubjects() { + return subjectRepository.findAll(); + } + + @PostMapping + public ResponseEntity createSubject(@RequestBody Map 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", "Дисциплина удалена")); + } +} diff --git a/backend/src/main/java/com/magistr/app/controller/TeacherSubjectController.java b/backend/src/main/java/com/magistr/app/controller/TeacherSubjectController.java new file mode 100644 index 0000000..022006f --- /dev/null +++ b/backend/src/main/java/com/magistr/app/controller/TeacherSubjectController.java @@ -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 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 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 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", "Привязка удалена")); + } +} diff --git a/backend/src/main/java/com/magistr/app/controller/UserController.java b/backend/src/main/java/com/magistr/app/controller/UserController.java index cd256ef..2dfad6b 100644 --- a/backend/src/main/java/com/magistr/app/controller/UserController.java +++ b/backend/src/main/java/com/magistr/app/controller/UserController.java @@ -31,6 +31,13 @@ public class UserController { .toList(); } + @GetMapping("/teachers") + public List 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()) { diff --git a/backend/src/main/java/com/magistr/app/dto/TeacherSubjectResponse.java b/backend/src/main/java/com/magistr/app/dto/TeacherSubjectResponse.java new file mode 100644 index 0000000..c3c4e93 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/dto/TeacherSubjectResponse.java @@ -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; + } +} diff --git a/backend/src/main/java/com/magistr/app/model/Subject.java b/backend/src/main/java/com/magistr/app/model/Subject.java new file mode 100644 index 0000000..c11d0b1 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/model/Subject.java @@ -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; + } +} diff --git a/backend/src/main/java/com/magistr/app/model/TeacherSubject.java b/backend/src/main/java/com/magistr/app/model/TeacherSubject.java new file mode 100644 index 0000000..4809bde --- /dev/null +++ b/backend/src/main/java/com/magistr/app/model/TeacherSubject.java @@ -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; + } +} diff --git a/backend/src/main/java/com/magistr/app/model/TeacherSubjectId.java b/backend/src/main/java/com/magistr/app/model/TeacherSubjectId.java new file mode 100644 index 0000000..f4e62b7 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/model/TeacherSubjectId.java @@ -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); + } +} diff --git a/backend/src/main/java/com/magistr/app/repository/SubjectRepository.java b/backend/src/main/java/com/magistr/app/repository/SubjectRepository.java new file mode 100644 index 0000000..8c5a8fa --- /dev/null +++ b/backend/src/main/java/com/magistr/app/repository/SubjectRepository.java @@ -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 { + Optional findByName(String name); +} diff --git a/backend/src/main/java/com/magistr/app/repository/TeacherSubjectRepository.java b/backend/src/main/java/com/magistr/app/repository/TeacherSubjectRepository.java new file mode 100644 index 0000000..8f90553 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/repository/TeacherSubjectRepository.java @@ -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 { + List findByUserId(Long userId); + List findBySubjectId(Long subjectId); +} diff --git a/backend/src/main/java/com/magistr/app/repository/UserRepository.java b/backend/src/main/java/com/magistr/app/repository/UserRepository.java index 5c7ae4a..3711bd3 100644 --- a/backend/src/main/java/com/magistr/app/repository/UserRepository.java +++ b/backend/src/main/java/com/magistr/app/repository/UserRepository.java @@ -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 { Optional findByUsername(String username); + + List findByRole(Role role); } diff --git a/db/init/init.sql b/db/init/init.sql index a2401e2..4234149 100644 --- a/db/init/init.sql +++ b/db/init/init.sql @@ -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; diff --git a/frontend/admin/admin.js b/frontend/admin/admin.js index af4a7f6..9d0c01b 100644 --- a/frontend/admin/admin.js +++ b/frontend/admin/admin.js @@ -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 = 'Ошибка загрузки'; + } + } + + 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 // ============================================================ diff --git a/frontend/admin/index.html b/frontend/admin/index.html index 41b3e09..c8fa44a 100644 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -74,6 +74,14 @@ Аудитории + + + + + + Дисциплины + + + +