diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md index af6fca0..a793505 100644 --- a/docs/FRONTEND.md +++ b/docs/FRONTEND.md @@ -146,7 +146,7 @@ frontend/ - Компоновка `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`. -- Вкладка `university-structure` содержит внутренние разделы `Кафедры`, `Специальности` и `Профили`: администратор создаёт профили как из общего списка, так и через кнопку `Профили` у конкретной специальности. +- Вкладка `university-structure` содержит внутренние разделы `Кафедры`, `Специальности` и `Профили`, оформленные тем же визуальным паттерном вкладок, что и `academic-calendar`: администратор создаёт профили как из общего списка, так и через кнопку `Профили` у конкретной специальности. - Вкладка `schedule` не обращается к старым `lessons` API. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы, отдельные часы и недели начала для лекций, лабораторных и практик, а также набор базовых слотов. Группы выбираются через выпадающий мультиселект. Поле подгруппы появляется только при выборе лабораторной работы; для лекций и практик оно не отображается. Если в правиле выбрана одна группа, селект подгруппы содержит пункт `Вся группа`; если выбрано несколько групп, лабораторный слот показывает мультиселект подгрупп, чтобы выбрать разные подгруппы разных групп. Типы занятий в слоте сортируются в порядке: лекция, лабораторная работа, практика. Список слотов отображается без внутреннего вертикального скролла: при добавлении строк форма расширяется вниз, а кнопка сохранения остаётся отдельным блоком под слотами. Из календарной системы здесь используется список семестров для выбора периода действия правила. Справа доступна сворачиваемая визуальная матрица: пользователь выбирает учебный год, семестр и группы, после чего матрица строится только по правилам выбранного семестра. Столбцы — выбранные в фильтре группы, строки — только день и время, где есть активные пары, ячейки показывают дисциплину, диапазон недель, тип, формат, преподавателя, аудиторию и подгруппы. Период недель не показывается для занятия на весь семестр; если занятие идёт до конца семестра не с первой недели, выводится только неделя начала в формате `(с 5 нед.)`, а ограниченный диапазон — как `(с 1 по 3 нед.)`. Если нечётная и чётная недели отличаются, ячейка делится на две половины; одинаковые занятия схлопываются в цельную ячейку. Кнопка с тремя точками в правой части карточки пары открывает контекстное меню: можно открыть полное правило в форме, изменить только день и базовую пару выбранного слота через компактную модалку или удалить правило целиком. В списке правил действия отображаются едиными кнопками одинакового размера с отступами между ними. - Вкладка `academic-calendar` полностью отделяет календарную систему от расписания занятий и внутри себя разделена на три вкладки: `Графики` для учебных годов, семестров и карточек календарных графиков, `Сетки` для редактора дневной сетки, `Дисциплины` для ручной привязки дисциплин из `/api/subjects` к номерам учебных семестров графика. Администратор выбирает форму обучения из общего справочника `/api/education-forms`, заполняет дневную сетку по курсам и кодам активностей, назначает ручную временную сетку на конкретную дату, а сохранение сетки идёт через `/api/admin/academic-calendars/{id}/grid`. - При сохранении правила во вкладке `schedule` frontend различает `409 Conflict` от остальных ошибок API. Если backend возвращает `conflictRule`, открывается широкое модальное окно разрешения конфликта: пользователь видит новое и ранее созданное правило, чипы причины конфликта из `conflictReasons` и подсветку соответствующих полей (`teacher`, `classroom`, `group`). Пользователь меняет у конфликтующего правила день, чётность, пару, преподавателя или аудиторию, после чего frontend сохраняет конфликтующее правило через `PUT /api/admin/schedule-rules/{id}` и автоматически повторяет исходный `POST` или `PUT`. Если после переноса появляется следующий конфликт, показывается следующее конфликтующее правило без потери исходного черновика. diff --git a/frontend/admin/css/components.css b/frontend/admin/css/components.css index 1e041c3..9cb01b8 100755 --- a/frontend/admin/css/components.css +++ b/frontend/admin/css/components.css @@ -1624,7 +1624,8 @@ } } -.academic-calendar-tabs { +.academic-calendar-tabs, +.university-structure-tabs { display: flex; flex-wrap: wrap; gap: 0.4rem; @@ -1632,7 +1633,8 @@ border-bottom: 1px solid var(--bg-card-border); } -.academic-calendar-tab { +.academic-calendar-tab, +.university-structure-tab { min-height: 40px; padding: 0.55rem 0.9rem; border: 1px solid transparent; @@ -1647,24 +1649,29 @@ } .academic-calendar-tab:hover, -.academic-calendar-tab:focus-visible { +.academic-calendar-tab:focus-visible, +.university-structure-tab:hover, +.university-structure-tab:focus-visible { color: var(--text-primary); background: var(--bg-hover); outline: none; } -.academic-calendar-tab.active { +.academic-calendar-tab.active, +.university-structure-tab.active { color: var(--accent-hover); background: var(--bg-card); border-color: var(--bg-card-border); } -.academic-calendar-tab-panel { +.academic-calendar-tab-panel, +.university-structure-tab-panel { display: grid; gap: 1rem; } -.academic-calendar-tab-panel[hidden] { +.academic-calendar-tab-panel[hidden], +.university-structure-tab-panel[hidden] { display: none; } @@ -2360,7 +2367,8 @@ grid-template-columns: 1fr; } - .academic-calendar-tab { + .academic-calendar-tab, + .university-structure-tab { flex: 1 1 100%; border-radius: var(--radius-sm); border: 1px solid var(--bg-card-border); diff --git a/frontend/admin/js/views/university-structure.js b/frontend/admin/js/views/university-structure.js index 18e81b0..c2ae22d 100644 --- a/frontend/admin/js/views/university-structure.js +++ b/frontend/admin/js/views/university-structure.js @@ -53,41 +53,26 @@ export async function initUniversityStructure() { let allProfiles = []; // --- Логика переключения табов --- - function deactivateTabs() { - [tabDepartments, tabSpecialties, tabProfiles].forEach(tab => { - if (tab) { - tab.classList.remove('active'); - tab.style.borderBottomColor = 'transparent'; - tab.style.color = 'var(--text-secondary)'; + const structureTabs = [ + { button: tabDepartments, panel: contentDepartments }, + { button: tabSpecialties, panel: contentSpecialties }, + { button: tabProfiles, panel: contentProfiles }, + ]; + + function activateStructureTab(activeButton) { + structureTabs.forEach(({ button, panel }) => { + const active = button === activeButton; + button?.classList.toggle('active', active); + button?.setAttribute('aria-selected', active ? 'true' : 'false'); + if (panel) { + panel.classList.toggle('active', active); + panel.hidden = !active; } }); - [contentDepartments, contentSpecialties, contentProfiles].forEach(content => { - if (content) content.style.display = 'none'; - }); } - tabDepartments.addEventListener('click', () => { - deactivateTabs(); - tabDepartments.classList.add('active'); - tabDepartments.style.borderBottomColor = 'var(--primary)'; - tabDepartments.style.color = 'var(--text-main)'; - contentDepartments.style.display = 'block'; - }); - - tabSpecialties.addEventListener('click', () => { - deactivateTabs(); - tabSpecialties.classList.add('active'); - tabSpecialties.style.borderBottomColor = 'var(--primary)'; - tabSpecialties.style.color = 'var(--text-main)'; - contentSpecialties.style.display = 'block'; - }); - - tabProfiles.addEventListener('click', () => { - deactivateTabs(); - tabProfiles.classList.add('active'); - tabProfiles.style.borderBottomColor = 'var(--primary)'; - tabProfiles.style.color = 'var(--text-main)'; - contentProfiles.style.display = 'block'; + structureTabs.forEach(({ button }) => { + button?.addEventListener('click', () => activateStructureTab(button)); }); // --- Загрузка данных --- diff --git a/frontend/admin/views/university-structure.html b/frontend/admin/views/university-structure.html index 44ce3e6..2526749 100644 --- a/frontend/admin/views/university-structure.html +++ b/frontend/admin/views/university-structure.html @@ -1,13 +1,13 @@