From 08b8607f053440db8f8ac6206be5eb0c3aa4c358 Mon Sep 17 00:00:00 2001 From: dipatrik10 Date: Tue, 2 Jun 2026 16:07:30 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=B2=D0=BA=D0=BB=D0=B0=D0=B4=D0=BA=D1=83=20=D0=B7=D0=B0=D1=8F?= =?UTF-8?q?=D0=B2=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/DATABASE.md | 6 +- docs/FRONTEND.md | 10 +- frontend/admin/css/components.css | 131 ++++++++++++++++- frontend/admin/css/layout.css | 84 ++++++++++- frontend/admin/index.html | 16 ++- frontend/admin/js/main.js | 55 ++++++- frontend/admin/js/views/teacher-requests.js | 136 ++++++++++++++++++ frontend/admin/js/views/users.js | 101 +------------ .../admin/views/department-workspace.html | 20 +-- frontend/admin/views/teacher-requests.html | 28 ++++ frontend/admin/views/users.html | 26 ---- 11 files changed, 456 insertions(+), 157 deletions(-) create mode 100644 frontend/admin/js/views/teacher-requests.js create mode 100644 frontend/admin/views/teacher-requests.html diff --git a/docs/DATABASE.md b/docs/DATABASE.md index 4fd4706..146d48d 100644 --- a/docs/DATABASE.md +++ b/docs/DATABASE.md @@ -777,11 +777,7 @@ Seed создаёт `Базовая сетка` (`DEFAULT`) и `Субботня | Файл | Описание | |------|----------| -<<<<<<< HEAD -| `V1__init.sql` | Инициализация: справочники, роли, refresh-сессии JWT, lifecycle-поля, история кафедр преподавателей, заявки кафедр на создание преподавателей, комментарии дисциплин, календарные учебные графики, динамическое расписание, версии/закрепления правил, точечные изменения расписания, тестовые правила, триггеры, комментарии | -======= -| `V1__init.sql` | Инициализация: справочники, роли, refresh-сессии JWT, lifecycle-поля, история кафедр преподавателей, комментарии дисциплин, календарные учебные графики, привязки дисциплин к графикам по семестрам, динамическое расписание, версии/закрепления правил, точечные изменения расписания, тестовые правила, триггеры, комментарии | ->>>>>>> 43d72c52356686ada2a54d73eccfdf491579fa0c +| `V1__init.sql` | Инициализация: справочники, роли, refresh-сессии JWT, lifecycle-поля, история кафедр преподавателей, заявки кафедр на создание преподавателей, комментарии дисциплин, календарные учебные графики, привязки дисциплин к графикам по семестрам, динамическое расписание, версии/закрепления правил, точечные изменения расписания, тестовые правила, триггеры, комментарии | ### Накатывание на существующих тенантов diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md index 5128e4a..ec13cd3 100644 --- a/docs/FRONTEND.md +++ b/docs/FRONTEND.md @@ -129,7 +129,8 @@ frontend/ | Tab | Описание | API | |-----|----------|-----| -| `users` | CRUD пользователей и очередь заявок кафедр на создание преподавателей | `/api/users`, `/api/teacher-requests` | +| `teacher-requests` | Очередь заявок кафедр на создание преподавателей с редактированием перед одобрением | `/api/teacher-requests`, `/api/departments` | +| `users` | CRUD пользователей | `/api/users` | | `groups` | CRUD групп, мультифильтр списка по формам обучения, настройка 0/2/3 подгрупп для лабораторных и назначения графиков | `/api/groups`, `/api/subgroups` | | `edu-forms` | Формы обучения | `/api/education-forms` | | `profiles` | Создание, редактирование и удаление профилей обучения специальностей | `/api/specialties/{id}/profiles` | @@ -146,14 +147,11 @@ frontend/ ### Особенности админских вкладок -<<<<<<< HEAD -- Вкладка `groups` загружает кафедры, специальности, профили, учебные годы и календарные графики. Список групп открывается через `/api/groups?includeArchived=true`, поэтому в таблице видны активные, будущие, завершившие обучение и архивные группы со статусом. Группа создаётся через `/api/groups` с `specialtyId` и `specialtyProfileId`, блок подгрупп использует `/api/subgroups` и `/api/groups/{id}/subgroups`, а блок назначений использует `/api/groups/{id}/calendar-assignments`. В селекты подгрупп и назначений попадают только группы с `active=true`. -- Вкладка `users` показывает pending-заявки кафедр на создание преподавателей. Администратор может скорректировать кафедру, логин, ФИО и должность, задать пароль минимум 8 символов, затем одобрить заявку через `/api/teacher-requests/{id}/approve` или отклонить её через `/api/teacher-requests/{id}/reject`. -======= - Вкладка `groups` загружает кафедры, специальности, профили, учебные годы и календарные графики. Список групп открывается через `/api/groups?includeArchived=true`, поэтому в таблице видны активные, будущие, завершившие обучение и архивные группы со статусом. Группа создаётся через `/api/groups` с `specialtyId` и `specialtyProfileId`, блок подгрупп использует `/api/subgroups` и `/api/groups/{id}/subgroups`, а блок назначений использует `/api/groups/{id}/calendar-assignments`. После назначения графика в таблице назначений сразу выводятся дисциплины графика, сгруппированные по номерам семестров. В селекты подгрупп и назначений попадают только группы с `active=true`. ->>>>>>> 43d72c52356686ada2a54d73eccfdf491579fa0c +- Вкладка `teacher-requests` показывает pending-заявки кафедр на создание преподавателей. Администратор может скорректировать кафедру, логин, ФИО и должность, задать пароль минимум 8 символов, затем одобрить заявку через `/api/teacher-requests/{id}/approve` или отклонить её через `/api/teacher-requests/{id}/reject`. Для роли `ADMIN` счётчик pending-заявок выводится в пункте меню «Заявки» и рядом с заголовком страницы, чтобы очередь была видна без открытия вкладки. - Вкладка `department-workspace` в блоке преподавателей объединяет данные `/api/department/teachers` и `/api/workload/teachers`: каждый преподаватель показывается одной карточкой с должностью и нагрузкой за выбранный период, преподаватели без занятий получают нулевую нагрузку, а преподаватели из расписания добавляются без дублей. Если дата начала периода выбрана позже даты окончания, поле окончания очищается, а расчёт нагрузки ждёт корректный период. - Вкладка `department-workspace` позволяет кафедре добавить существующего активного преподавателя на свою кафедру через `/api/department/teachers/{teacherId}/assignments`, отправить заявку на нового преподавателя через `/api/department/teacher-requests` и видеть статусы собственных заявок в таблице. +- Компоновка `department-workspace` использует собственные CSS-сетки `department-workspace-filter-grid` и `department-workspace-actions-grid`: фильтры периода отделены от сеток расписания, загрузка дисциплин занимает широкую колонку, формы преподавателей выравниваются справа, а списки и таблицы идут полноширинными блоками ниже. - Вкладка `schedule-view` показывает найденные занятия в режиме одной активной таблицы. Пользователь выбирает, что смотреть: группу, преподавателя, аудиторию или кафедру; frontend запрашивает двухнедельный диапазон от понедельника выбранной даты и собирает найденные расписания в переключатель результатов. На странице не выводится стек таблиц: виден один выбранный результат, а остальные доступны через чипы и кнопки предыдущего/следующего расписания; чипы результатов переносятся и отделены от счётчика стабильным отступом. Для режима кафедры и роли `DEPARTMENT` расписание ограничивается кафедрой пользователя; преподавательские и студенческие отдельные страницы пока остаются самостоятельными. Таблица строится как строки пар и столбцы дней недели. Нечётная неделя отображается в верхней половине ячейки, чётная — в нижней, а одинаковые занятия в обе недели схлопываются в цельную ячейку. Бейдж диапазона недель скрывается для занятий на весь семестр, а для занятий до конца семестра показывает только неделю начала в формате `(с 5 нед.)`. На мобильной ширине вместо широкой недельной матрицы показывается один день активного расписания с переключателем дней. - Вкладка `auditorium-workload` стала общей вкладкой `Загруженность`: в поле «Что смотреть» выбираются аудитории, преподаватели или кафедры. Сводная матрица по выбранной дате использует одинаковую структуру: строки — выбранный тип сущности, столбцы — эффективные временные слоты дня из `/api/admin/time-slots/effective`, занятость собирается из динамического расписания `/api/schedule` по группам. Кафедральная матрица группирует занятия по кафедре преподавателя. Для аудиторий доступны фильтры корпуса, вместимости и оборудования. В поле «Отображение» можно выбрать конкретную аудиторию, преподавателя или кафедру; тогда сводная матрица заменяется одной таблицей по дням недели и времени для двухнедельного периода от выбранной даты. Таблица выбранной сущности растягивается до нижней части экрана. Ячейка делится вертикально только если верхняя и нижняя недели отличаются: нечётная неделя отображается сверху, чётная — снизу. Если состояние или занятие одинаковое, ячейка остаётся цельной. Чётность берётся из расписания, а для свободных дней рассчитывается по семестрам из `/api/admin/calendar/years`. - Вкладка `profiles` выделена под профили обучения: администратор выбирает специальность, создаёт профиль, редактирует описание и удаляет неиспользуемые профили. diff --git a/frontend/admin/css/components.css b/frontend/admin/css/components.css index e8c9512..1892e41 100755 --- a/frontend/admin/css/components.css +++ b/frontend/admin/css/components.css @@ -385,6 +385,22 @@ align-items: end; } +.department-workspace-control-card { + display: grid; + gap: 1rem; +} + +.department-workspace-control-card .card-header-row { + margin-bottom: 0; +} + +.department-workspace-filter-grid { + display: grid; + grid-template-columns: minmax(260px, 1.35fr) repeat(2, minmax(180px, 0.65fr)); + gap: 1rem; + align-items: end; +} + .schedule-view-metrics, .department-workspace-grid { display: grid; @@ -397,7 +413,63 @@ } .department-workspace-grid-wide { - grid-template-columns: repeat(4, minmax(220px, 1fr)); + grid-template-columns: repeat(12, minmax(0, 1fr)); + align-items: stretch; +} + +.department-workspace-actions-grid { + gap: 1rem; +} + +.department-subject-import-card { + grid-column: span 7; + grid-row: span 2; +} + +.department-teacher-assign-card { + grid-column: span 5; +} + +.department-teacher-request-card { + grid-column: span 5; +} + +.department-teachers-card { + grid-column: 1 / -1; +} + +.department-subject-import-card, +.department-teacher-assign-card, +.department-teacher-request-card, +.department-teachers-card, +.department-workspace-table-card { + min-width: 0; +} + +.department-subject-import-card, +.department-teacher-assign-card, +.department-teacher-request-card { + display: flex; + flex-direction: column; +} + +.department-subject-import-card .stack-form, +.department-teacher-assign-card .stack-form, +.department-teacher-request-card .stack-form { + display: flex; + flex-direction: column; + flex: 1; +} + +.department-subject-import-card .subject-import-slots-panel { + flex: 1; +} + +.department-subject-import-card .stack-form > .btn, +.department-teacher-assign-card .stack-form > .btn, +.department-teacher-request-card .stack-form > .btn { + align-self: flex-start; + margin-top: auto; } .teacher-request-actions { @@ -407,6 +479,12 @@ min-width: 180px; } +.teacher-request-toolbar { + display: flex; + align-items: center; + gap: 0.75rem; +} + #teacher-requests-tbody input, #teacher-requests-tbody select { min-width: 150px; @@ -427,7 +505,6 @@ box-shadow: 0 0 0 3px var(--accent-glow); } -.department-subject-import-card, .department-teachers-card { display: flex; flex-direction: column; @@ -575,7 +652,7 @@ .subject-import-row { display: grid; - grid-template-columns: 160px minmax(260px, 1fr) auto; + grid-template-columns: minmax(120px, 0.7fr) minmax(240px, 1.3fr) auto; column-gap: 0.65rem; row-gap: 0.75rem; align-items: end; @@ -722,6 +799,10 @@ min-width: 0; } +.subject-import-row .btn { + min-height: 42px; +} + .subject-import-row .form-group label { margin-bottom: 0.45rem; } @@ -731,6 +812,14 @@ grid-template-columns: 1fr; } + .department-subject-import-card, + .department-teacher-assign-card, + .department-teacher-request-card, + .department-teachers-card { + grid-column: 1; + grid-row: auto; + } + .academic-year-semesters { grid-template-columns: 1fr; } @@ -740,6 +829,40 @@ } } +@media (max-width: 1280px) and (min-width: 901px) { + .department-workspace-grid-wide { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .department-subject-import-card, + .department-teachers-card { + grid-column: 1 / -1; + grid-row: auto; + } + + .department-teacher-assign-card, + .department-teacher-request-card { + grid-column: span 1; + } +} + +@media (max-width: 700px) { + .department-workspace-filter-grid { + grid-template-columns: 1fr; + } + + .subject-import-row { + grid-template-columns: 1fr; + } + + .department-subject-import-card .stack-form > .btn, + .department-teacher-assign-card .stack-form > .btn, + .department-teacher-request-card .stack-form > .btn, + .subject-import-row .btn { + width: 100%; + } +} + .academic-year-semester-meta { display: grid; gap: 0.2rem; @@ -1869,7 +1992,7 @@ @media (max-width: 1350px) { .schedule-view-filter-grid, .schedule-view-metrics, - .department-workspace-grid { + .department-workspace-grid:not(.department-workspace-grid-wide) { grid-template-columns: 1fr; } diff --git a/frontend/admin/css/layout.css b/frontend/admin/css/layout.css index 57bbc88..207a7d1 100755 --- a/frontend/admin/css/layout.css +++ b/frontend/admin/css/layout.css @@ -104,6 +104,7 @@ } .nav-item svg { + flex: 0 0 auto; transition: transform var(--transition); } @@ -112,6 +113,38 @@ transform: scale(1.15) rotate(-5deg); } +.nav-item span:not(.nav-badge) { + min-width: 0; +} + +.nav-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 22px; + margin-left: auto; + padding: 0 0.45rem; + border: 1px solid rgba(248, 113, 113, 0.28); + border-radius: 999px; + background: rgba(248, 113, 113, 0.16); + color: #fecaca; + font-size: 0.72rem; + font-weight: 800; + line-height: 1; +} + +.nav-badge[hidden], +.topbar-request-badge[hidden] { + display: none !important; +} + +[data-theme="light"] .nav-badge { + background: #fee2e2; + border-color: #fecaca; + color: #b91c1c; +} + .sidebar-footer { padding: 0.75rem; border-top: 1px solid var(--bg-card-border); @@ -247,7 +280,7 @@ display: none; } - .sidebar.collapsed .nav-item span, + .sidebar.collapsed .nav-item span:not(.nav-badge), .sidebar.collapsed .btn-settings span { position: absolute; left: calc(100% + 10px); @@ -271,13 +304,13 @@ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); } - [data-theme="light"] .sidebar.collapsed .nav-item span, + [data-theme="light"] .sidebar.collapsed .nav-item span:not(.nav-badge), [data-theme="light"] .sidebar.collapsed .btn-settings span { background: rgba(255, 255, 255, 0.95); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } - .sidebar.collapsed .nav-item:hover span, + .sidebar.collapsed .nav-item:hover span:not(.nav-badge), .sidebar.collapsed .btn-settings:hover span { opacity: 1; visibility: visible; @@ -298,6 +331,16 @@ padding: 0.75rem 0; overflow: visible; } + + .sidebar.collapsed .nav-badge { + position: absolute; + top: 0.35rem; + right: 0.35rem; + min-width: 18px; + height: 18px; + padding: 0 0.3rem; + font-size: 0.65rem; + } .sidebar.collapsed .btn-settings { justify-content: center; @@ -333,9 +376,44 @@ font-size: 1.3rem; font-weight: 700; letter-spacing: -0.02em; +} + +.topbar-title-group { + display: flex; + align-items: center; + gap: 0.75rem; + min-width: 0; flex: 1; } +.topbar-request-badge { + min-height: 30px; + padding: 0 0.75rem; + border: 1px solid rgba(248, 113, 113, 0.26); + border-radius: 999px; + background: rgba(248, 113, 113, 0.13); + color: #fecaca; + font: inherit; + font-size: 0.8rem; + font-weight: 800; + line-height: 1; + cursor: pointer; + white-space: nowrap; + transition: background var(--transition), border-color var(--transition), color var(--transition), transform var(--transition); +} + +.topbar-request-badge:hover { + transform: translateY(-1px); + background: rgba(248, 113, 113, 0.2); + border-color: rgba(248, 113, 113, 0.42); +} + +[data-theme="light"] .topbar-request-badge { + background: #fee2e2; + border-color: #fecaca; + color: #b91c1c; +} + .content { padding: 1.5rem 2rem; display: flex; diff --git a/frontend/admin/index.html b/frontend/admin/index.html index 6327494..32d95df 100755 --- a/frontend/admin/index.html +++ b/frontend/admin/index.html @@ -66,6 +66,17 @@ Дашборд + + + + + + + + + Заявки + + -

Загрузка...

+
+

Загрузка...

+ +
diff --git a/frontend/admin/js/main.js b/frontend/admin/js/main.js index 4fda5d3..bff3cf5 100755 --- a/frontend/admin/js/main.js +++ b/frontend/admin/js/main.js @@ -3,7 +3,7 @@ if (!['localhost', '127.0.0.1'].includes(window.location.hostname)) { import('./otel.js').catch(e => console.warn('OTel init skipped:', e.message)); } -import { isAuthenticatedAsAny, logout } from './api.js'; +import { api, isAuthenticatedAsAny, logout } from './api.js'; import { applyRippleEffect, closeAllDropdownsOnOutsideClick } from './utils.js'; import { startDropdownAutoObserver, initAllCustomDropdowns } from './dropdown.js'; @@ -38,6 +38,14 @@ const ROUTES = { await module.initUsers(); } }, + 'teacher-requests': { + title: 'Заявки', + file: '/admin/views/teacher-requests.html', + init: async () => { + const module = await import('./views/teacher-requests.js'); + await module.initTeacherRequests(); + } + }, groups: { title: 'Управление группами', file: '/admin/views/groups.html', @@ -115,7 +123,7 @@ const ROUTES = { const ROLE_NAVIGATION = { ADMIN: { defaultTab: 'dashboard', - tabs: ['dashboard', 'schedule', 'academic-calendar', 'auditorium-workload', 'schedule-view', 'classrooms', 'groups', 'university-structure', 'subjects', 'users'] + tabs: ['dashboard', 'teacher-requests', 'schedule', 'academic-calendar', 'auditorium-workload', 'schedule-view', 'classrooms', 'groups', 'university-structure', 'subjects', 'users'] }, EDUCATION_OFFICE: { defaultTab: 'dashboard', @@ -143,6 +151,8 @@ const menuToggle = document.getElementById('menu-toggle'); const sidebarCloseBtn = document.getElementById('sidebar-close-btn'); const btnLogout = document.getElementById('btn-logout'); const main = document.querySelector('.main'); +const teacherRequestsNavBadge = document.getElementById('teacher-requests-nav-badge'); +const teacherRequestsTitleBadge = document.getElementById('teacher-requests-title-badge'); const roleNavigation = ROLE_NAVIGATION[currentRole] || ROLE_NAVIGATION.SCHEDULE_VIEWER; const allowedTabs = new Set(roleNavigation.tabs); @@ -150,6 +160,7 @@ const allowedTabs = new Set(roleNavigation.tabs); applyRippleEffect(); closeAllDropdownsOnOutsideClick(); configureRoleInterface(); +refreshTeacherRequestBadge(); // Init sidebar state from localStorage on load if (window.innerWidth > 768 && localStorage.getItem('sidebar-collapsed') === 'true') { @@ -207,6 +218,10 @@ btnLogout.addEventListener('click', async () => { window.location.href = '/'; }); +teacherRequestsTitleBadge?.addEventListener('click', () => { + switchTab('teacher-requests'); +}); + // Navigation navItems.forEach(item => { item.addEventListener('click', (e) => { @@ -271,6 +286,42 @@ function configureRoleInterface() { document.querySelector('.logo span').textContent = firstAllowedTitle; } +async function refreshTeacherRequestBadge(countOverride = null) { + if (currentRole !== 'ADMIN') return; + try { + const count = countOverride === null + ? (await api.get('/api/teacher-requests?status=PENDING') || []).length + : countOverride; + updateTeacherRequestBadge(teacherRequestsNavBadge, count, count > 99 ? '99+' : String(count)); + updateTeacherRequestBadge(teacherRequestsTitleBadge, count, requestCountLabel(count)); + } catch (error) { + if (teacherRequestsNavBadge) teacherRequestsNavBadge.hidden = true; + if (teacherRequestsTitleBadge) teacherRequestsTitleBadge.hidden = true; + console.warn('Не удалось загрузить счётчик заявок:', error.message); + } +} + +function updateTeacherRequestBadge(element, count, label) { + if (!element) return; + const normalizedCount = Number(count) || 0; + element.textContent = label; + element.hidden = normalizedCount <= 0; + element.setAttribute('aria-hidden', normalizedCount <= 0 ? 'true' : 'false'); + element.setAttribute('aria-label', normalizedCount > 0 ? `Заявок в очереди: ${normalizedCount}` : 'Заявок в очереди нет'); +} + +function requestCountLabel(count) { + const mod10 = count % 10; + const mod100 = count % 100; + if (mod10 === 1 && mod100 !== 11) return `${count} заявка`; + if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return `${count} заявки`; + return `${count} заявок`; +} + +document.addEventListener('teacherRequestsChanged', (event) => { + refreshTeacherRequestBadge(Number.isFinite(event.detail?.count) ? event.detail.count : null); +}); + const hashTab = window.location.hash.replace('#', ''); switchTab(allowedTabs.has(hashTab) ? hashTab : roleNavigation.defaultTab); diff --git a/frontend/admin/js/views/teacher-requests.js b/frontend/admin/js/views/teacher-requests.js new file mode 100644 index 0000000..787b0db --- /dev/null +++ b/frontend/admin/js/views/teacher-requests.js @@ -0,0 +1,136 @@ +import { api } from '../api.js'; +import { escapeHtml, hideAlert, showAlert } from '../utils.js'; + +export async function initTeacherRequests() { + const requestsTbody = document.getElementById('teacher-requests-tbody'); + const refreshButton = document.getElementById('teacher-requests-refresh'); + const requestsCount = document.getElementById('teacher-requests-count'); + let departments = []; + + async function loadDepartments() { + departments = await api.get('/api/departments'); + } + + async function loadRequests() { + requestsTbody.innerHTML = 'Загрузка...'; + hideAlert('teacher-requests-alert'); + try { + const requests = await api.get('/api/teacher-requests?status=PENDING'); + const normalizedRequests = Array.isArray(requests) ? requests : []; + renderRequests(normalizedRequests); + updateCount(normalizedRequests.length); + document.dispatchEvent(new CustomEvent('teacherRequestsChanged', { + detail: { count: normalizedRequests.length } + })); + } catch (error) { + requestsTbody.innerHTML = `${escapeHtml(error.message || 'Ошибка загрузки заявок')}`; + updateCount(0); + } + } + + function renderRequests(requests) { + if (!requests.length) { + requestsTbody.innerHTML = 'Нет заявок на рассмотрении'; + return; + } + requestsTbody.innerHTML = requests.map(request => ` + + + + + + + + + ${escapeHtml(request.comment || '-')} + +
+ + +
+ + + `).join(''); + } + + function updateCount(count) { + if (!requestsCount) return; + requestsCount.textContent = requestCountLabel(count); + } + + function requestCountLabel(count) { + const mod10 = count % 10; + const mod100 = count % 100; + if (mod10 === 1 && mod100 !== 11) return `${count} заявка`; + if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return `${count} заявки`; + return `${count} заявок`; + } + + async function approveRequest(row, requestId) { + const payload = { + departmentId: Number(row.querySelector('.teacher-request-department')?.value), + username: row.querySelector('.teacher-request-username')?.value.trim(), + fullName: row.querySelector('.teacher-request-fullname')?.value.trim(), + jobTitle: row.querySelector('.teacher-request-jobtitle')?.value.trim(), + password: row.querySelector('.teacher-request-password')?.value + }; + if (!payload.departmentId || !payload.username || !payload.fullName || !payload.password) { + showAlert('teacher-requests-alert', 'Заполните кафедру, логин, ФИО и пароль', 'error'); + return; + } + if (payload.password.length < 8) { + showAlert('teacher-requests-alert', 'Пароль должен быть не короче 8 символов', 'error'); + return; + } + try { + await api.post(`/api/teacher-requests/${encodeURIComponent(requestId)}/approve`, payload); + showAlert('teacher-requests-alert', 'Преподаватель создан, заявка одобрена', 'success'); + await loadRequests(); + } catch (error) { + showAlert('teacher-requests-alert', error.message || 'Ошибка одобрения заявки', 'error'); + } + } + + async function rejectRequest(requestId) { + const reviewComment = prompt('Причина отклонения'); + if (reviewComment === null) return; + try { + await api.post(`/api/teacher-requests/${encodeURIComponent(requestId)}/reject`, { reviewComment }); + showAlert('teacher-requests-alert', 'Заявка отклонена', 'success'); + await loadRequests(); + } catch (error) { + showAlert('teacher-requests-alert', error.message || 'Ошибка отклонения заявки', 'error'); + } + } + + refreshButton?.addEventListener('click', loadRequests); + + requestsTbody.addEventListener('click', async (event) => { + const approveButton = event.target.closest('.teacher-request-approve'); + const rejectButton = event.target.closest('.teacher-request-reject'); + if (!approveButton && !rejectButton) return; + + const row = event.target.closest('tr[data-request-id]'); + const requestId = row?.dataset.requestId; + if (!requestId) return; + + if (approveButton) { + await approveRequest(row, requestId); + return; + } + await rejectRequest(requestId); + }); + + try { + await loadDepartments(); + await loadRequests(); + } catch (error) { + requestsTbody.innerHTML = `${escapeHtml(error.message || 'Ошибка загрузки')}`; + } +} diff --git a/frontend/admin/js/views/users.js b/frontend/admin/js/views/users.js index 92be2a7..d70ce31 100644 --- a/frontend/admin/js/views/users.js +++ b/frontend/admin/js/views/users.js @@ -21,9 +21,6 @@ const ROLE_BADGE = { export async function initUsers() { const usersTbody = document.getElementById('users-tbody'); const createForm = document.getElementById('create-form'); - const teacherRequestsTbody = document.getElementById('teacher-requests-tbody'); - const teacherRequestsRefresh = document.getElementById('teacher-requests-refresh'); - let departments = []; async function loadUsers() { try { @@ -36,53 +33,6 @@ export async function initUsers() { } } - async function loadDepartments() { - departments = await api.get('/api/departments'); - } - - async function loadTeacherRequests() { - if (!teacherRequestsTbody) return; - teacherRequestsTbody.innerHTML = 'Загрузка...'; - hideAlert('teacher-requests-alert'); - try { - const requests = await api.get('/api/teacher-requests?status=PENDING'); - renderTeacherRequests(Array.isArray(requests) ? requests : []); - } catch (error) { - teacherRequestsTbody.innerHTML = `${escapeHtml(error.message || 'Ошибка загрузки заявок')}`; - } - } - - function renderTeacherRequests(requests) { - if (!requests.length) { - teacherRequestsTbody.innerHTML = 'Нет заявок на рассмотрении'; - return; - } - teacherRequestsTbody.innerHTML = requests.map(request => ` - - - - - - - - - ${escapeHtml(request.comment || '-')} - -
- - -
- - - `).join(''); - } - function renderUsers(users) { if (!users || !users.length) { usersTbody.innerHTML = 'Нет пользователей'; @@ -143,54 +93,6 @@ export async function initUsers() { } }); - teacherRequestsRefresh?.addEventListener('click', loadTeacherRequests); - - teacherRequestsTbody?.addEventListener('click', async (event) => { - const approveButton = event.target.closest('.teacher-request-approve'); - const rejectButton = event.target.closest('.teacher-request-reject'); - if (!approveButton && !rejectButton) return; - - const row = event.target.closest('tr[data-request-id]'); - const requestId = row?.dataset.requestId; - if (!requestId) return; - - if (approveButton) { - const payload = { - departmentId: Number(row.querySelector('.teacher-request-department')?.value), - username: row.querySelector('.teacher-request-username')?.value.trim(), - fullName: row.querySelector('.teacher-request-fullname')?.value.trim(), - jobTitle: row.querySelector('.teacher-request-jobtitle')?.value.trim(), - password: row.querySelector('.teacher-request-password')?.value - }; - if (!payload.departmentId || !payload.username || !payload.fullName || !payload.password) { - showAlert('teacher-requests-alert', 'Заполните кафедру, логин, ФИО и пароль', 'error'); - return; - } - if (payload.password.length < 8) { - showAlert('teacher-requests-alert', 'Пароль должен быть не короче 8 символов', 'error'); - return; - } - try { - await api.post(`/api/teacher-requests/${encodeURIComponent(requestId)}/approve`, payload); - showAlert('teacher-requests-alert', 'Преподаватель создан, заявка одобрена', 'success'); - await Promise.all([loadTeacherRequests(), loadUsers()]); - } catch (error) { - showAlert('teacher-requests-alert', error.message || 'Ошибка одобрения заявки', 'error'); - } - return; - } - - const reviewComment = prompt('Причина отклонения'); - if (reviewComment === null) return; - try { - await api.post(`/api/teacher-requests/${encodeURIComponent(requestId)}/reject`, { reviewComment }); - showAlert('teacher-requests-alert', 'Заявка отклонена', 'success'); - await loadTeacherRequests(); - } catch (error) { - showAlert('teacher-requests-alert', error.message || 'Ошибка отклонения заявки', 'error'); - } - }); - usersTbody.addEventListener('click', async (e) => { const deleteBtn = e.target.closest('.btn-archive-user'); if (!deleteBtn) return; @@ -216,6 +118,5 @@ export async function initUsers() { } }); - await loadDepartments(); - await Promise.all([loadUsers(), loadTeacherRequests()]); + await loadUsers(); } diff --git a/frontend/admin/views/department-workspace.html b/frontend/admin/views/department-workspace.html index d85a4b8..8f4d13a 100644 --- a/frontend/admin/views/department-workspace.html +++ b/frontend/admin/views/department-workspace.html @@ -1,10 +1,10 @@ -
+

Рабочая область кафедры

-
+