From 6774cd673c4252f295ac5eb801ba705a02baf346 Mon Sep 17 00:00:00 2001 From: Zuev Date: Sun, 15 Feb 2026 03:15:43 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=B8=D0=BD=D0=B0=D0=BC=D0=B8?= =?UTF-8?q?=D1=87=D0=B5=D1=81=D0=BA=D0=B8=D0=B5=20=D1=84=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D1=8B=20=D0=BE=D0=B1=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20+?= =?UTF-8?q?=20=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D0=B0=20=D0=B3=D1=80?= =?UTF-8?q?=D1=83=D0=BF=D0=BF=D1=8B=20=D0=B2=20=D0=B0=D0=B4=D0=BC=D0=B8?= =?UTF-8?q?=D0=BD=D0=BA=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/EducationFormController.java | 67 ++++ .../app/controller/GroupController.java | 77 ++++ .../magistr/app/dto/CreateGroupRequest.java | 23 ++ .../com/magistr/app/dto/GroupResponse.java | 32 ++ .../com/magistr/app/model/EducationForm.java | 34 ++ .../com/magistr/app/model/StudentGroup.java | 46 +++ .../repository/EducationFormRepository.java | 11 + .../app/repository/GroupRepository.java | 14 + db/init/init.sql | 14 + frontend/admin/admin.css | 80 ++++ frontend/admin/admin.js | 354 ++++++++++++++---- frontend/admin/index.html | 115 +++++- 12 files changed, 782 insertions(+), 85 deletions(-) create mode 100644 backend/src/main/java/com/magistr/app/controller/EducationFormController.java create mode 100644 backend/src/main/java/com/magistr/app/controller/GroupController.java create mode 100644 backend/src/main/java/com/magistr/app/dto/CreateGroupRequest.java create mode 100644 backend/src/main/java/com/magistr/app/dto/GroupResponse.java create mode 100644 backend/src/main/java/com/magistr/app/model/EducationForm.java create mode 100644 backend/src/main/java/com/magistr/app/model/StudentGroup.java create mode 100644 backend/src/main/java/com/magistr/app/repository/EducationFormRepository.java create mode 100644 backend/src/main/java/com/magistr/app/repository/GroupRepository.java diff --git a/backend/src/main/java/com/magistr/app/controller/EducationFormController.java b/backend/src/main/java/com/magistr/app/controller/EducationFormController.java new file mode 100644 index 0000000..dd31357 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/controller/EducationFormController.java @@ -0,0 +1,67 @@ +package com.magistr.app.controller; + +import com.magistr.app.model.EducationForm; +import com.magistr.app.model.StudentGroup; +import com.magistr.app.repository.EducationFormRepository; +import com.magistr.app.repository.GroupRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api/education-forms") +public class EducationFormController { + + private final EducationFormRepository educationFormRepository; + private final GroupRepository groupRepository; + + public EducationFormController(EducationFormRepository educationFormRepository, + GroupRepository groupRepository) { + this.educationFormRepository = educationFormRepository; + this.groupRepository = groupRepository; + } + + @GetMapping + public List> getAll() { + return educationFormRepository.findAll().stream() + .map(ef -> Map.of("id", ef.getId(), "name", ef.getName())) + .toList(); + } + + @PostMapping + public ResponseEntity create(@RequestBody Map body) { + String name = body.get("name"); + if (name == null || name.isBlank()) { + return ResponseEntity.badRequest().body(Map.of("message", "Название обязательно")); + } + name = name.trim(); + if (educationFormRepository.findByName(name).isPresent()) { + return ResponseEntity.badRequest().body(Map.of("message", "Такая форма обучения уже существует")); + } + + EducationForm ef = new EducationForm(); + ef.setName(name); + educationFormRepository.save(ef); + + return ResponseEntity.ok(Map.of("id", ef.getId(), "name", ef.getName())); + } + + @DeleteMapping("/{id}") + public ResponseEntity delete(@PathVariable Long id) { + if (!educationFormRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + + // Check if any groups use this education form + List linked = groupRepository.findByEducationFormId(id); + if (!linked.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of( + "message", "Невозможно удалить: есть привязанные группы (" + linked.size() + ")")); + } + + educationFormRepository.deleteById(id); + return ResponseEntity.ok(Map.of("message", "Форма обучения удалена")); + } +} diff --git a/backend/src/main/java/com/magistr/app/controller/GroupController.java b/backend/src/main/java/com/magistr/app/controller/GroupController.java new file mode 100644 index 0000000..b8f7795 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/controller/GroupController.java @@ -0,0 +1,77 @@ +package com.magistr.app.controller; + +import com.magistr.app.dto.CreateGroupRequest; +import com.magistr.app.dto.GroupResponse; +import com.magistr.app.model.EducationForm; +import com.magistr.app.model.StudentGroup; +import com.magistr.app.repository.EducationFormRepository; +import com.magistr.app.repository.GroupRepository; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@RestController +@RequestMapping("/api/groups") +public class GroupController { + + private final GroupRepository groupRepository; + private final EducationFormRepository educationFormRepository; + + public GroupController(GroupRepository groupRepository, + EducationFormRepository educationFormRepository) { + this.groupRepository = groupRepository; + this.educationFormRepository = educationFormRepository; + } + + @GetMapping + public List getAllGroups() { + return groupRepository.findAll().stream() + .map(g -> new GroupResponse( + g.getId(), + g.getName(), + g.getEducationForm().getId(), + g.getEducationForm().getName())) + .toList(); + } + + @PostMapping + public ResponseEntity createGroup(@RequestBody CreateGroupRequest request) { + if (request.getName() == null || request.getName().isBlank()) { + return ResponseEntity.badRequest().body(Map.of("message", "Название группы обязательно")); + } + if (groupRepository.findByName(request.getName().trim()).isPresent()) { + return ResponseEntity.badRequest().body(Map.of("message", "Группа с таким названием уже существует")); + } + if (request.getEducationFormId() == null) { + return ResponseEntity.badRequest().body(Map.of("message", "Форма обучения обязательна")); + } + + Optional efOpt = educationFormRepository.findById(request.getEducationFormId()); + if (efOpt.isEmpty()) { + return ResponseEntity.badRequest().body(Map.of("message", "Форма обучения не найдена")); + } + + StudentGroup group = new StudentGroup(); + group.setName(request.getName().trim()); + group.setEducationForm(efOpt.get()); + groupRepository.save(group); + + return ResponseEntity.ok(new GroupResponse( + group.getId(), + group.getName(), + group.getEducationForm().getId(), + group.getEducationForm().getName())); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteGroup(@PathVariable Long id) { + if (!groupRepository.existsById(id)) { + return ResponseEntity.notFound().build(); + } + groupRepository.deleteById(id); + return ResponseEntity.ok(Map.of("message", "Группа удалена")); + } +} diff --git a/backend/src/main/java/com/magistr/app/dto/CreateGroupRequest.java b/backend/src/main/java/com/magistr/app/dto/CreateGroupRequest.java new file mode 100644 index 0000000..43e604a --- /dev/null +++ b/backend/src/main/java/com/magistr/app/dto/CreateGroupRequest.java @@ -0,0 +1,23 @@ +package com.magistr.app.dto; + +public class CreateGroupRequest { + + private String name; + private Long educationFormId; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getEducationFormId() { + return educationFormId; + } + + public void setEducationFormId(Long educationFormId) { + this.educationFormId = educationFormId; + } +} diff --git a/backend/src/main/java/com/magistr/app/dto/GroupResponse.java b/backend/src/main/java/com/magistr/app/dto/GroupResponse.java new file mode 100644 index 0000000..63becb8 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/dto/GroupResponse.java @@ -0,0 +1,32 @@ +package com.magistr.app.dto; + +public class GroupResponse { + + private Long id; + private String name; + private Long educationFormId; + private String educationFormName; + + public GroupResponse(Long id, String name, Long educationFormId, String educationFormName) { + this.id = id; + this.name = name; + this.educationFormId = educationFormId; + this.educationFormName = educationFormName; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Long getEducationFormId() { + return educationFormId; + } + + public String getEducationFormName() { + return educationFormName; + } +} diff --git a/backend/src/main/java/com/magistr/app/model/EducationForm.java b/backend/src/main/java/com/magistr/app/model/EducationForm.java new file mode 100644 index 0000000..a2f1c58 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/model/EducationForm.java @@ -0,0 +1,34 @@ +package com.magistr.app.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "education_forms") +public class EducationForm { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 100) + private String name; + + public EducationForm() { + } + + 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/StudentGroup.java b/backend/src/main/java/com/magistr/app/model/StudentGroup.java new file mode 100644 index 0000000..3ad914a --- /dev/null +++ b/backend/src/main/java/com/magistr/app/model/StudentGroup.java @@ -0,0 +1,46 @@ +package com.magistr.app.model; + +import jakarta.persistence.*; + +@Entity +@Table(name = "student_groups") +public class StudentGroup { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false, length = 100) + private String name; + + @ManyToOne(optional = false) + @JoinColumn(name = "education_form_id", nullable = false) + private EducationForm educationForm; + + public StudentGroup() { + } + + 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; + } + + public EducationForm getEducationForm() { + return educationForm; + } + + public void setEducationForm(EducationForm educationForm) { + this.educationForm = educationForm; + } +} diff --git a/backend/src/main/java/com/magistr/app/repository/EducationFormRepository.java b/backend/src/main/java/com/magistr/app/repository/EducationFormRepository.java new file mode 100644 index 0000000..dd94980 --- /dev/null +++ b/backend/src/main/java/com/magistr/app/repository/EducationFormRepository.java @@ -0,0 +1,11 @@ +package com.magistr.app.repository; + +import com.magistr.app.model.EducationForm; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface EducationFormRepository extends JpaRepository { + + Optional findByName(String name); +} diff --git a/backend/src/main/java/com/magistr/app/repository/GroupRepository.java b/backend/src/main/java/com/magistr/app/repository/GroupRepository.java new file mode 100644 index 0000000..e2fea0f --- /dev/null +++ b/backend/src/main/java/com/magistr/app/repository/GroupRepository.java @@ -0,0 +1,14 @@ +package com.magistr.app.repository; + +import com.magistr.app.model.StudentGroup; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface GroupRepository extends JpaRepository { + + Optional findByName(String name); + + List findByEducationFormId(Long educationFormId); +} diff --git a/db/init/init.sql b/db/init/init.sql index c0df4b1..a504d82 100644 --- a/db/init/init.sql +++ b/db/init/init.sql @@ -11,3 +11,17 @@ CREATE TABLE IF NOT EXISTS users ( INSERT INTO users (username, password, role) VALUES ('admin', crypt('admin', gen_salt('bf', 10)), 'ADMIN') ON CONFLICT (username) DO NOTHING; + +CREATE TABLE IF NOT EXISTS education_forms ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL +); + +INSERT INTO education_forms (name) VALUES ('Бакалавриат'), ('Магистратура'), ('Специалитет') +ON CONFLICT (name) DO NOTHING; + +CREATE TABLE IF NOT EXISTS student_groups ( + id BIGSERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + education_form_id BIGINT NOT NULL REFERENCES education_forms(id) +); diff --git a/frontend/admin/admin.css b/frontend/admin/admin.css index 3336fb5..7b6498b 100644 --- a/frontend/admin/admin.css +++ b/frontend/admin/admin.css @@ -339,6 +339,86 @@ 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; +} + +.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; diff --git a/frontend/admin/admin.js b/frontend/admin/admin.js index 1369d76..425f28a 100644 --- a/frontend/admin/admin.js +++ b/frontend/admin/admin.js @@ -9,36 +9,103 @@ return; } - const tbody = document.getElementById('users-tbody'); - const createForm = document.getElementById('create-form'); - const createAlert = document.getElementById('create-alert'); + // ---- 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'); - // ---- Mobile Menu ---- - function toggleSidebar() { - sidebar.classList.toggle('open'); - sidebarOverlay.classList.toggle('open'); + // 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'); + + const navItems = document.querySelectorAll('.nav-item[data-tab]'); + const tabContents = document.querySelectorAll('.tab-content'); + + // ---- State ---- + let allGroups = []; + let allEducationForms = []; + + // ---- Tab Switching ---- + const TAB_TITLES = { + users: 'Управление пользователями', + groups: 'Управление группами', + 'edu-forms': 'Формы обучения', + }; + + 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(); + + sidebar.classList.remove('open'); + sidebarOverlay.classList.remove('open'); } - menuToggle.addEventListener('click', toggleSidebar); - sidebarOverlay.addEventListener('click', toggleSidebar); + // ---- Mobile Menu ---- + menuToggle.addEventListener('click', () => { + sidebar.classList.toggle('open'); + sidebarOverlay.classList.toggle('open'); + }); + sidebarOverlay.addEventListener('click', () => { + sidebar.classList.remove('open'); + sidebarOverlay.classList.remove('open'); + }); - const ROLE_LABELS = { - ADMIN: 'Администратор', - TEACHER: 'Преподаватель', - STUDENT: 'Студент', - }; + // ---- Helpers ---- + const ROLE_LABELS = { ADMIN: 'Администратор', TEACHER: 'Преподаватель', STUDENT: 'Студент' }; + const ROLE_BADGE = { ADMIN: 'badge-admin', TEACHER: 'badge-teacher', STUDENT: 'badge-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 + // ============================================================ - // ---- Load Users ---- async function loadUsers() { try { const res = await fetch('/api/users', { @@ -47,112 +114,241 @@ const users = await res.json(); renderUsers(users); } catch (e) { - tbody.innerHTML = 'Ошибка загрузки'; + usersTbody.innerHTML = 'Ошибка загрузки'; } } function renderUsers(users) { if (!users.length) { - tbody.innerHTML = 'Нет пользователей'; + usersTbody.innerHTML = 'Нет пользователей'; return; } - - tbody.innerHTML = users.map(u => ` + usersTbody.innerHTML = users.map(u => ` ${u.id} ${escapeHtml(u.username)} ${ROLE_LABELS[u.role] || u.role} - - `).join(''); + `).join(''); } - function escapeHtml(str) { - const div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; - } - - // ---- Create User ---- createForm.addEventListener('submit', async (e) => { e.preventDefault(); - hideAlert(); - + 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('Заполните все поля', 'error'); - return; - } + if (!username || !password) { showAlert(createAlert, 'Заполните все поля', 'error'); return; } try { const res = await fetch('/api/users', { method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + token, - }, + headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ username, password, role }), }); - const data = await res.json(); - if (res.ok) { - showAlert(`Пользователь "${data.username}" создан`, 'success'); + showAlert(createAlert, `Пользователь "${data.username}" создан`, 'success'); createForm.reset(); loadUsers(); } else { - showAlert(data.message || 'Ошибка создания', 'error'); + showAlert(createAlert, data.message || 'Ошибка создания', 'error'); } - } catch (e) { - showAlert('Ошибка соединения', 'error'); - } + } catch (e) { showAlert(createAlert, 'Ошибка соединения', 'error'); } }); - // ---- Delete User ---- - tbody.addEventListener('click', async (e) => { + usersTbody.addEventListener('click', async (e) => { const btn = e.target.closest('.btn-delete'); if (!btn) return; - - const id = btn.dataset.id; if (!confirm('Удалить пользователя?')) return; - try { - const res = await fetch('/api/users/' + id, { + const res = await fetch('/api/users/' + btn.dataset.id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token }, }); - - if (res.ok) { - loadUsers(); - } else { - alert('Ошибка удаления'); - } - } catch (e) { - alert('Ошибка соединения'); - } + if (res.ok) loadUsers(); + else alert('Ошибка удаления'); + } catch (e) { alert('Ошибка соединения'); } }); - // ---- Logout ---- + // ============================================================ + // 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('Ошибка соединения'); } + }); + + // ============================================================ + // LOGOUT & INIT + // ============================================================ + btnLogout.addEventListener('click', () => { localStorage.removeItem('token'); localStorage.removeItem('role'); window.location.href = '/'; }); - // ---- Helpers ---- - function showAlert(msg, type) { - createAlert.className = 'form-alert ' + type; - createAlert.textContent = msg; - } - - function hideAlert() { - createAlert.className = 'form-alert'; - createAlert.textContent = ''; - } - - // Init loadUsers(); })(); diff --git a/frontend/admin/index.html b/frontend/admin/index.html index 8cdada3..19fdeae 100644 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -31,7 +31,7 @@