# 🎨 Frontend ## Общая информация | Параметр | Значение | |----------|----------| | **Фреймворк** | Нет (Vanilla JavaScript) | | **Модульная система** | ES6 Modules (`import`/`export`) | | **Стили** | CSS (модульный подход) | | **Шрифт** | Inter на странице входа, системный шрифт в кабинетах расписания | | **Веб-сервер** | Apache httpd:alpine | --- ## Структура файлов ``` frontend/ ├── index.html # 🔐 Страница авторизации (общая) ├── script.js # Логика авторизации ├── style.css # Стили страницы авторизации ├── theme-toggle.js # Переключение светлой/тёмной темы ├── Dockerfile # httpd:alpine │ ├── admin/ # 👨‍💼 Интерфейс администратора │ ├── index.html # SPA-оболочка с sidebar │ ├── css/ │ │ ├── main.css # CSS-переменные, цвета, типографика │ │ ├── layout.css # Раскладка (sidebar, topbar, content) │ │ ├── components.css # Кнопки, таблицы, карточки, формы │ │ ├── modals.css # Модальные окна │ │ ├── auditorium-workload.css # Таблицы расписаний и загруженности │ │ └── departments-data.css # Стили создания кафедры/специальности │ ├── js/ │ │ ├── main.js # Инициализация, маршрутизация, навигация │ │ ├── api.js # HTTP-обёртка (fetch + Authorization) │ │ ├── utils.js # Утилиты │ │ ├── otel.js # OpenTelemetry (клиентская телеметрия, только прод) │ │ └── views/ # Модули представлений │ │ ├── users.js # Управление пользователями │ │ ├── groups.js # Управление группами │ │ ├── classrooms.js # Управление аудиториями │ │ ├── subjects.js # Управление дисциплинами │ │ ├── equipments.js # Управление оборудованием │ │ ├── edu-forms.js # Формы обучения │ │ ├── profiles.js # Профили обучения специальностей │ │ ├── department-workspace.js # Кабинет кафедры в общей панели │ │ ├── schedule-view.js # Read-only просмотр расписаний в матрицах │ │ ├── schedule.js # Конструктор правил расписания │ │ ├── academic-calendar.js # Календарные учебные графики │ │ ├── auditorium-workload.js # Загруженность аудиторий, преподавателей и кафедр │ │ ├── database.js # Управление тенантами │ │ └── departments-data.js # Создание кафедры/специальности │ ├── views/ # HTML-шаблоны представлений │ │ ├── users.html │ │ ├── groups.html │ │ ├── classrooms.html │ │ ├── subjects.html │ │ ├── equipments.html │ │ ├── edu-forms.html │ │ ├── profiles.html │ │ ├── department-workspace.html │ │ ├── schedule-view.html │ │ ├── schedule.html │ │ ├── academic-calendar.html │ │ ├── auditorium-workload.html │ │ ├── database.html │ │ └── departments-data.html │ │ │ └── settings/ # ⚙️ Страница настроек (отдельный SPA) │ ├── index.html # Оболочка с собственной sidebar │ ├── css/ │ │ ├── main.css # CSS-переменные, базовые стили │ │ └── layout.css # Sidebar, topbar, content │ ├── js/ │ │ ├── main.js # Навигация по вкладкам настроек │ │ └── views/ │ │ └── time-slots.js # Настройка временных слотов │ └── views/ │ ├── general.html # Общие настройки (заглушка) │ └── time-slots.html # Базовая, субботняя и ручные сетки времени │ ├── teacher/ # 👩‍🏫 Интерфейс преподавателя │ └── index.html # Недельный просмотр динамического расписания преподавателя ├── department/ # 🏛 Кабинет кафедры │ └── index.html # Redirect в `/admin/#department-workspace` ├── edu-office/ # 🗓 Кабинет учебного отдела │ └── index.html # Redirect в `/admin/#schedule-view` │ └── student/ # 🎓 Интерфейс студента └── index.html # Недельный просмотр динамического расписания группы ``` --- ## Система маршрутизации (Admin SPA) Админ-панель работает как **Single Page Application** без фреймворка. Навигация реализована через `data-tab` атрибуты на элементах sidebar: ```html Пользователи Группы Расписание занятий Календарный график ``` При клике на пункт меню `main.js`: 1. Загружает HTML-шаблон из `views/{tab}.html` через `fetch()` 2. Вставляет его в `#app-content` 3. Подключает соответствующий JS-модуль из `js/views/{tab}.js` 4. Обновляет заголовок страницы (`#page-title`) `main.js` также фильтрует вкладки по роли: | Роль | Доступные вкладки | |------|-------------------| | `ADMIN` | Все вкладки | | `EDUCATION_OFFICE` | Просмотр расписаний, конструктор расписания, календарный график, загруженность, аудитории, оборудование | | `DEPARTMENT` | Кабинет кафедры, просмотр расписаний | | `SCHEDULE_VIEWER` | Только просмотр расписаний | Пути `/department/` и `/edu-office/` оставлены как входные redirect-страницы в общую панель. Отдельные кабинеты не дублируют UI админ-панели. Пункты меню, недоступные роли, не отображаются в sidebar: `main.js` выставляет `hidden`, а `layout.css` явно скрывает такие элементы, чтобы базовый `display: flex` у `.nav-item` не возвращал их на экран. ### Разделы админ-панели | Tab | Описание | API | |-----|----------|-----| | `users` | CRUD пользователей | `/api/users` | | `groups` | CRUD групп, подгруппы лабораторных и назначения графиков | `/api/groups`, `/api/subgroups` | | `edu-forms` | Формы обучения | `/api/education-forms` | | `profiles` | Создание, редактирование и удаление профилей обучения специальностей | `/api/specialties/{id}/profiles` | | `equipments` | Оборудование | `/api/equipments` | | `classrooms` | Аудитории | `/api/classrooms` | | `subjects` | Дисциплины | `/api/subjects` | | `department-workspace` | Кабинет кафедры: дисциплины, импорт, комментарии, преподаватели и нагрузка | `/api/department/*`, `/api/workload/teachers` | | `schedule-view` | Read-only просмотр расписаний: по одной выбранной дате строится двухнедельный диапазон, найденные расписания выбираются в переключателе, а на экране отображается одна активная совмещённая таблица чётной/нечётной недели | `/api/schedule/search` | | `schedule` | Конструктор правил динамического расписания | `/api/admin/schedule-rules`, `/api/admin/time-slots`, `/api/admin/calendar/years`, `/api/lesson-types`, `/api/subgroups` | | `academic-calendar` | Учебные годы, семестры, создание календарных графиков и Excel-подобный редактор дневной сетки | `/api/admin/calendar`, `/api/admin/academic-calendars`, `/api/admin/calendar/activity-types`, `/api/education-forms` | | `auditorium-workload` | Динамическая загруженность аудиторий, преподавателей и кафедр: сводная матрица по дате или совмещённая таблица выбранной сущности по чётной/нечётной неделе | `/api/classrooms`, `/api/users/teachers`, `/api/departments`, `/api/admin/time-slots`, `/api/equipments`, `/api/groups`, `/api/schedule`, `/api/admin/calendar/years` | | `database` | Тенанты | `/api/database` | | `departments-data` | Создание, редактирование и удаление кафедр/специальностей | `/api/departments`, `/api/specialties` | ### Особенности админских вкладок - Вкладка `groups` загружает кафедры, специальности, профили, учебные годы и календарные графики. Группа создаётся через `/api/groups` с `specialtyId` и `specialtyProfileId`, блок подгрупп использует `/api/subgroups` и `/api/groups/{id}/subgroups`, а блок назначений использует `/api/groups/{id}/calendar-assignments`. - Вкладка `schedule-view` показывает найденные занятия в режиме одной активной таблицы. Пользователь выбирает, что смотреть: группу, преподавателя, аудиторию или кафедру; frontend запрашивает двухнедельный диапазон от понедельника выбранной даты и собирает найденные расписания в переключатель результатов. На странице не выводится стек таблиц: виден один выбранный результат, а остальные доступны через чипы и кнопки предыдущего/следующего расписания. Для режима кафедры и роли `DEPARTMENT` расписание ограничивается кафедрой пользователя; преподавательские и студенческие отдельные страницы пока остаются самостоятельными. Таблица строится как строки пар и столбцы дней недели. Нечётная неделя отображается в верхней половине ячейки, чётная — в нижней, а одинаковые занятия в обе недели схлопываются в цельную ячейку. На мобильной ширине вместо широкой недельной матрицы показывается один день активного расписания с переключателем дней. - Вкладка `auditorium-workload` стала общей вкладкой `Загруженность`: в поле «Что смотреть» выбираются аудитории, преподаватели или кафедры. Сводная матрица по выбранной дате использует одинаковую структуру: строки — выбранный тип сущности, столбцы — эффективные временные слоты дня из `/api/admin/time-slots/effective`, занятость собирается из динамического расписания `/api/schedule` по группам. Кафедральная матрица группирует занятия по кафедре преподавателя. Для аудиторий доступны фильтры корпуса, вместимости и оборудования. В поле «Отображение» можно выбрать конкретную аудиторию, преподавателя или кафедру; тогда сводная матрица заменяется одной таблицей по дням недели и времени для двухнедельного периода от выбранной даты. Таблица выбранной сущности растягивается до нижней части экрана. Ячейка делится вертикально только если верхняя и нижняя недели отличаются: нечётная неделя отображается сверху, чётная — снизу. Если состояние или занятие одинаковое, ячейка остаётся цельной. Чётность берётся из расписания, а для свободных дней рассчитывается по семестрам из `/api/admin/calendar/years`. - Вкладка `profiles` выделена под профили обучения: администратор выбирает специальность, создаёт профиль, редактирует описание и удаляет неиспользуемые профили. - Вкладка `schedule` не обращается к старым `lessons` API. Создание и редактирование расписания выполняется через правила `/api/admin/schedule-rules`, где каждое правило содержит группы, отдельные часы и недели начала для лекций, лабораторных и практик, а также набор базовых слотов. Группы выбираются через выпадающий мультиселект. Поле подгруппы появляется только при выборе лабораторной работы; для лекций и практик оно не отображается. Если в правиле выбрана одна группа, селект подгруппы содержит пункт `Вся группа`; если выбрано несколько групп, лабораторный слот показывает мультиселект подгрупп, чтобы выбрать разные подгруппы разных групп. Из календарной системы здесь используется список семестров для выбора периода действия правила. - Вкладка `academic-calendar` полностью отделяет календарную систему от расписания занятий: администратор создаёт учебные годы и семестры, заводит календарные графики, выбирает форму обучения из общего справочника `/api/education-forms`, заполняет дневную сетку по курсам и кодам активностей, назначает ручную временную сетку на конкретную дату, а сохранение сетки идёт через `/api/admin/academic-calendars/{id}/grid`. - Редактор годового графика во вкладке `academic-calendar` показывает компактную табличную сетку: курсы раскрываются отдельными секциями со стрелкой, семестры внутри курса на широком экране идут рядом в две колонки одинаковой высоты, столбцы подписаны номерами недель учебного года, строки — днями недели, а ячейки содержат буквенный код активности. Подробная расшифровка и изменение кода открываются в компактном модальном окне по клику на ячейку; при наведении отображается кастомная подсказка с датой, курсом, кодом активности и временной сеткой. Ячейки можно выделять протяжкой мышью и менять код активности через ту же модалку, выбранные ячейки подсвечиваются мягкой заливкой, а кнопка «Применить» закрывает окно. - Вкладка `departments-data` использует модальные формы редактирования и маршруты `PUT/DELETE /api/departments/{id}` и `PUT/DELETE /api/specialties/{id}`. - Вкладка `classrooms` теперь показывает архивные аудитории через `includeArchived=true`. Кнопка удаления заменена на архивирование: аудитория выводится из эксплуатации, но остаётся в историческом расписании. Для архивных аудиторий доступно восстановление. - Вкладка `users` поддерживает роли `EDUCATION_OFFICE`, `DEPARTMENT` и `SCHEDULE_VIEWER`; удаление пользователя работает как архивирование. ### Страница настроек (`/admin/settings/`) Настройки — это **отдельный SPA** со своей боковой панелью и вкладками, не связанными с основной админ-панелью. - Доступ: через dropdown «Настройки» в footer боковой панели админки для `ADMIN` и `EDUCATION_OFFICE` - Кнопка «Назад в панель» для возврата в `/admin/` - Текущие вкладки: - **Общие настройки** — заглушка (только `ADMIN`) - **Временные слоты** — выбор сетки через выпадающий список, CRUD слотов выбранной сетки, добавление ручных сеток через модальное окно и удаление выбранной ручной сетки. Базовая сетка применяется по умолчанию, субботняя — автоматически по субботам; ручное применение выполняется в сетке календарного графика. --- ## API-клиент (`api.js`) Все HTTP-запросы проходят через обёртку `apiFetch()`. Access JWT читается из `localStorage` перед каждым запросом: ```javascript export async function apiFetch(endpoint, method = 'GET', body = null, retryOnUnauthorized = true) { const response = await fetch(endpoint, { method, headers: getHeaders(body ? 'application/json' : null), credentials: 'same-origin', body: body ? JSON.stringify(body) : undefined }); if (response.status === 401 && retryOnUnauthorized) { const refreshed = await refreshAccessToken(); if (refreshed) return apiFetch(endpoint, method, body, false); clearAuthState(); window.location.href = '/'; } if (!response.ok) { throw new Error(data?.message || `Ошибка HTTP: ${response.status}`); } return await response.json(); } // Shortcut-методы export const api = { get: (url) => apiFetch(url, 'GET'), post: (url, body) => apiFetch(url, 'POST', body), put: (url, body) => apiFetch(url, 'PUT', body), delete: (url, body) => apiFetch(url, 'DELETE', body) }; ``` При `401` клиент один раз вызывает `POST /api/auth/refresh`, обновляет `localStorage.token` и повторяет исходный запрос. Если refresh неуспешен, auth state очищается и пользователь возвращается на страницу входа. --- ## Аутентификация (Frontend) ### Страница входа (`/index.html`) 1. Пользователь вводит логин/пароль 2. `script.js` отправляет `POST /api/auth/login` 3. При успехе сохраняет в `localStorage`: - `token` — access JWT - `role` — роль пользователя - `departmentId` — кафедра пользователя - `userId` — ID пользователя для личного расписания преподавателя 4. Refresh-токен сохраняется браузером как `HttpOnly` cookie и недоступен JavaScript 5. Перенаправляет на соответствующий интерфейс: - `ADMIN` → `/admin/` - `EDUCATION_OFFICE` → `/admin/#schedule-view` - `DEPARTMENT` → `/admin/#department-workspace` - `SCHEDULE_VIEWER` → `/admin/#schedule-view` - `TEACHER` → `/teacher/` - `STUDENT` → `/student/` ### Проверка авторизации На каждой странице проверяется наличие токена и роли: ```javascript export function isAuthenticatedAsAdmin() { const role = localStorage.getItem('role'); return getToken() && role === 'ADMIN'; } ``` ### Выход Кнопка «Выйти» вызывает `POST /api/auth/logout`, затем очищает `localStorage` и перенаправляет на `/`. --- ## Кабинеты расписания ### Учебный отдел (`/admin/#schedule-view`) Учебный отдел работает в общей админ-панели с ограниченным набором вкладок: - расширенный поиск `/api/schedule/search` по дате в периоде, преподавателю, аудитории и кафедре; - конструктор расписания через `/api/admin/schedule-rules`; - создание точечного изменения пары через `/api/edu-office/schedule/overrides` на уровне API; - матричный просмотр загруженности аудиторий, преподавателей и кафедр через общую вкладку `Загруженность`; - аудитории, оборудование, календарный график и загруженность. ### Кафедра (`/admin/#department-workspace`) Кабинет кафедры встроен в общую панель и работает в контексте `departmentId` текущего пользователя: - список дисциплин кафедры через `/api/department/subjects`; - загрузка дисциплин из текстового списка в формате `код; название`; - комментарии к дисциплинам; - просмотр расписания кафедры через вкладку `schedule-view`; - загруженность преподавателей кафедры. ### Просмотр расписаний (`/admin/#schedule-view`) Роль `SCHEDULE_VIEWER` видит только read-only вкладку просмотра расписаний. Доступны фильтры по дате в периоде, группе, преподавателю, аудитории, кафедре, дисциплине, типу занятия, чётности и разрезу таблиц. Найденные занятия отображаются отдельными совмещёнными таблицами чётной/нечётной недели с парами по строкам и днями недели по столбцам. ### Преподаватель (`/teacher/`) Страница показывает недельную сетку занятий преподавателя. ID преподавателя берётся из `localStorage.userId`, который сохраняется после `POST /api/auth/login`. Основные элементы: - навигация по неделям: предыдущая, текущая, следующая; - выбор даты через `input[type="date"]`; - запрос `GET /api/schedule?teacherId={userId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}`; - отображение дисциплины, времени, типа занятия, лабораторных подгрупп, аудитории и всех групп правила. Если пользователь вошёл до появления поля `userId`, страница попросит выполнить вход заново. ### Студент (`/student/`) В текущей модели студент не связан с конкретной группой, поэтому страница использует выбор группы из `/api/groups`. Основные элементы: - селект группы с сохранением выбора в `localStorage.studentGroupId`; - недельная сетка по дням; - запрос `GET /api/schedule?groupId={groupId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}`; - отображение дисциплины, времени, преподавателя, аудитории, формата, типа занятия и лабораторных подгрупп. --- ## 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`** — Стили создания кафедры/специальности ### Темизация CSS-переменные позволяют поддерживать светлую/тёмную тему: ```css :root { --bg-primary: #ffffff; --text-primary: #1a1a2e; --accent: #6366f1; } [data-theme="dark"] { --bg-primary: #0f0f23; --text-primary: #e2e8f0; --accent: #818cf8; } ``` Переключение — через `theme-toggle.js`. --- ## Боковая панель (Sidebar) - **Скрытие/раскрытие** — кнопка-крестик в правом верхнем углу sidebar - **Десктоп** (`>768px`): sidebar складывается влево, контент расширяется; состояние сохраняется в `localStorage` (`sidebar-collapsed`) - **Мобильные** (`≤768px`): sidebar скрывается за кнопкой-гамбургер, выезжает как overlay с затемнением - **Dropdown «Настройки»** в footer sidebar — содержит ссылку на страницу настроек и кнопку выхода --- ## OpenTelemetry (`otel.js`) Клиентская телеметрия (document-load, fetch, XHR) отправляется через `BatchSpanProcessor` на `/otel/v1/traces`. - **На production** — загружается автоматически через динамический `import()` - **На localhost** — пропускается, чтобы избежать таймаутов CDN `esm.sh` ```javascript if (!['localhost', '127.0.0.1'].includes(window.location.hostname)) { import('./otel.js').catch(e => console.warn('OTel init skipped:', e.message)); } ``` --- ## Адаптивность Интерфейс адаптирован под мобильные устройства: - Sidebar скрывается на экранах < 768px, выезжает как overlay - Появляется кнопка-гамбургер (`#menu-toggle`) - Кнопка-крестик закрывает sidebar на всех устройствах - Таблицы получают горизонтальный скролл