Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на React 19 (модель strangler, docs/REACT_MIGRATION.md). - 15 вкладок админки: lazy-загрузка, общий error boundary, сервисный слой - Удалены legacy-views и переходный слой LegacyTabView (45 файлов) - Сборка esbuild (frontend/scripts/build-react.mjs) с бюджетами gzip на чанки - CSP-хэши, кэширование: chunk'и immutable, CSS/HTML no-cache (react-cache.conf) - Фиксы: lazy-импорт именованных экспортов (краш после авторизации), растяжение #admin-root/#settings-root на всю ширину, возврат subjects/lessonTypes из справочников (краш просмотра расписаний), отказ от заведомо широкого поиска расписания без выбранной цели, центрирование карточки логина - Тесты: 237/237 (npm run check), регрессионные на lazy-вкладки и ширину - Документация: REACT_MIGRATION.md, REACT_MIGRATION_RESULT.md, FRONTEND.md
631 lines
90 KiB
Markdown
631 lines
90 KiB
Markdown
# 🎨 Frontend
|
||
|
||
## Общая информация
|
||
|
||
| Параметр | Значение |
|
||
|----------|----------|
|
||
| **Фреймворк** | Переходный режим: React 19 для страницы входа, кабинетов студента и преподавателя, страницы настроек и общего слоя `react/shared`, Vanilla JavaScript для остальных разделов |
|
||
| **Модульная система** | ES6 Modules (`import`/`export`) |
|
||
| **Стили** | CSS (модульный подход) |
|
||
| **Шрифт** | Системный стек без внешних font-CDN |
|
||
| **Веб-сервер** | Apache httpd на Alpine со строгим CSP |
|
||
|
||
Пошаговый план и актуальная точка продолжения находятся в
|
||
[`REACT_MIGRATION.md`](REACT_MIGRATION.md). Миграция выполняется по целым страницам и
|
||
разделам без изменения действующих URL и REST-контрактов.
|
||
|
||
---
|
||
|
||
## Структура файлов
|
||
|
||
```
|
||
frontend/
|
||
├── package.json # Exact build-зависимости и команды проверки
|
||
├── package-lock.json # Зафиксированное npm-дерево
|
||
├── auth-session.js # Access JWT и профиль сессии только в памяти вкладки
|
||
├── telemetry.js # Same-origin загрузчик собранного OTel bundle
|
||
├── schedule-overview.js # Общий расчёт и рендер «Сегодня / Следующая пара»
|
||
├── schedule-overview.css # Общие карточки ближайших занятий и поискового выбора группы
|
||
├── security.conf # CSP и защитные HTTP-заголовки Apache
|
||
├── react-cache.conf # Cache-Control: immutable для chunk'ов, no-cache для entrypoint, CSS и HTML
|
||
├── telemetry/
|
||
│ └── otel-entry.js # Исходная точка сборки OpenTelemetry
|
||
├── scripts/
|
||
│ ├── build-react.mjs # Production-сборка React entrypoint и проверка gzip-бюджета
|
||
│ ├── build-react-tests.mjs # Сборка компонентных проверок для node:test
|
||
│ └── build-vendor.mjs # Сборка `/vendor/otel.js` через esbuild
|
||
├── react/
|
||
│ ├── login/
|
||
│ │ ├── LoginApp.jsx # React-компонент страницы входа
|
||
│ │ ├── login-service.js # Валидация и контракт POST /api/auth/login
|
||
│ │ └── main.jsx # React entrypoint для `/`
|
||
│ ├── student/ # Кабинет студента на React
|
||
│ │ ├── StudentApp.jsx # Сессия, выбор группы, неделя и состояния
|
||
│ │ ├── WeekGrid.jsx # Недельная сетка дней и занятий
|
||
│ │ ├── student-service.js # Чипы, статусы и контракты API кабинета студента
|
||
│ │ ├── main.jsx # React entrypoint для `/student/`
|
||
│ │ └── testing/ # render-student.jsx для DOM-проверок
|
||
│ ├── teacher/ # Кабинет преподавателя на React
|
||
│ │ ├── TeacherApp.jsx # Сессия, вкладки, неделя и состояния
|
||
│ │ ├── TeacherWeekGrid.jsx # Матрица слотов и мобильный список дней
|
||
│ │ ├── AbsencePanel.jsx # Заявки об отсутствии
|
||
│ │ ├── PreferencesTab.jsx # Семестровый календарь доступности
|
||
│ │ ├── RequestsTab.jsx # Журнал заявок на изменение пар
|
||
│ │ ├── RequestDialog.jsx # Диалог переноса/аудитории/отмены
|
||
│ │ ├── teacher-service.js # Слоты, матрица недели, метки и контракты API
|
||
│ │ ├── main.jsx # React entrypoint для `/teacher/`
|
||
│ │ └── testing/ # render-teacher.jsx для DOM-проверок
|
||
│ ├── settings/ # Страница настроек на React
|
||
│ │ ├── SettingsApp.jsx # Сессия, боковая панель, вкладки и тема
|
||
│ │ ├── GeneralTab.jsx # Заглушка общих настроек
|
||
│ │ ├── TimeSlotsTab.jsx # Сетки времени и CRUD временных слотов
|
||
│ │ ├── EduFormsTab.jsx # Формы обучения
|
||
│ │ ├── DatabaseTab.jsx # Статус подключения и тенанты
|
||
│ │ ├── settings-service.js # Метки сеток, валидация и контракты API
|
||
│ │ ├── main.jsx # React entrypoint для `/admin/settings/`
|
||
│ │ └── testing/ # render-settings.jsx для DOM-проверок
|
||
│ ├── admin/ # Оболочка основной админ-панели на React
|
||
│ │ ├── AdminApp.jsx # Сессия, sidebar с секциями, topbar, hash-router
|
||
│ │ ├── react-tabs.js # Реестр всех вкладок админ-панели (lazy + Suspense)
|
||
│ │ ├── admin-service.js # Маршруты, секции навигации, склонения, контракты API
|
||
│ │ ├── classrooms-service.js # Статусы, оборудование и контракты API аудиторий
|
||
│ │ ├── subjects-service.js # Состояние реестра, URL-параметры и контракты API дисциплин
|
||
│ │ ├── dashboard-service.js # Метрики, слоты, свободные аудитории и конфликты Red Zone
|
||
│ │ ├── users-service.js # Роли, состояние реестра и контракты API пользователей
|
||
│ │ ├── groups-service.js # Подгруппы, календарные графики и контракты API групп
|
||
│ │ ├── university-structure-service.js # Фильтрация списков и контракты API кафедр/специальностей/профилей
|
||
│ │ ├── teacher-requests-service.js # Состояние реестра, валидация одобрения и контракты API заявок
|
||
│ │ ├── department-workspace-service.js # Нагрузка, payload-валидация и контракты API кабинета кафедры
|
||
│ │ ├── auditorium-workload-service.js # Дедупликация занятий, чётность недель и контракты API загруженности
|
||
│ │ ├── academic-calendar-service.js # Учебные годы, сетка графика, диалог ячейки и контракты API календаря
|
||
│ │ ├── schedule-view-service.js # Разделы расписания, семестровая матрица и контракты API просмотра
|
||
│ │ ├── schedule-service.js # Слоты правил, payload-валидация, визуальная матрица и контракты API конструктора
|
||
│ │ ├── schedule-override-service.js # Источник правки, календарь семестра и контракты API разовых правок
|
||
│ │ ├── schedule-versions-service.js # Группировка версий, подписи журнала и контракты API контура публикаций
|
||
│ │ ├── schedule-quality-service.js # Диапазоны оценки, фильтры проблем и контракт API анализа качества
|
||
│ │ ├── teacher-absences-service.js # Решения мастера замены, подписи статусов и контракты API отсутствий
|
||
│ │ ├── tabs/ # Вкладки админ-панели (волны 1–6, все вкладки на React)
|
||
│ │ │ ├── DashboardTab.jsx # Метрики, мониторинг пар, Red Zone
|
||
│ │ │ ├── ClassroomsTab.jsx # Аудиторный фонд с мультиселектом оборудования
|
||
│ │ │ ├── SubjectsTab.jsx # Реестр дисциплин с пагинацией и преподавателями
|
||
│ │ │ ├── UsersTab.jsx # Реестр пользователей с фильтром роли и архивированием
|
||
│ │ │ ├── GroupsTab.jsx # Реестр групп, подгруппы и календарные графики
|
||
│ │ │ ├── UniversityStructureTab.jsx # Кафедры, специальности и профили обучения
|
||
│ │ │ ├── TeacherRequestsTab.jsx # Реестр заявок с редактируемыми строками и отклонением через PromptDialog
|
||
│ │ │ ├── DepartmentWorkspaceTab.jsx # Кабинет кафедры: дисциплины, преподаватели, нагрузка, заявки
|
||
│ │ │ ├── AuditoriumWorkloadTab.jsx # Загруженность: сводная матрица и двухнедельная сводка сущности
|
||
│ │ │ ├── AcademicCalendarTab.jsx # Календарные графики: годы, сетки, дисциплины (MaskedDateInput)
|
||
│ │ │ ├── ScheduleViewTab.jsx # Просмотр расписаний: фильтры, разделы, матрица и панель правок
|
||
│ │ │ ├── ScheduleTab.jsx # Конструктор правил: каскад версий, форма правила и визуальная матрица
|
||
│ │ │ ├── ScheduleVersionsTab.jsx # Контур публикаций: черновики, diff, архив, журнал и модальные операции
|
||
│ │ │ ├── ScheduleQualityTab.jsx # Анализ качества: скорборд, метрики, карта проблем и панель правила
|
||
│ │ │ └── TeacherAbsencesTab.jsx # Отсутствия: реестр, мастер замены, пожелания и заявки на изменение
|
||
│ │ ├── main.jsx # React entrypoint для `/admin/`
|
||
│ │ └── testing/ # render-admin.jsx для DOM-проверок
|
||
│ └── shared/ # Общий React-слой для следующих переносимых страниц
|
||
│ ├── api-client.js # requestJson/apiClient поверх auth-транспорта
|
||
│ ├── auth-client.js # Восстановление сессии и logout с redirect
|
||
│ ├── dates.js # Дата-математика (недели, ISO, форматирование)
|
||
│ ├── session-policy.js # Фазы loading/ready/unauthorized/error и русские тексты
|
||
│ ├── use-authorized-session.js # Хук проверки роли при монтировании страницы
|
||
│ ├── runtime-entry.jsx # Единая точка экспорта общего runtime-chunk
|
||
│ ├── ui/ # AppErrorBoundary, AsyncState, FormAlert, AsyncCombobox, ScheduleOverview, ConfirmDialog, Pagination
|
||
│ └── testing/ # render-shared.jsx для DOM-проверок через renderToStaticMarkup
|
||
├── tests/
|
||
│ ├── academic-calendar-grid.test.mjs # Неполная первая неделя календарного графика
|
||
│ ├── academic-calendar-title.test.mjs # Составное название календарного графика
|
||
│ ├── academic-calendar-tooltip.test.mjs # Позиционирование подсказки возле курсора и границ viewport
|
||
│ ├── schedule-overview.test.mjs # Текущее занятие, список на сегодня и следующая пара
|
||
│ ├── admin-ui-regressions.test.mjs # Заголовки, меню, действия слотов и компоновка вкладок
|
||
│ ├── auth-session.test.mjs # Login/refresh/reload/logout и single-flight refresh
|
||
│ ├── react-login.test.mjs # Валидация и API-контракт React-формы входа
|
||
│ ├── react-shared.test.mjs # Общая политика сессии, API-клиент и DOM-проверки состояний
|
||
│ ├── react-student.test.mjs # Дата-математика, чипы, контракты API и DOM-проверки кабинета студента
|
||
│ ├── react-teacher.test.mjs # Слоты, матрица недели, метки, payload заявок и DOM-проверки кабинета преподавателя
|
||
│ ├── react-settings.test.mjs # Вкладки по ролям, метки сеток, валидация и контракты API страницы настроек
|
||
│ ├── react-admin.test.mjs # Навигация по ролям, hash-маршруты, склонения и DOM-проверки оболочки админки
|
||
│ ├── react-admin-tabs.test.mjs # Сервисы и DOM-проверки вкладок волн 1–5 (дашборд, аудитории, дисциплины, пользователи, группы, структура вуза, заявки, кабинет кафедры, загруженность, календарные графики, вкладки расписания)
|
||
│ ├── dashboard-conflicts.test.mjs # Регрессии дат и состояний проверки конфликтов
|
||
│ ├── schedule-overrides.test.mjs # Действия, роли, недельный выбор и подбор времени разовой правки
|
||
│ ├── schedule-quality.test.mjs # Метрики качества, фильтры проблем и доступность вкладки
|
||
│ ├── schedule-versions.test.mjs # Статусы версий, журнал и навигация в черновик/качество
|
||
│ ├── schedule-view-semesters.test.mjs # Выбор семестра и расчёт двухнедельного диапазона просмотра
|
||
│ ├── teacher-absences.test.mjs # Payload мастера замены и доступность вкладки по ролям
|
||
│ ├── teacher-preferences.test.mjs # Календарь пожеланий, заявки и подсказки конструктора
|
||
│ └── security-policy.test.mjs # Web Storage, XSS, пароли, язык, CSP и Dockerfile
|
||
├── index.html # 🔐 Страница авторизации (общая)
|
||
├── style.css # Стили страницы авторизации
|
||
├── theme-toggle.js # Переключение светлой/тёмной темы
|
||
├── Dockerfile # Multi-stage: npm bundle → Apache httpd
|
||
│
|
||
├── admin/ # 👨💼 Интерфейс администратора
|
||
│ ├── index.html # Монтирует React-оболочку `/react/admin.js`
|
||
│ ├── css/
|
||
│ │ ├── main.css # CSS-переменные, цвета, типографика
|
||
│ │ ├── layout.css # Раскладка (sidebar, topbar, content)
|
||
│ │ ├── components.css # Кнопки, таблицы, карточки, формы
|
||
│ │ ├── modals.css # Модальные окна
|
||
│ │ ├── auditorium-workload.css # Таблицы расписаний и загруженности
|
||
│ │ ├── departments-data.css # Стили создания кафедры/специальности
|
||
│ │ ├── teacher-absences.css # Отсутствия, пожелания и очередь заявок преподавателей
|
||
│ │ ├── schedule-quality.css # Диагностический экран качества расписания
|
||
│ │ └── schedule-versions.css # Контур публикации, diff и журнал версий
|
||
│ ├── js/
|
||
│ │ ├── role-capabilities.js # Единая матрица вкладок admin/settings по ролям
|
||
│ │ ├── api.js # HTTP-обёртка (fetch + Authorization)
|
||
│ │ ├── dashboard-conflicts.js # Чистые функции дат, загрузки и состояний Red Zone
|
||
│ │ ├── date-input.js # Маска ДД.ММ.ГГГГ и преобразование дат в ISO
|
||
│ │ ├── dialog.js # Доступные подтверждения, сообщения и ввод причины
|
||
│ │ ├── dirty-state.js # Защита несохранённых изменений
|
||
│ │ ├── url-state.js # Чтение и запись фильтров в query string
|
||
│ │ ├── view-state.js # Единые loading/empty/error-состояния таблиц
|
||
│ │ ├── academic-period.js # Каскад учебного года и семестра в профильных вкладках
|
||
│ │ ├── utils.js # Утилиты
|
||
│ │ ├── schedule-period.js # Выбор ближайшего учебного периода для расписаний и нагрузки
|
||
│ │ └── views/ # Общие чистые хелперы (используются React-сервисами)
|
||
│ │ ├── academic-calendar-grid.js # Расчёт ISO-недели дневной сетки (общий с React-сервисом)
|
||
│ │ └── academic-calendar-title.js # Название из кода, профиля, формы и года (общий с React-сервисом)
|
||
│ │
|
||
│ └── settings/ # ⚙️ Страница настроек (отдельный SPA на React)
|
||
│ ├── index.html # Оболочка с корневым элементом settings-root
|
||
│ └── css/
|
||
│ ├── main.css # CSS-переменные, базовые стили
|
||
│ └── layout.css # Sidebar, topbar, content
|
||
│
|
||
├── teacher/ # 👩🏫 Интерфейс преподавателя (React)
|
||
│ ├── index.html # Оболочка с корневым элементом teacher-root
|
||
│ └── style.css # Адаптивный кабинет в стиле диспетчерского журнала
|
||
├── department/ # 🏛 Кабинет кафедры
|
||
│ └── index.html # Redirect в `/admin/#department-workspace`
|
||
├── edu-office/ # 🗓 Кабинет учебного отдела
|
||
│ └── index.html # Redirect в `/admin/#schedule-view`
|
||
│
|
||
└── student/ # 🎓 Интерфейс студента (React)
|
||
├── index.html # Оболочка с корневым элементом student-root
|
||
└── style.css # Стили кабинета без inline-блока
|
||
```
|
||
|
||
---
|
||
|
||
## Система маршрутизации (Admin SPA)
|
||
|
||
Оболочка админ-панели работает на **React** (`react/admin/AdminApp.jsx`, entrypoint
|
||
`/react/admin.js`): sidebar с секциями, topbar с заголовком вкладки и счётчиком заявок,
|
||
тема, сворачивание панели, настройки с выходом и hash-router.
|
||
|
||
Навигация реализована через hash-адреса вкладок (`#users`, `#schedule` и т. д.):
|
||
|
||
- при старте активная вкладка читается из `location.hash` (неразрешённые роли
|
||
значения заменяются вкладкой по умолчанию);
|
||
- слушатель `hashchange` синхронизирует вкладку с адресной строкой (работают
|
||
кнопки «назад/вперёд» и ссылки из других кабинетов);
|
||
- переключение идёт через `requestTabSwitch` с guard'ом несохранённых изменений
|
||
из общего модуля `admin/js/dirty-state.js`.
|
||
|
||
Содержимое вкладки выбирается по реестру `react/admin/react-tabs.js`:
|
||
|
||
- **все 15 вкладок админ-панели** (волна 1: `dashboard`, `classrooms`, `subjects`;
|
||
волна 2: `users`, `groups`, `university-structure`; волна 3:
|
||
`teacher-requests`, `department-workspace`; волна 4: `auditorium-workload`,
|
||
`academic-calendar`; волна 5: `schedule-view`, `schedule-versions`,
|
||
`schedule-quality`, `teacher-absences`; волна 6: `schedule`)
|
||
рендерятся React-компонентами из `react/admin/tabs/`, каждый загружается
|
||
отдельным lazy-chunk'ом (`React.lazy` + `Suspense`, fallback — индикатор
|
||
`tab-loading`);
|
||
- переходный слой `LegacyTabView` + `legacy-views.js` и каталоги
|
||
`admin/views/*.html` / `admin/js/views/{tab}.js` удалены после волны 6 —
|
||
HTML-фрагменты больше не загружаются через `fetch`, в `admin/js/views/`
|
||
остались только общие чистые хелперы календарных графиков.
|
||
|
||
React-оболочка админки (`react/admin/admin-service.js`) и React-оболочка настроек
|
||
(`react/settings/SettingsApp.jsx`) получают разрешённые вкладки из единого
|
||
`admin/js/role-capabilities.js`; локальные дубли `ROLE_NAVIGATION`/`ROLE_TABS` удалены.
|
||
|
||
| Роль | Доступные вкладки |
|
||
|------|-------------------|
|
||
| `ADMIN` | Все вкладки |
|
||
| `EDUCATION_OFFICE` | Просмотр, конструктор и анализ качества расписаний, отсутствия и замены, календарный график, загруженность и аудитории; в settings — временные слоты и формы обучения |
|
||
| `DEPARTMENT` | Кабинет кафедры, просмотр расписаний, отсутствия и подтверждение заявок своих преподавателей |
|
||
| `SCHEDULE_VIEWER` | Только просмотр расписаний |
|
||
|
||
Пути `/department/` и `/edu-office/` оставлены как входные redirect-страницы в общую панель. Отдельные кабинеты не дублируют UI админ-панели.
|
||
|
||
Пункты меню, недоступные роли, не попадают в sidebar: React-оболочка фильтрует
|
||
секции навигации по матрице ролей до рендера. Верхние панели основной админки и
|
||
settings SPA не содержат неработающей кнопки бургер-меню. В светлой теме пункты и
|
||
заголовки sidebar получают контрастные тёмные цвета для всех ролей, включая
|
||
`DEPARTMENT`.
|
||
|
||
### Разделы админ-панели
|
||
|
||
| Tab | Описание | API |
|
||
|-----|----------|-----|
|
||
| `dashboard` | Сводные метрики и проверка конфликтов текущей недели с явным статусом полноты данных | `/api/departments`, `/api/classrooms`, `/api/groups`, `/api/users/teachers`, `/api/schedule/search`, `/api/admin/time-slots` |
|
||
| `teacher-requests` | Очередь заявок кафедр на создание преподавателей с поиском, статусом, пагинацией и редактированием перед одобрением | `/api/teacher-requests/page`, `/api/departments` |
|
||
| `users` | CRUD пользователей с серверными поиском, ролевым фильтром, сортировкой и пагинацией | `/api/users/page` |
|
||
| `groups` | CRUD групп, серверный поиск и пагинация, мультифильтр по формам обучения, настройка 0/2/3 подгрупп и поиск совместимого графика | `/api/groups/page`, `/api/groups/options`, `/api/admin/academic-calendars/options`, `/api/groups/{id}/subgroups` |
|
||
| `classrooms` | Аудитории | `/api/classrooms` |
|
||
| `subjects` | Дисциплины с серверным реестром, привязками только для текущей страницы и асинхронным выбором преподавателя | `/api/subjects/page`, `/api/teacher-subjects?subjectId=…`, `/api/users/teachers/options` |
|
||
| `university-structure` | Кафедры, специальности и профили обучения; профили доступны отдельной внутренней вкладкой и через кнопку специальности | `/api/departments`, `/api/specialties`, `/api/specialties/{id}/profiles` |
|
||
| `department-workspace` | Кабинет кафедры: дисциплины, импорт, комментарии, преподаватели, привязка преподавателей, заявки на новых преподавателей и нагрузка | `/api/department/*`, `/api/department/teacher-requests`, `/api/workload/teachers` |
|
||
| `schedule-view` | Просмотр опубликованного расписания: учебный год и семестр выбираются каскадом в дополнительных фильтрах, поисковые списки сущностей при открытии остаются свёрнутыми и используют размытый непрозрачный слой меню, найденные расписания переключаются без общего контекста страницы, а `ADMIN` и `EDUCATION_OFFICE` редактируют конкретное занятие в боковой панели без изменения правила | `/api/schedule/semesters`, `/api/schedule/search`, `/api/edu-office/schedule/overrides`, `/api/admin/time-slots/effective` |
|
||
| `teacher-absences` | Запросы преподавателей: отсутствия и мастер замены, согласование семестровых пожеланий, заявки на перенос, аудиторию или отмену; преподаватель ищется асинхронно по всем кафедрам для `ADMIN`/`EDUCATION_OFFICE` и только по своей кафедре для `DEPARTMENT`; поисковое меню размывает фон, а фильтры согласования оформлены для светлой и тёмной тем | `/api/teacher-absences`, `/api/teacher-preferences`, `/api/teacher-change-requests`, `/api/users/teachers/options` |
|
||
| `schedule` | Конструктор правил: локальный каскад `Учебный год → Семестр → Версия` открывает опубликованное расписание или черновик; первичная загрузка скрывает нестабильную компоновку до подготовки полей, а таблица правил использует фиксированные колонки и серверную пагинацию; группы, преподаватели и аудитории ищутся на сервере, подгруппы загружаются только для выбранных групп; предупреждение о несохранённых изменениях появляется только после действия пользователя | `/api/admin/schedule-rules`, `/api/edu-office/schedule/versions`, `/api/teacher-preferences`, `/api/admin/time-slots`, `/api/admin/calendar/years`, `/api/lesson-types`, `/api/groups/options`, `/api/subgroups?groupId=…`, `/api/users/teachers/options`, `/api/classrooms/options` |
|
||
| `schedule-versions` | Контур публикации с локальным выбором учебного года и семестра: текущая версия, черновики, diff, архив, восстановление и журнал | `/api/edu-office/schedule/versions` |
|
||
| `schedule-quality` | Диагностика выбранной версии через каскад `Учебный год → Семестр → Версия`: оценка, метрики и проблемные правила с переходом к их редактированию | `/api/edu-office/schedule/quality`, `/api/admin/schedule-rules` |
|
||
| `academic-calendar` | Учебные годы, семестры, создание календарных графиков, Excel-подобный редактор дневной сетки и привязка дисциплин к семестрам графика; редко используемая форма создания учебного года расположена внизу вкладки `Графики` | `/api/admin/calendar`, `/api/admin/academic-calendars`, `/api/admin/academic-calendars/{id}/subjects`, `/api/admin/calendar/activity-types`, `/api/specialties`, `/api/specialties/{id}/profiles`, `/api/education-forms`, `/api/subjects` |
|
||
| `auditorium-workload` | Динамическая загруженность аудиторий, преподавателей и кафедр: сводная матрица по дате или совмещённая таблица выбранной сущности по чётной/нечётной неделе | `/api/classrooms`, `/api/users/teachers`, `/api/departments`, `/api/admin/time-slots`, `/api/equipments`, `/api/groups`, `/api/schedule`, `/api/admin/calendar/years` |
|
||
|
||
### Особенности админских вкладок
|
||
|
||
- Общий `formatLocalDate()` формирует `YYYY-MM-DD` из локальных компонентов `Date`, не
|
||
используя `toISOString()`. Кабинет кафедры и редактор академического календаря также
|
||
выполняют календарную арифметику локальными компонентами, поэтому даты первых часов
|
||
суток и границы месяца не сдвигаются из-за преобразования в UTC.
|
||
- Вкладка `dashboard` формирует date-only значения из локальных компонентов даты, а текущую неделю — от отдельного объекта понедельника до `понедельник + 6 дней`. Расписания кафедр загружаются независимо через `Promise.allSettled`: `COMPLETE` означает ответы всех кафедр, `PARTIAL` — только части, `NOT_RUN` — отсутствие пригодных ответов или кафедр. Зелёная карточка «Конфликты расписания не обнаружены» разрешена только для `COMPLETE` без найденных конфликтов; частичный результат всегда остаётся предупреждением, а полный отказ показывается как «Проверка не выполнена». Технические причины отказов в DOM не выводятся.
|
||
- Вкладка `groups` загружает кафедры, специальности, профили, учебные годы и календарные графики. Список групп открывается через `/api/groups?includeArchived=true`, поэтому в таблице видны активные, будущие, завершившие обучение и архивные группы со статусом. Группа создаётся через `/api/groups` с `specialtyId` и `specialtyProfileId`, а модалка редактирования использует широкую сетку полей без внутреннего пустого скролла. Блок подгрупп использует `/api/subgroups` и `/api/groups/{id}/subgroups`, а блок назначений использует `/api/groups/{id}/calendar-assignments`. После назначения графика в таблице назначений сразу выводятся дисциплины графика, сгруппированные по номерам семестров. В селекты подгрупп и назначений попадают только группы с `active=true`.
|
||
- Вкладка `teacher-requests` показывает pending-заявки кафедр на создание преподавателей. Администратор может скорректировать кафедру, логин, ФИО и должность, задать пароль минимум 8 символов в скрытом поле с `autocomplete="new-password"`, затем одобрить заявку через `/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` и видеть статусы собственных заявок в таблице.
|
||
- Вкладка `teacher-absences` доступна администратору, учебному отделу и кафедре. Верхний
|
||
командный блок показывает очередь инцидентов, форма регистрирует преподавателя, период и
|
||
причину, а реестр разделяет статусы согласования. Полноэкранный мастер выводит каждое
|
||
затронутое занятие отдельной строкой и предлагает только кандидатов, уже проверенных
|
||
backend. Пустая строка не отправляется; выбранные решения применяются одним пакетом. Ниже
|
||
расположены реестр пожеланий на семестр и очередь заявок на изменение опубликованных пар.
|
||
Пожелание можно принять или отклонить с комментарием. Заявку применяют только
|
||
`ADMIN` и `EDUCATION_OFFICE`; кафедра видит записи своих преподавателей без управляющих
|
||
действий. Фильтры обоих реестров используют тематические кастомные списки с отдельными
|
||
поверхностями для светлой и тёмной тем, а поиск преподавателя — общее размытое меню
|
||
`AsyncCombobox`. Каждая карточка показывает исходное и запрошенное состояние, результат
|
||
предварительной проверки и хронологию решения.
|
||
- Вкладка `schedule-quality` доступна администратору и учебному отделу. После каскадного
|
||
выбора учебного года, семестра и версии она выводит круговую оценку от 0 до 100, карточки метрик и реестр правил
|
||
с фильтрами по серьёзности и типу. Все проблемные занятия одного правила показаны одной
|
||
карточкой; в ней выводятся число затронутых занятий, категории и суммарный штраф. Кнопка
|
||
`Изменить правило` открывает выбранную версию в конструкторе и сразу заполняет форму этим
|
||
правилом. Сценарий одинаков для `PUBLISHED` и `DRAFT`; `ARCHIVED` анализируется только для
|
||
чтения. Переход между вкладками передаёт ID версии и правила через одноразовые ключи
|
||
`magistr.schedule.openVersionId` и `magistr.schedule.openRuleId` в `localStorage`.
|
||
- Вкладка `schedule-versions` доступна администратору и учебному отделу и оформлена как
|
||
отдельный контур публикации. В верхней карточке учебный год сначала ограничивает список
|
||
семестров, поэтому архив периодов не превращается в один длинный селект. Карточка показывает версию, которую видят конечные
|
||
пользователи; ниже расположены черновики, сравнение правил и занятий, архив и журнал.
|
||
В конструкторе локальный каскад `Учебный год → Семестр → Версия` по умолчанию выбирает
|
||
опубликованную версию текущего семестра и выделяет её
|
||
зелёным контуром; её изменения применяются сразу. Селект позволяет перейти в медный режим
|
||
черновика. Кнопка `Создать черновик` копирует выбранное опубликованное расписание или
|
||
черновик и сразу открывает новую рабочую копию. Перед публикацией интерфейс
|
||
одновременно запрашивает полную валидацию и diff, требует причину и блокирует действие
|
||
при ошибках. Кнопки версий передают их ID одноразово через `localStorage` и открывают конструктор
|
||
или анализ качества в нужном контексте; архивную публикацию можно восстановить с причиной.
|
||
- Компоновка `department-workspace` использует собственные CSS-сетки `department-workspace-filter-grid` и `department-workspace-actions-grid`: фильтры периода отделены от сеток расписания, загрузка дисциплин занимает широкую колонку, формы преподавателей выравниваются справа, а списки и таблицы идут полноширинными блоками ниже.
|
||
- Вкладка `schedule-view` показывает найденные занятия в режиме одной активной таблицы. Пользователь выбирает, что смотреть: группу, преподавателя, аудиторию или кафедру; поисковые меню группы, преподавателя и аудитории используют плотный фон с `backdrop-filter`, чтобы строки не смешивались с карточками под списком. Основная кнопка `Показать` расположена в заголовке блока параметров, а пустое состояние таблицы с подсказкой об обновлении содержит дополнительную кнопку `Показать расписание`. В дополнительных фильтрах учебный год каскадно ограничивает список семестров из `/api/schedule/semesters`; оба значения сохраняются в URL. При загрузке автоматически выбирается текущий семестр, а между учебными периодами — ближайший будущий; если будущего нет, используется последний завершённый. Экран всегда показывает опубликованную версию: выбор черновика остаётся в `schedule-quality` и `schedule`. Для выбранного семестра frontend запрашивает весь период от его первой до последней даты и собирает повторяющиеся занятия в одну семестровую матрицу. На странице не выводится стек таблиц: виден один выбранный результат, а остальные доступны через чипы и кнопки предыдущего/следующего расписания; чипы результатов переносятся и отделены от счётчика стабильным отступом. Для режима кафедры и роли `DEPARTMENT` расписание ограничивается действующей кафедральной связью преподавателя на дату занятия; группы другой кафедры не отбрасываются, если занятие ведёт преподаватель текущей кафедры. Преподавательские и студенческие отдельные страницы пока остаются самостоятельными. Таблица строится как строки пар и столбцы дней недели. Нечётная неделя отображается в верхней половине ячейки, чётная — в нижней, а одинаковые занятия в обе недели схлопываются в цельную ячейку. Каждая карточка занятия явно показывает фактические границы недель: `(с 1 по 18 нед.)` либо `(7 нед.)`. На мобильной ширине вместо широкой матрицы показывается один день активного расписания с переключателем дней.
|
||
- Для `ADMIN` и `EDUCATION_OFFICE` карточка занятия содержит кнопку `Изменить`, а уже изменённая пара — индикатор разовой правки. Справа открывается полупрозрачная боковая панель с размытием содержимого под ней; внешний затемнённый слой также размывает страницу, а на мобильном устройстве панель занимает весь экран. Режим `Редактирование` сравнивает `Было по правилу / Станет`, позволяет изменить дату, эффективный временной слот, преподавателя, аудиторию, формат и комментарий, отменить занятие или удалить override через `Вернуть по правилу`. Преподаватель и аудитория ищутся асинхронно через `/api/users/teachers/options` и `/api/classrooms/options`; в подписи аудитории показывается её название. По умолчанию выводятся семь дней исходной недели; кнопка `Выбрать другую дату` раскрывает календарь всего семестра, где неучебные даты отключены. После смены даты загружается эффективная сетка дня: сначала выбирается тот же ID слота, затем совпадающий интервал, иначе требуется ручной выбор. Дата или время формируют `MOVE`, а только преподаватель, аудитория или формат — `REPLACE`.
|
||
- Режим `Изменения за период` загружает overrides, у которых в двухнедельный диапазон попала исходная или целевая дата. Поэтому в реестре остаются отменённые занятия и входящие/исходящие переносы, которых нет на исходном месте в таблице. После сохранения, отмены или возврата расписание и реестр обновляются без смены выбранного периода. Для `DEPARTMENT` и `SCHEDULE_VIEWER` вкладка остаётся полностью read-only: кнопка, панель и запросы управляющего API не создаются.
|
||
- Вкладка `auditorium-workload` стала общей вкладкой `Загруженность`: в поле «Что смотреть» выбираются аудитории, преподаватели или кафедры. Сводная матрица по выбранной дате использует одинаковую структуру: строки — выбранный тип сущности, столбцы — эффективные временные слоты дня из `/api/admin/time-slots/effective`, занятость собирается из динамического расписания `/api/schedule` по группам. Кафедральная матрица группирует занятия по кафедре преподавателя. Для аудиторий доступны фильтры корпуса, вместимости и оборудования. В поле «Отображение» можно выбрать конкретную аудиторию, преподавателя или кафедру; тогда сводная матрица заменяется одной таблицей по дням недели и времени для двухнедельного периода от выбранной даты. Таблица выбранной сущности растягивается до нижней части экрана. Ячейка делится вертикально только если верхняя и нижняя недели отличаются: нечётная неделя отображается сверху, чётная — снизу. Если состояние или занятие одинаковое, ячейка остаётся цельной. Чётность берётся из расписания, а для свободных дней рассчитывается по семестрам из `/api/admin/calendar/years`.
|
||
- Вкладка `university-structure` содержит внутренние разделы `Кафедры`, `Специальности` и `Профили`, оформленные тем же визуальным паттерном вкладок, что и `academic-calendar`: администратор создаёт профили как из общего списка, так и через кнопку `Профили` у конкретной специальности.
|
||
- Таблицы правил конструктора и календарных графиков загружают страницы с сервера: по умолчанию 25 записей, доступны 50 и 100. Поиск правил работает по дисциплине, группе и ID; поиск графиков — по названию, направлению, профилю, форме обучения и году, дополнительно доступен фильтр учебного года. Поиск сбрасывает страницу, устаревшие ответы не заменяют актуальную выдачу. Скрытая визуальная матрица конструктора загружается и строится только при открытии; переход из анализа качества получает нужное правило по ID независимо от страницы таблицы. Программное обновление селектов периода обновляет обёртки без повторного вызова обработчиков смены года и семестра.
|
||
- В разделе календарного графика списки графиков в «Сетках» и «Дисциплинах» используют `AsyncCombobox` и `/api/admin/academic-calendars/options`: поиск на сервере с задержкой 320 мс, до 20 результатов плюс выбранная запись. Полный справочник графиков не загружается в DOM; выбор редактора не зависит от страницы таблицы. Отмена перехода с несохранёнными дисциплинами восстанавливает выбранный график, устаревшие ответы загрузки игнорируются, а сохранение сетки требует предварительной загрузки именно выбранного графика.
|
||
- В «Структуре вуза» кафедры, специальности и профили имеют отдельный поиск и клиентскую пагинацию по 25/50/100 строк. Поиск учитывает названия, коды и ID, а у профилей также специальность и описание.
|
||
- В «Качестве расписания» правила после фильтрации упорядочиваются по суммарному штрафу по убыванию; при равном штрафе сохраняется исходный порядок.
|
||
- Вкладка `schedule` имеет заголовок `Конструктор правил` и не обращается к старым `lessons` API. До завершения первичной загрузки справочников и правил вкладка показывает единое состояние загрузки, после чего открывает уже подготовленную форму; таблица правил использует `table-layout: fixed`, явный `colgroup` и отключённую анимацию строк, поэтому заголовки колонок не смещаются при замене состояния загрузки данными. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы, отдельные часы и недели начала для лекций, лабораторных и практик, а также набор базовых слотов. Для выбранных преподавателя и семестра frontend загружает согласованные `/api/teacher-preferences` и выводит под строкой слота компактные маркеры строгой недоступности, предпочтительного или нежелательного времени и пожеланий «пары подряд»/«без окон»; количество недоступных дат показывается отдельным маркером. Группы выбираются через выпадающий мультиселект. Поле подгруппы появляется только при выборе лабораторной работы; для лекций и практик оно не отображается. Если в правиле выбрана одна группа, селект подгруппы содержит пункт `Вся группа`; если выбрано несколько групп, лабораторный слот показывает мультиселект подгрупп, чтобы выбрать разные подгруппы разных групп. Типы занятий в слоте сортируются в порядке: лекция, лабораторная работа, практика. Единственный слот имеет действие `Очистить`, которое сбрасывает его поля; при наличии нескольких слотов у каждой строки показывается действие `Удалить`. Оба действия используют размер формы, минимальную ширину `125px` и высоту `44px`, поэтому совпадают по масштабу с соседними селектами. Список слотов отображается без внутреннего вертикального скролла: при добавлении строк форма расширяется вниз, а кнопка сохранения остаётся отдельным блоком под слотами. Из календарной системы здесь используется список семестров для выбора периода действия правила. Справа доступна сворачиваемая визуальная матрица: пользователь выбирает учебный год, семестр и группы, после чего матрица строится только по правилам выбранного семестра. Столбцы — выбранные в фильтре группы, строки — только день и время, где есть активные пары, ячейки показывают дисциплину, диапазон недель, тип, формат, преподавателя, аудиторию и подгруппы. Период недель не показывается для занятия на весь семестр; если занятие идёт до конца семестра не с первой недели, выводится только неделя начала в формате `(с 5 нед.)`, а ограниченный диапазон — как `(с 1 по 3 нед.)`. Если нечётная и чётная недели отличаются, ячейка делится на две половины; одинаковые занятия схлопываются в цельную ячейку. Кнопка с тремя точками в правой части карточки пары открывает контекстное меню возле нажатой кнопки, с автоматическим разворотом от границ viewport: можно открыть полное правило в форме, изменить только день и базовую пару выбранного слота через компактную модалку или удалить правило целиком. В списке правил действия отображаются едиными кнопками одинакового размера с отступами между ними.
|
||
- Вкладка `academic-calendar` полностью отделяет календарную систему от расписания занятий и внутри себя разделена на три вкладки: `Графики` для учебных годов, семестров и карточек календарных графиков, `Сетки` для редактора дневной сетки, `Дисциплины` для ручной привязки дисциплин из `/api/subjects` к номерам учебных семестров графика. Форма редкого ежегодного действия — создания учебного года вместе с семестрами — расположена после реестров учебных годов и календарных графиков, в нижней части вкладки `Графики`. Пользователь вводит только четырёхзначный год начала, например `2026`; интерфейс подставляет год окончания `2027`, формирует название `2026-2027` и внутренний период `01.09.2026–30.06.2027`. Границы семестров можно скорректировать до сохранения. У календарного графика нет поля ввода и отдельного столбца названия; отображаемая подпись и сохраняемое backend название автоматически собираются как `код специальности — профиль обучения — форма обучения — учебный год`. Frontend не отправляет `title` календарного графика в payload. Администратор выбирает форму обучения из общего справочника `/api/education-forms`, заполняет дневную сетку по курсам и кодам активностей, назначает ручную временную сетку на конкретную дату, а сохранение сетки идёт через `/api/admin/academic-calendars/{id}/grid`. Каждый код активности окрашивает ячейку, подсказку, диалог и итоговый чип своим `colorCode`. REST-контракт сетки остаётся дневным: backend объединяет соседние даты с одинаковой активностью в периоды при записи и разворачивает их при чтении. Шесть полей дат принимают до восьми цифр, автоматически показывают их в формате `ДД.ММ.ГГГГ`, ограничивают год четырьмя цифрами и перед отправкой преобразуют значение в ISO `ГГГГ-ММ-ДД`; два поля учебного года принимают и показывают только четыре цифры.
|
||
- При сохранении правила во вкладке `schedule` frontend различает `409 Conflict` от остальных ошибок API. Если backend возвращает `conflictRule`, открывается широкое модальное окно разрешения конфликта: пользователь видит новое и ранее созданное правило, чипы причины конфликта из `conflictReasons` и подсветку соответствующих полей (`teacher`, `classroom`, `group`). Пользователь меняет у конфликтующего правила день, чётность, пару, преподавателя или аудиторию, после чего frontend сохраняет конфликтующее правило через `PUT /api/admin/schedule-rules/{id}` и автоматически повторяет исходный `POST` или `PUT`. Если после переноса появляется следующий конфликт, показывается следующее конфликтующее правило без потери исходного черновика.
|
||
- Редактор годового графика во вкладке `academic-calendar` показывает компактную табличную сетку: курсы раскрываются отдельными секциями со стрелкой, семестры внутри курса на широком экране идут рядом в две равные колонки одинаковой высоты, столбцы подписаны номерами недель учебного года, строки — днями недели, а ячейки содержат буквенный код активности. Недели выровнены по интервалам `понедельник–воскресенье`: если учебный год начинается не в понедельник, предшествующие позиции первой недели остаются пустыми, а следующий понедельник открывает неделю 2. В рамках одного курса семестровые таблицы получают одинаковое число недельных колонок: недостающие колонки заполняются пустыми ячейками, поэтому левая и правая части занимают всю ширину секции курса без внешней пустоты. Подробная расшифровка и изменение кода открываются в компактном модальном окне по клику на ячейку; при наведении возле курсора отображается кастомная подсказка с датой, курсом, кодом активности и временной сеткой. У границ viewport подсказка автоматически раскрывается в противоположную сторону и остаётся полностью видимой, а код активности в ней центрируется внутри квадратного индикатора. Ячейки можно выделять протяжкой мышью и менять код активности через ту же модалку, выбранные ячейки подсвечиваются мягкой заливкой, а кнопка «Применить» закрывает окно.
|
||
- Вкладка `classrooms` теперь показывает архивные аудитории через `includeArchived=true`. Кнопка удаления заменена на архивирование: аудитория выводится из эксплуатации, но остаётся в историческом расписании. Для архивных аудиторий доступно восстановление.
|
||
- Вкладка `users` поддерживает роли `EDUCATION_OFFICE`, `DEPARTMENT` и `SCHEDULE_VIEWER`; удаление пользователя работает как архивирование.
|
||
|
||
### Общие UX-компоненты первого пакета
|
||
|
||
- Общей панели учебного контекста под topbar нет. Каскадные селекты периода размещены только
|
||
в профильных вкладках: `schedule`, `schedule-versions`, `schedule-quality` и
|
||
`schedule-view`. Календарный график и назначение графика группе сохраняют собственные
|
||
локальные селекты.
|
||
- Вкладка и фильтры сохраняются в URL. Реестры групп, пользователей, дисциплин и заявок
|
||
восстанавливают поиск, фильтры, сортировку, номер и размер страницы после reload или
|
||
возврата по ссылке. Просмотр расписания сохраняет вид сущности, выбранную сущность, дату,
|
||
учебный год, семестр и дополнительные фильтры; конечные кабинеты сохраняют неделю, вкладку и группу.
|
||
- Общий React-компонент `react/shared/ui/Pagination.jsx` работает с общим ответом
|
||
`PageResponse`; размер страницы — 25, 50 или 100. `view-state.js` унифицирует
|
||
загрузку со spinner, пустой результат и ошибку с кнопкой повторного запроса.
|
||
- `AsyncCombobox` загружает варианты с сервера, сохраняет выбранный ID, поддерживает
|
||
клавиатуру и роли `combobox/listbox`. Он используется для групп, преподавателей,
|
||
аудиторий и совместимых календарных графиков; предварительная загрузка вариантов не
|
||
раскрывает меню. Студент больше не загружает полный список групп.
|
||
- `dialog.js` заменяет нативные `confirm`, `prompt` и `alert`: возвращает фокус, удерживает
|
||
его внутри окна, закрывается по Escape и может требовать причину. Нативных диалогов в
|
||
admin/settings-модулях нет.
|
||
- `dirty-state.js` предупреждает при смене вкладки, версии, графика или закрытии страницы.
|
||
Календарная сетка и привязки дисциплин показывают маркер `•` на кнопке сохранения;
|
||
конструктор правил отслеживает изменения полей и состава слотов, но игнорирует
|
||
программную синхронизацию селектов при загрузке и сбросе формы.
|
||
- `trackUxEvent()` создаёт базовые события смены раздела, загрузки реестров и расписания,
|
||
сохранения календаря/правила и просмотра ближайших занятий. В production они
|
||
преобразуются в OpenTelemetry spans, а до готовности bundle хранятся в ограниченной
|
||
очереди.
|
||
|
||
### Страница настроек (`/admin/settings/`)
|
||
|
||
Страница переведена на React (`react/settings/`, entrypoint `dist/react/settings.js`) и использует
|
||
общий слой `react/shared`: `useAuthorizedSession(SETTINGS_ROLES)`, `apiClient`, `AsyncState`,
|
||
`AppErrorBoundary` и общий `ConfirmDialog`/`PromptDialog` (классы `.project-dialog*`,
|
||
закрытие по Escape и клику по фону; `PromptDialog` — реплика legacy `promptAction`
|
||
с обязательным текстовым полем и ошибкой внутри диалога). Настройки — **отдельный SPA** со своей боковой панелью и вкладками, не связанными с
|
||
основной админ-панелью; как и в legacy-версии, hash-навигации нет — активная вкладка живёт
|
||
в состоянии React.
|
||
|
||
- Доступ: через dropdown «Настройки» в footer боковой панели админки для `ADMIN` и `EDUCATION_OFFICE`;
|
||
список и вкладка по умолчанию фильтруются единой матрицей `admin/js/role-capabilities.js`
|
||
- Кнопка «Назад в панель» для возврата в `/admin/`, кнопка выхода через общий `logoutAndRedirect`
|
||
- Сворачивание боковой панели сохраняется в `localStorage.sidebar-collapsed`; тема управляется
|
||
в React (кнопка в topbar), `theme-toggle.js` на странице больше не подключается
|
||
- Текущие вкладки:
|
||
- **Общие настройки** — заглушка (только `ADMIN`)
|
||
- **Временные слоты** — выбор сетки через выпадающий список, CRUD слотов выбранной сетки, добавление ручных сеток через модальное окно и удаление выбранной ручной сетки. Поле длительности доступно только для чтения и пересчитывается при изменении времени начала или окончания; в API отправляются только границы, а окончательное значение вычисляет backend. Базовая сетка применяется по умолчанию, субботняя — автоматически по субботам; ручное применение выполняется в сетке календарного графика.
|
||
- **Формы обучения** — создание и удаление форм обучения
|
||
- **База данных** — статус текущего подключения, реестр тенантов, проверка и добавление нового
|
||
подключения (пароль помечен `autocomplete="new-password"`); inline-стили legacy заменены
|
||
классами `.db-status-grid`/`.form-group-wide`/`.form-row-actions`
|
||
|
||
---
|
||
|
||
## API-клиент (`api.js`)
|
||
|
||
Все защищённые HTTP-запросы проходят через `fetchWithAuth()` из `auth-session.js`.
|
||
Access JWT и `role`/`departmentId`/`userId` существуют только в памяти JavaScript-модуля:
|
||
|
||
```javascript
|
||
export async function fetchWithAuth(endpoint, options = {}, retry = true) {
|
||
const headers = new Headers(options.headers || {});
|
||
if (accessToken) headers.set('Authorization', `Bearer ${accessToken}`);
|
||
const response = await fetch(endpoint, { ...options, headers, credentials: 'same-origin' });
|
||
if (response.status === 401 && retry && await refreshAccessToken()) {
|
||
return fetchWithAuth(endpoint, options, false);
|
||
}
|
||
return response;
|
||
}
|
||
```
|
||
|
||
При загрузке защищённой страницы память восстанавливается через `POST /api/auth/refresh` и
|
||
`HttpOnly` cookie. При `401` клиент выполняет не более одной общей refresh-ротации для всех
|
||
параллельных запросов и один раз повторяет исходный запрос. Неуспешный refresh очищает память
|
||
и возвращает пользователя на страницу входа. Legacy auth-ключи только удаляются из
|
||
`localStorage`/`sessionStorage`; новые данные авторизации туда не записываются.
|
||
|
||
После мутаций `api.js` инвалидирует кэш не только по точному URL, но и по связанным префиксам. Для заявок и кафедральных привязок очищаются `/api/users`, `/api/users/teachers`, `/api/teacher-requests`, `/api/department/teacher-requests` и `/api/department/teachers`, чтобы таблицы заявок и списки преподавателей обновлялись без ручного сброса страницы.
|
||
|
||
---
|
||
|
||
## Frontend-тесты
|
||
|
||
Регрессионные проверки используют встроенный `node:test` (Node.js 18+). Помимо тестов
|
||
дашборда, `auth-session.test.mjs` покрывает login-state, reload через refresh-cookie,
|
||
single-flight ротацию, повтор после `401` и logout. `security-policy.test.mjs` запрещает
|
||
auth-данные в Web Storage, удалённые/inline-скрипты, динамические inline-стили и рассинхрон
|
||
SHA-256 style-хэшей с CSP. Там же закреплены безопасный DOM-рендеринг ошибок без
|
||
интерполяции `exception.message` в `innerHTML`, скрытые парольные поля с корректным
|
||
`autocomplete`, русские метки tenant-контура и отсутствие англоязычных/raw exception
|
||
сообщений в собственных frontend-логах.
|
||
|
||
Команды выполняются из каталога `frontend/`:
|
||
|
||
```bash
|
||
npm run build # собрать dist/vendor/otel.js и dist/react/login.js
|
||
npm test # frontend unit/static tests
|
||
npm run check # React-сборка, синтаксис auth/UI-модулей + все frontend-тесты
|
||
```
|
||
|
||
React-сборка выполняется существующим esbuild без отдельного dev-сервера. Для страницы
|
||
входа установлен лимит 75 000 байт gzip; превышение завершает сборку ошибкой. При появлении
|
||
нескольких React entrypoint общий runtime будет вынесен в отдельный кэшируемый chunk.
|
||
|
||
---
|
||
|
||
## Аутентификация (Frontend)
|
||
|
||
### Страница входа (`/index.html`)
|
||
|
||
1. Пользователь вводит логин/пароль
|
||
2. `react/login/LoginApp.jsx` через `login-service.js` отправляет `POST /api/auth/login`
|
||
3. При успехе кладёт access JWT и профиль пользователя только в память текущей страницы.
|
||
4. Refresh-токен сохраняется браузером как `HttpOnly` cookie и недоступен JavaScript.
|
||
5. После перехода новый документ восстанавливает память через `POST /api/auth/refresh`.
|
||
6. Пользователь перенаправляется на соответствующий интерфейс:
|
||
- `ADMIN` → `/admin/`
|
||
- `EDUCATION_OFFICE` → `/admin/#schedule-view`
|
||
- `DEPARTMENT` → `/admin/#department-workspace`
|
||
- `SCHEDULE_VIEWER` → `/admin/#schedule-view`
|
||
- `TEACHER` → `/teacher/`
|
||
- `STUDENT` → `/student/`
|
||
|
||
### Проверка авторизации
|
||
|
||
Каждая защищённая страница сначала восстанавливает сессию и проверяет роль из памяти:
|
||
|
||
```javascript
|
||
const session = await restoreSession();
|
||
if (!session || !AUTHORIZED_ROLES.includes(session.role)) {
|
||
window.location.replace('/');
|
||
}
|
||
```
|
||
|
||
### Выход
|
||
|
||
Кнопка «Выйти» вызывает `POST /api/auth/logout`, очищает access JWT и профиль из памяти и
|
||
перенаправляет на `/`. Refresh-cookie очищает backend.
|
||
|
||
---
|
||
|
||
## Кабинеты расписания
|
||
|
||
### Учебный отдел (`/admin/#schedule-view`)
|
||
|
||
Учебный отдел работает в общей админ-панели с ограниченным набором вкладок:
|
||
|
||
- расширенный поиск `/api/schedule/search` по дате в периоде, преподавателю, аудитории и кафедре;
|
||
- конструктор расписания через `/api/admin/schedule-rules`;
|
||
- разовая правка конкретного занятия через боковую панель: перенос даты/времени, замена ресурсов, отмена и возврат по правилу;
|
||
- матричный просмотр загруженности аудиторий, преподавателей и кафедр через общую вкладку `Загруженность`;
|
||
- аудитории, оборудование, календарный график и загруженность.
|
||
|
||
### Кафедра (`/admin/#department-workspace`)
|
||
|
||
Кабинет кафедры встроен в общую панель и работает в контексте `departmentId` текущего пользователя:
|
||
|
||
- список дисциплин кафедры через `/api/department/subjects`;
|
||
- загрузка дисциплин из текстового списка в формате `код; название`;
|
||
- комментарии к дисциплинам;
|
||
- просмотр расписания кафедры через вкладку `schedule-view`;
|
||
- загруженность преподавателей кафедры.
|
||
|
||
### Просмотр расписаний (`/admin/#schedule-view`)
|
||
|
||
Роль `SCHEDULE_VIEWER` видит только доступную для чтения вкладку просмотра опубликованного расписания. Доступны каскад учебного года и семестра, а также фильтры по группе, преподавателю, аудитории, кафедре, дисциплине, типу занятия, чётности и разрезу таблиц. При выборе семестра найденные занятия отображаются одной матрицей на весь период с явными границами недель для каждой пары.
|
||
|
||
### Преподаватель (`/teacher/`)
|
||
|
||
Страница переведена на React (`react/teacher/`, entrypoint `dist/react/teacher.js`) и использует
|
||
общий слой `react/shared`: `useAuthorizedSession(['TEACHER'])`, `apiClient`, `AsyncState`,
|
||
`AppErrorBoundary`, дата-математику `dates.js` и общий `ScheduleOverview` с ролью `teacher`.
|
||
ID преподавателя берётся из восстановленного в памяти профиля сессии.
|
||
|
||
Кабинет объединяет недельную сетку занятий, семестровый календарь доступности и журнал
|
||
заявок на изменение пар в трёх вкладках; активная вкладка и дата недели сохраняются
|
||
в query-параметрах `teacherTab`/`teacherDate` через общий `url-state.js`.
|
||
|
||
Основные элементы:
|
||
- общий блок «Сегодня» и карточка «Следующая пара»; отдельный запрос покрывает ближайшие
|
||
14 дней, поэтому следующая пара находится и за границей текущей недели;
|
||
- навигация по неделям: предыдущая, текущая, следующая; выбор даты через `input[type="date"]`;
|
||
- запрос `GET /api/schedule?teacherId={userId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}`;
|
||
- матрица «слоты × дни недели» на десктопе и дневной список на мобильных (переключение
|
||
через `matchMedia` с живым откликом на смену ширины);
|
||
- отображение дисциплины, времени, типа занятия, лабораторных подгрупп, аудитории и всех групп правила;
|
||
- вкладка «Мои пожелания» с выбором семестра, режимом отметки строгих, предпочтительных и
|
||
нежелательных интервалов, полностью недоступных дат, пар подряд и расписания без окон;
|
||
- цветовые статусы `PENDING`, `APPROVED`, `REJECTED`, `CANCELLED`; ожидающее пожелание можно
|
||
отозвать кликом по отмеченной ячейке или кнопкой в списке дат;
|
||
- кнопка «Запросить изменение» в карточке занятия из правила и модальное окно переноса,
|
||
смены аудитории или отмены. Недоступные кандидаты отфильтрованы после проверки backend;
|
||
- вкладка журнала заявок с причиной, решением и историей статусов; ожидающую заявку можно
|
||
отозвать; счётчик ожидающих заявок выводится прямо на вкладке навигации;
|
||
- форма собственной заявки на отсутствие через `POST /api/teacher-absences`;
|
||
- список статусов заявок и отмена ещё не подтверждённой записи через
|
||
`DELETE /api/teacher-absences/{id}`.
|
||
|
||
Если refresh-cookie недействительна или роль не `TEACHER`, страница возвращает пользователя на вход.
|
||
|
||
### Студент (`/student/`)
|
||
|
||
Страница переведена на React (`react/student/`, entrypoint `dist/react/student.js`) и использует
|
||
общий слой `react/shared`: `useAuthorizedSession(['STUDENT'])`, `apiClient`, `AsyncState`,
|
||
`AppErrorBoundary` и React-версия поискового списка `AsyncCombobox`. Модель обзора «Сегодня /
|
||
Следующая пара» переиспользует чистые функции `schedule-overview.js`.
|
||
|
||
В текущей модели студент не связан с конкретной группой, поэтому страница использует
|
||
асинхронный поиск группы через `/api/groups/options`.
|
||
|
||
Основные элементы:
|
||
- поисковый combobox группы с подсказками по специальности, профилю и курсу; выбор хранится
|
||
в `localStorage.studentGroupId` и `studentGroup` query string;
|
||
- общий блок «Сегодня» и карточка «Следующая пара», рассчитанные по ближайшим 14 дням;
|
||
- недельная сетка по дням с навигацией «Предыдущая/Следующая неделя», «Сегодня» и выбором даты;
|
||
- запрос `GET /api/schedule?groupId={groupId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}`;
|
||
- отображение дисциплины, времени, преподавателя, аудитории, формата, типа занятия и лабораторных подгрупп;
|
||
- URL-параметры `studentGroup` и `studentDate` сохраняются через общий `url-state.js`.
|
||
|
||
---
|
||
|
||
## CSS-архитектура
|
||
|
||
### Модульный подход
|
||
|
||
Стили разделены на модульные файлы (порядок подключения важен):
|
||
|
||
1. **`main.css`** — CSS-переменные (цвета, шрифты, отступы), глобальные стили, тёмная тема
|
||
2. **`layout.css`** — Sidebar, topbar, content area, dropdown настроек, responsive
|
||
3. **`components.css`** — Кнопки, таблицы, карточки, badge, формы, theme-toggle
|
||
4. **`modals.css`** — Модальные окна
|
||
5. **`departments-data.css`** — Стили создания кафедры/специальности
|
||
|
||
### Темизация
|
||
|
||
Общий `ui-foundation.css` подключается последним во всех пяти оболочках.
|
||
Текст, заголовки и элементы управления используют прежний стек `Inter`,
|
||
`-apple-system`, `BlinkMacSystemFont`, `Segoe UI`, `sans-serif` без внешнего CDN.
|
||
Если Inter не установлен, браузер выбирает системный шрифт без засечек.
|
||
Базовые стили содержат запасной стек на случай недоступности общего файла.
|
||
|
||
Список свободных аудиторий на дашборде имеет высоту 180 px и внутреннюю
|
||
вертикальную прокрутку, доступную с клавиатуры. В запросах преподавателей
|
||
статус, комментарий и кнопки размещаются под содержимым заявки; кнопки
|
||
переносятся целиком при нехватке ширины, статусы не растягиваются по высоте.
|
||
|
||
CSS-переменные позволяют поддерживать светлую/тёмную тему:
|
||
|
||
```css
|
||
:root {
|
||
--bg-primary: #ffffff;
|
||
--text-primary: #1a1a2e;
|
||
--accent: #4F46E5;
|
||
}
|
||
|
||
[data-theme="dark"] {
|
||
--bg-primary: #0f0f23;
|
||
--text-primary: #e2e8f0;
|
||
--accent: #8B5CF6;
|
||
}
|
||
```
|
||
|
||
### Система кнопок
|
||
|
||
Кнопки админ-панели, отдельной SPA настроек, страницы входа и личных кабинетов используют единую семантическую модель:
|
||
|
||
- `btn` — обязательная база для всех кнопок и ссылок-кнопок.
|
||
- Размеры: `btn-sm` — 32px для табличных строк, `btn-md` — 40px для форм и панелей, `btn-lg` — 48px для входа и главных широких действий.
|
||
- Иконки: `btn-icon-sm` — 32x32, `btn-icon-md` — 40x40. Все icon-only кнопки должны иметь `aria-label` на русском языке.
|
||
- Варианты: `btn-primary` для основного действия блока, `btn-secondary` для редактирования/обновления/показа, `btn-ghost` для очистки/отмены/сворачивания, `btn-danger` для удаления, `btn-danger-subtle` для архивирования, выхода и других рискованных действий без немедленного удаления.
|
||
- Каждый вариант `btn`, включая `primary`, `ghost`, `danger` и disabled-состояние, сохраняет видимую обводку в светлой и тёмной теме.
|
||
- Единый радиус кнопок — `8px`. Локальные переопределения `.btn-primary`, `.btn-secondary`, `.btn-delete` в feature CSS запрещены: новые экраны должны наследовать токены из `frontend/admin/css/components.css`.
|
||
- Primary-акцент общий для продукта: `#4F46E5` в светлой теме и `#8B5CF6` в тёмной. Ролевые цвета кнопок в кабинетах преподавателя и студента не используются.
|
||
|
||
Переключение — через `theme-toggle.js`.
|
||
|
||
---
|
||
|
||
## Боковая панель (Sidebar)
|
||
|
||
- **Скрытие/раскрытие** — кнопка-крестик в правом верхнем углу sidebar
|
||
- **Десктоп** (`>768px`): sidebar складывается влево, контент расширяется; состояние сохраняется в `localStorage` (`sidebar-collapsed`)
|
||
- **Мобильные** (`≤768px`): sidebar скрывается за кнопкой-гамбургер, выезжает как overlay с затемнением
|
||
- **Dropdown «Настройки»** в footer sidebar — содержит ссылку на страницу настроек и кнопку выхода
|
||
|
||
---
|
||
|
||
## OpenTelemetry (локальный bundle)
|
||
|
||
Клиентская телеметрия (document-load, fetch, XHR) отправляется через `BatchSpanProcessor` на
|
||
same-origin путь `/otel/v1/traces`.
|
||
|
||
- npm-зависимости зафиксированы exact-версиями в `package-lock.json`;
|
||
- `scripts/build-vendor.mjs` собирает их через esbuild в `/vendor/otel.js`;
|
||
- браузер импортирует только same-origin bundle, CDN-код в runtime отсутствует;
|
||
- на `localhost` телеметрия не включается.
|
||
- прикладные `magistr:ux-event` в production записываются отдельными spans с безопасными
|
||
скалярными атрибутами; очередь ограничена 100 событиями.
|
||
|
||
```javascript
|
||
telemetryPromise = import('/vendor/otel.js');
|
||
```
|
||
|
||
---
|
||
|
||
## Адаптивность
|
||
|
||
Интерфейс адаптирован под мобильные устройства:
|
||
- Sidebar скрывается на экранах < 768px, выезжает как overlay
|
||
- Появляется кнопка-гамбургер (`#menu-toggle`)
|
||
- Кнопка-крестик закрывает sidebar на всех устройствах
|
||
- Таблицы получают горизонтальный скролл
|