Compare commits
1 Commits
main
...
dynamic_sc
| Author | SHA1 | Date | |
|---|---|---|---|
| 41fde2566d |
@@ -146,13 +146,14 @@ frontend/
|
||||
|
||||
### Особенности админских вкладок
|
||||
|
||||
- Вкладка `groups` загружает кафедры, специальности, профили, учебные годы и календарные графики. Список групп открывается через `/api/groups?includeArchived=true`, поэтому в таблице видны активные, будущие, завершившие обучение и архивные группы со статусом. Группа создаётся через `/api/groups` с `specialtyId` и `specialtyProfileId`, блок подгрупп использует `/api/subgroups` и `/api/groups/{id}/subgroups`, а блок назначений использует `/api/groups/{id}/calendar-assignments`. В селекты подгрупп и назначений попадают только группы с `active=true`.
|
||||
- Вкладка `department-workspace` в блоке преподавателей объединяет данные `/api/department/teachers` и `/api/workload/teachers`: каждый преподаватель показывается одной карточкой с должностью и нагрузкой за выбранный период, преподаватели без занятий получают нулевую нагрузку, а преподаватели из расписания добавляются без дублей. Если дата начала периода выбрана позже даты окончания, поле окончания очищается, а расчёт нагрузки ждёт корректный период.
|
||||
- Вкладка `groups` загружает кафедры, специальности, профили, учебные годы и календарные графики. Список групп открывается через `/api/groups?includeArchived=true`, поэтому в таблице видны активные, будущие, завершившие обучение и архивные группы со статусом. Группа создаётся через `/api/groups` с `specialtyId` и `specialtyProfileId`, блок подгрупп использует `/api/subgroups` и `/api/groups/{id}/subgroups`, а блок назначений использует `/api/groups/{id}/calendar-assignments`. В селекты подгрупп и назначений попадают только группы с `active=true`. Действия группы выводятся в `group-actions` с фиксированными колонками, чтобы активные и архивные строки не смещали кнопки. Кнопка восстановления архивной группы использует стиль `btn-restore-group`: размеры и поведение как у архивной кнопки, но с жёлтым warning-акцентом.
|
||||
- Вкладка `department-workspace` в блоке преподавателей объединяет данные `/api/department/teachers` и `/api/workload/teachers`: каждый преподаватель показывается одной компактной карточкой с должностью и нагрузкой за выбранный период, преподаватели без занятий получают нулевую нагрузку, а преподаватели из расписания добавляются без дублей. Блоки загрузки дисциплин и преподавателей стоят в равных по высоте колонках; загрузка дисциплин использует фиксированную колонку кода и равномерные отступы до названия, а у карточки преподавателей задан стабильный зазор между заголовком и нагрузкой. Если дата начала периода выбрана позже даты окончания, поле окончания очищается, а расчёт нагрузки ждёт корректный период.
|
||||
- Вкладка `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`, где каждое правило содержит группы, отдельные часы и недели начала для лекций, лабораторных и практик, а также набор базовых слотов. Группы выбираются через выпадающий мультиселект. Поле подгруппы появляется только при выборе лабораторной работы; для лекций и практик оно не отображается. Если в правиле выбрана одна группа, селект подгруппы содержит пункт `Вся группа`; если выбрано несколько групп, лабораторный слот показывает мультиселект подгрупп, чтобы выбрать разные подгруппы разных групп. Список слотов отображается без внутреннего вертикального скролла: при добавлении строк форма расширяется вниз, а кнопка сохранения остаётся отдельным блоком под слотами. Из календарной системы здесь используется список семестров для выбора периода действия правила. Справа доступна сворачиваемая визуальная матрица: столбцы — выбранные в фильтре группы, строки — только день и время, где есть активные пары, ячейки показывают дисциплину, диапазон недель, тип, формат, преподавателя, аудиторию и подгруппы. Если нечётная и чётная недели отличаются, ячейка делится на две половины; одинаковые занятия схлопываются в цельную ячейку.
|
||||
- Вкладка `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`. Кнопка удаления заменена на архивирование: аудитория выводится из эксплуатации, но остаётся в историческом расписании. Для архивных аудиторий доступно восстановление.
|
||||
@@ -230,6 +231,8 @@ export const api = {
|
||||
- `TEACHER` → `/teacher/`
|
||||
- `STUDENT` → `/student/`
|
||||
|
||||
При неуспешной авторизации `script.js` оставляет введённый логин, очищает поле пароля и возвращает фокус в него.
|
||||
|
||||
### Проверка авторизации
|
||||
|
||||
На каждой странице проверяется наличие токена и роли:
|
||||
@@ -309,6 +312,10 @@ export function isAuthenticatedAsAdmin() {
|
||||
4. **`modals.css`** — Модальные окна
|
||||
5. **`departments-data.css`** — Стили создания кафедры/специальности
|
||||
|
||||
Мультиселекты с чекбоксами используют общий паттерн `.checkbox-item` + `.checkmark` + `.checkbox-label`; вкладка `classrooms` применяет его для выбора оборудования в создании и редактировании аудитории.
|
||||
|
||||
Дашборд использует классы `dashboard-container`, `dashboard-metrics-grid`, `dashboard-middle` и `dashboard-side-stack`: средний ряд растягивается по высоте самого высокого столбца, а правые компактные карточки выравниваются одинаковыми строками.
|
||||
|
||||
### Темизация
|
||||
|
||||
CSS-переменные позволяют поддерживать светлую/тёмную тему:
|
||||
@@ -338,6 +345,8 @@ CSS-переменные позволяют поддерживать светл
|
||||
- **Мобильные** (`≤768px`): sidebar скрывается за кнопкой-гамбургер, выезжает как overlay с затемнением
|
||||
- **Dropdown «Настройки»** в footer sidebar — содержит ссылку на страницу настроек и кнопку выхода
|
||||
|
||||
- **Секционные подписи sidebar** (`Расписание`, `Ресурсы`, `Система`) оформляются в `layout.css`, а их контраст для светлой и тёмной темы задаётся переменными `--sidebar-section-title` и `--sidebar-section-divider` в `main.css`.
|
||||
|
||||
---
|
||||
|
||||
## OpenTelemetry (`otel.js`)
|
||||
|
||||
@@ -48,6 +48,189 @@
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ===== Dashboard ===== */
|
||||
.dashboard-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.dashboard-metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(180px, 1fr));
|
||||
gap: 1.25rem;
|
||||
}
|
||||
|
||||
.dashboard-metric-card {
|
||||
--metric-color: var(--accent);
|
||||
--metric-bg: rgba(99, 102, 241, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
min-height: 112px;
|
||||
padding: 1.25rem;
|
||||
border-left: 4px solid var(--metric-color);
|
||||
}
|
||||
|
||||
.dashboard-metric-card--students {
|
||||
--metric-color: #10b981;
|
||||
--metric-bg: rgba(16, 185, 129, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-metric-card--teachers {
|
||||
--metric-color: #f59e0b;
|
||||
--metric-bg: rgba(245, 158, 11, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-metric-card--classrooms {
|
||||
--metric-color: #3b82f6;
|
||||
--metric-bg: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.dashboard-metric-card .metric-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: var(--metric-color);
|
||||
background: var(--metric-bg);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.dashboard-middle {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 2fr) minmax(320px, 1fr);
|
||||
gap: 1.25rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.dashboard-monitor-card,
|
||||
.dashboard-compact-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dashboard-monitor-card {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.dashboard-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dashboard-card-header h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dashboard-time-slot {
|
||||
flex: 0 0 auto;
|
||||
padding: 0.35rem 0.75rem;
|
||||
border-radius: 999px;
|
||||
color: var(--accent-hover);
|
||||
background: var(--bg-hover);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dashboard-monitor-table {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dashboard-side-stack {
|
||||
display: grid;
|
||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 1.25rem;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.dashboard-compact-card {
|
||||
min-height: 0;
|
||||
padding: 1.25rem;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.dashboard-card-title {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.dashboard-badge-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.dashboard-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.65rem;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.dashboard-action-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
min-height: 40px;
|
||||
padding: 0.6rem 0.85rem;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.dashboard-action-link.btn-secondary {
|
||||
color: var(--accent-hover);
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dashboard-action-link.btn-secondary:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-hover);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.dashboard-metrics-grid {
|
||||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.dashboard-middle {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-side-stack {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.dashboard-metrics-grid,
|
||||
.dashboard-side-stack {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.dashboard-card-header {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Form Structure ===== */
|
||||
.form-row {
|
||||
display: flex;
|
||||
@@ -167,14 +350,34 @@
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.department-workspace-grid {
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.department-subject-import-card,
|
||||
.department-teachers-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.department-teachers-card .card-header-row {
|
||||
margin-bottom: 0.85rem;
|
||||
}
|
||||
|
||||
.department-teachers-card .metric-grid {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.metric-card,
|
||||
.department-comment-item {
|
||||
padding: 0.85rem;
|
||||
padding: 0.8rem 0.85rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
@@ -183,7 +386,7 @@
|
||||
.metric-card strong,
|
||||
.department-comment-item strong {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
margin-bottom: 0.28rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -299,8 +502,9 @@
|
||||
|
||||
.subject-import-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 1fr) minmax(200px, 3fr) auto;
|
||||
gap: 0.75rem;
|
||||
grid-template-columns: 160px minmax(260px, 1fr) auto;
|
||||
column-gap: 0.65rem;
|
||||
row-gap: 0.75rem;
|
||||
align-items: end;
|
||||
padding: 0.75rem;
|
||||
background: var(--bg-input);
|
||||
@@ -323,16 +527,97 @@
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
.schedule-slot-summary,
|
||||
.schedule-semester-line {
|
||||
.schedule-slot-summary {
|
||||
margin-bottom: 0.35rem;
|
||||
}
|
||||
|
||||
.schedule-semester-line {
|
||||
display: flex;
|
||||
.academic-year-semesters {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
gap: 0.35rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.academic-years-table tbody td {
|
||||
padding-top: 0.6rem;
|
||||
padding-bottom: 0.6rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.academic-years-table thead th:first-child,
|
||||
.academic-years-table thead th:nth-child(2),
|
||||
.academic-years-table thead th:last-child {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.academic-year-center-cell {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.academic-year-semesters-cell {
|
||||
min-width: 520px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.academic-year-actions-cell {
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.academic-year-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
gap: 0.4rem;
|
||||
min-height: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.academic-year-semester-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.5rem 0.65rem;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.subject-import-code-field,
|
||||
.subject-import-name-field {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.subject-import-row .form-group label {
|
||||
margin-bottom: 0.45rem;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.academic-year-semesters {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.academic-year-semesters-cell {
|
||||
min-width: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
.academic-year-semester-meta {
|
||||
display: grid;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
|
||||
.academic-year-semester-type {
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.academic-year-semester-period {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.schedule-inline-table {
|
||||
@@ -1807,12 +2092,35 @@ input[type="number"] {
|
||||
box-shadow: 0 2px 10px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
.group-actions-cell {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 88px 104px 104px 116px;
|
||||
gap: 0.35rem;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.group-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-height: 28px;
|
||||
padding: 0.35rem 0.65rem;
|
||||
font-size: 0.82rem;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-delete,
|
||||
.btn-restore-group {
|
||||
padding: 0.35rem 0.7rem;
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
border: 1px solid rgba(248, 113, 113, 0.2);
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--error);
|
||||
font-family: inherit;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
@@ -1821,11 +2129,28 @@ input[type="number"] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: rgba(248, 113, 113, 0.1);
|
||||
border: 1px solid rgba(248, 113, 113, 0.2);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: rgba(248, 113, 113, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.btn-restore-group {
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid rgba(245, 158, 11, 0.24);
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
.btn-restore-group:hover {
|
||||
background: rgba(245, 158, 11, 0.2);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.btn-icon-toggle {
|
||||
background: transparent;
|
||||
border: 1px solid var(--bg-card-border);
|
||||
@@ -1864,6 +2189,12 @@ input[type="number"] {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.group-actions .group-action-btn {
|
||||
min-height: 28px;
|
||||
padding: 0.35rem 0.65rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
/* ===== Alerts ===== */
|
||||
.form-alert {
|
||||
display: none;
|
||||
|
||||
@@ -56,6 +56,21 @@
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar-section-divider {
|
||||
height: 1px;
|
||||
background: var(--sidebar-section-divider);
|
||||
margin: 0.5rem 1.5rem;
|
||||
}
|
||||
|
||||
.sidebar-section-title {
|
||||
padding: 0.5rem 1.25rem 0.25rem;
|
||||
color: var(--sidebar-section-title);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -307,6 +322,14 @@
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-section-title {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-section-divider {
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.sidebar.collapsed .btn-settings {
|
||||
justify-content: center;
|
||||
padding: 0.65rem 0;
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
--bg-input: rgba(255, 255, 255, 0.04);
|
||||
--bg-input-focus: rgba(255, 255, 255, 0.08);
|
||||
--bg-hover: rgba(255, 255, 255, 0.06);
|
||||
--sidebar-section-title: rgba(248, 250, 252, 0.45);
|
||||
--sidebar-section-divider: rgba(229, 231, 235, 0.1);
|
||||
|
||||
/* Typography */
|
||||
--text-primary: #f8fafc;
|
||||
@@ -48,6 +50,8 @@
|
||||
--bg-input: rgba(0, 0, 0, 0.03);
|
||||
--bg-input-focus: rgba(0, 0, 0, 0.06);
|
||||
--bg-hover: rgba(0, 0, 0, 0.05);
|
||||
--sidebar-section-title: rgba(51, 65, 85, 0.78);
|
||||
--sidebar-section-divider: rgba(148, 163, 184, 0.32);
|
||||
--text-primary: #0f172a;
|
||||
--text-secondary: #475569;
|
||||
--text-placeholder: #94a3b8;
|
||||
|
||||
@@ -16,19 +16,6 @@
|
||||
<link rel="stylesheet" href="css/modals.css">
|
||||
<link rel="stylesheet" href="css/departments-data.css">
|
||||
<link rel="stylesheet" href="css/auditorium-workload.css">
|
||||
<style>
|
||||
.sidebar.collapsed .sidebar-section-title {
|
||||
display: none !important;
|
||||
}
|
||||
.sidebar-section-divider {
|
||||
height: 1px;
|
||||
background: rgba(229, 231, 235, 0.1);
|
||||
margin: 0.5rem 1.5rem;
|
||||
}
|
||||
.sidebar.collapsed .sidebar-section-divider {
|
||||
margin: 0.5rem 0.5rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -81,7 +68,7 @@
|
||||
|
||||
<!-- Раздел: Расписание -->
|
||||
<div class="sidebar-section-divider"></div>
|
||||
<div class="sidebar-section-title" style="font-size: 0.7rem; font-weight: 700; color: rgba(255,255,255,0.4); padding: 0.5rem 1.25rem 0.25rem; text-transform: uppercase; letter-spacing: 0.05em;">Расписание</div>
|
||||
<div class="sidebar-section-title">Расписание</div>
|
||||
|
||||
<a href="#" class="nav-item" data-tab="schedule">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -121,7 +108,7 @@
|
||||
|
||||
<!-- Раздел: Субъекты и ресурсы -->
|
||||
<div class="sidebar-section-divider"></div>
|
||||
<div class="sidebar-section-title" style="font-size: 0.7rem; font-weight: 700; color: rgba(255,255,255,0.4); padding: 0.5rem 1.25rem 0.25rem; text-transform: uppercase; letter-spacing: 0.05em;">Ресурсы</div>
|
||||
<div class="sidebar-section-title">Ресурсы</div>
|
||||
|
||||
<a href="#" class="nav-item" data-tab="classrooms">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
@@ -156,7 +143,7 @@
|
||||
|
||||
<!-- Раздел: Система -->
|
||||
<div class="sidebar-section-divider"></div>
|
||||
<div class="sidebar-section-title" style="font-size: 0.7rem; font-weight: 700; color: rgba(255,255,255,0.4); padding: 0.5rem 1.25rem 0.25rem; text-transform: uppercase; letter-spacing: 0.05em;">Система</div>
|
||||
<div class="sidebar-section-title">Система</div>
|
||||
|
||||
<a href="#" class="nav-item" data-tab="users">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
|
||||
@@ -217,12 +217,14 @@ export async function initAcademicCalendar() {
|
||||
}
|
||||
academicYearsTbody.innerHTML = academicYears.map(year => `
|
||||
<tr>
|
||||
<td>${escapeHtml(year.title)}</td>
|
||||
<td>${escapeHtml(year.startDate)} - ${escapeHtml(year.endDate)}</td>
|
||||
<td>${renderSemesterList(year.semesters || [])}</td>
|
||||
<td>
|
||||
<td class="academic-year-center-cell academic-year-title-cell">${escapeHtml(year.title)}</td>
|
||||
<td class="academic-year-center-cell academic-year-period-cell">${escapeHtml(formatDisplayDateRange(year.startDate, year.endDate))}</td>
|
||||
<td class="academic-year-semesters-cell">${renderSemesterList(year.semesters || [])}</td>
|
||||
<td class="academic-year-actions-cell">
|
||||
<div class="academic-year-actions">
|
||||
<button class="btn-edit-classroom btn-edit-year" data-id="${year.id}">Изменить</button>
|
||||
<button class="btn-delete btn-delete-year" data-id="${year.id}">Удалить</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
@@ -230,12 +232,19 @@ export async function initAcademicCalendar() {
|
||||
|
||||
function renderSemesterList(items) {
|
||||
if (!items.length) return '<span class="muted">Нет семестров</span>';
|
||||
return items.map(semester => `
|
||||
<div class="schedule-semester-line">
|
||||
${escapeHtml(semesterLabel(semester))}
|
||||
<button class="btn-edit-classroom btn-edit-semester" data-id="${semester.id}">Изменить</button>
|
||||
return `<div class="academic-year-semesters">${items.map(semester => {
|
||||
const title = semesterTypeTitle(semester);
|
||||
const period = formatDisplayDateRange(semester.startDate, semester.endDate);
|
||||
return `
|
||||
<div class="academic-year-semester-item">
|
||||
<div class="academic-year-semester-meta">
|
||||
<span class="academic-year-semester-type">${escapeHtml(title)}</span>
|
||||
<span class="academic-year-semester-period">${escapeHtml(period)}</span>
|
||||
</div>
|
||||
`).join('');
|
||||
<button class="btn-edit-classroom btn-edit-semester" data-id="${semester.id}" aria-label="Изменить ${escapeHtml(title)} семестр ${escapeHtml(period)}">Изменить</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}</div>`;
|
||||
}
|
||||
|
||||
async function saveAcademicYear(event) {
|
||||
@@ -282,7 +291,7 @@ export async function initAcademicCalendar() {
|
||||
if (editSemesterButton) {
|
||||
const semester = semesters.find(item => item.id == editSemesterButton.dataset.id);
|
||||
if (!semester) return;
|
||||
semesterFormTitle.textContent = 'Редактирование семестра';
|
||||
semesterFormTitle.textContent = `Редактирование семестра: ${semesterTypeTitle(semester)}`;
|
||||
semesterIdInput.value = semester.id;
|
||||
semesterYearSelect.value = semester.academicYearId || '';
|
||||
semesterTypeSelect.value = normalizeSemesterType(semester.semesterType);
|
||||
@@ -1164,7 +1173,13 @@ export async function initAcademicCalendar() {
|
||||
|
||||
function semesterLabel(semester) {
|
||||
const type = normalizeSemesterType(semester.semesterType);
|
||||
return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${semester.startDate} - ${semester.endDate})`;
|
||||
return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${formatDisplayDateRange(semester.startDate, semester.endDate)})`;
|
||||
}
|
||||
|
||||
function semesterTypeTitle(semester) {
|
||||
const type = normalizeSemesterType(semester.semesterType);
|
||||
const label = SEMESTER_LABELS[type] || type || 'семестр';
|
||||
return label.charAt(0).toUpperCase() + label.slice(1);
|
||||
}
|
||||
|
||||
function normalizeSemesterType(type) {
|
||||
@@ -1195,6 +1210,16 @@ export async function initAcademicCalendar() {
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
|
||||
function formatDisplayDate(value) {
|
||||
const [year, month, day] = String(value || '').split('-');
|
||||
if (!year || !month || !day) return String(value || '');
|
||||
return `${day}-${month}-${year}`;
|
||||
}
|
||||
|
||||
function formatDisplayDateRange(startDate, endDate) {
|
||||
return `${formatDisplayDate(startDate)} — ${formatDisplayDate(endDate)}`;
|
||||
}
|
||||
|
||||
function dateRangeCaption(rows) {
|
||||
if (!rows.length) return '';
|
||||
const dates = rows.map(row => row.date).sort();
|
||||
|
||||
@@ -13,6 +13,7 @@ async function fetchEquipments() {
|
||||
function renderEquipmentCheckboxes(equipments, containerId, selectTextId, selectedIds = []) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (!container) return;
|
||||
const selected = new Set(selectedIds.map(id => String(id)));
|
||||
|
||||
if (!equipments || !equipments.length) {
|
||||
container.innerHTML = '<p class="text-muted"><small>Оборудование отсутствует</small></p>';
|
||||
@@ -21,9 +22,10 @@ function renderEquipmentCheckboxes(equipments, containerId, selectTextId, select
|
||||
}
|
||||
|
||||
container.innerHTML = equipments.map(eq => `
|
||||
<label class="checkbox-container">
|
||||
<input type="checkbox" value="${eq.id}" ${selectedIds.includes(eq.id) ? 'checked' : ''}>
|
||||
<span>${escapeHtml(eq.name)}</span>
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="${escapeHtml(eq.id)}" ${selected.has(String(eq.id)) ? 'checked' : ''}>
|
||||
<span class="checkmark"></span>
|
||||
<span class="checkbox-label">${escapeHtml(eq.name)}</span>
|
||||
</label>
|
||||
`).join('');
|
||||
|
||||
|
||||
@@ -112,11 +112,11 @@ export async function initDepartmentWorkspace() {
|
||||
if (!importRowsContainer) return;
|
||||
importRowsContainer.innerHTML = (rows || [{ code: '', name: '' }]).map((row, index) => `
|
||||
<div class="subject-import-row" data-index="${index}">
|
||||
<div class="form-group">
|
||||
<div class="form-group subject-import-code-field">
|
||||
<label>Код</label>
|
||||
<input type="text" class="subject-import-code" value="${escapeHtml(row.code || '')}" placeholder="DB201">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-group subject-import-name-field">
|
||||
<label>Название</label>
|
||||
<input type="text" class="subject-import-name" value="${escapeHtml(row.name || '')}" placeholder="Базы данных">
|
||||
</div>
|
||||
|
||||
@@ -171,13 +171,15 @@ export async function initGroups() {
|
||||
<td>${escapeHtml(specialityLabel(group.specialtyId || group.specialityCode))}</td>
|
||||
<td>${escapeHtml(group.specialtyProfileName || '-')}</td>
|
||||
<td><span class="badge ${statusBadgeClass(group)}">${escapeHtml(statusLabel(group))}</span></td>
|
||||
<td style="text-align: right; white-space: nowrap;">
|
||||
<button class="btn-edit-classroom btn-edit-group" data-id="${group.id}">Изменить</button>
|
||||
<button class="btn-primary btn-manage-subgroups" data-id="${group.id}" style="padding: 0.25rem 0.5rem; font-size: 0.85rem;">Подгруппы</button>
|
||||
<button class="btn-primary btn-manage-calendar" data-id="${group.id}" style="padding: 0.25rem 0.5rem; font-size: 0.85rem;">Календарь</button>
|
||||
<td class="group-actions-cell">
|
||||
<div class="group-actions">
|
||||
<button class="btn-edit-classroom btn-edit-group group-action-btn" data-id="${group.id}">Изменить</button>
|
||||
<button class="btn-primary btn-manage-subgroups group-action-btn" data-id="${group.id}">Подгруппы</button>
|
||||
<button class="btn-primary btn-manage-calendar group-action-btn" data-id="${group.id}">Календарь</button>
|
||||
${group.status === 'ARCHIVED'
|
||||
? `<button class="btn-restore-group" data-id="${group.id}" style="margin-left: 0.5rem;">Восстановить</button>`
|
||||
: `<button class="btn-delete" data-id="${group.id}" style="margin-left: 0.5rem;">Архив</button>`}
|
||||
? `<button class="btn-restore-group group-action-btn" data-id="${group.id}">Восстановить</button>`
|
||||
: `<button class="btn-delete group-action-btn" data-id="${group.id}">Архив</button>`}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
|
||||
@@ -56,6 +56,7 @@ const LESSON_TYPE_LIMITS = [
|
||||
startWeekInputId: 'schedule-rule-practice-start-week'
|
||||
}
|
||||
];
|
||||
const LESSON_TYPE_ORDER = new Map(LESSON_TYPE_LIMITS.map((config, index) => [config.key, index]));
|
||||
|
||||
export async function initSchedule() {
|
||||
const ruleForm = document.getElementById('schedule-rule-form');
|
||||
@@ -186,6 +187,7 @@ export async function initSchedule() {
|
||||
api.get('/api/lesson-types'),
|
||||
api.get('/api/subgroups')
|
||||
]);
|
||||
lessonTypes = sortLessonTypes(lessonTypes);
|
||||
populateSubjectSelects();
|
||||
}
|
||||
|
||||
@@ -1022,6 +1024,15 @@ export async function initSchedule() {
|
||||
return { value: lessonType.id, label: lessonType.name || lessonType.lessonType || `ID ${lessonType.id}` };
|
||||
}
|
||||
|
||||
function sortLessonTypes(items) {
|
||||
return [...items].sort((first, second) => {
|
||||
const firstOrder = LESSON_TYPE_ORDER.get(lessonTypeKey(first)) ?? LESSON_TYPE_ORDER.size;
|
||||
const secondOrder = LESSON_TYPE_ORDER.get(lessonTypeKey(second)) ?? LESSON_TYPE_ORDER.size;
|
||||
return firstOrder - secondOrder
|
||||
|| naturalCompare(first.name || first.lessonType, second.name || second.lessonType);
|
||||
});
|
||||
}
|
||||
|
||||
function lessonTypeKeyById(lessonTypeId) {
|
||||
const lessonType = lessonTypes.find(item => String(item.id) === String(lessonTypeId));
|
||||
return lessonType ? lessonTypeKey(lessonType) : null;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<button class="btn-primary" id="academic-years-refresh">Обновить</button>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<table class="academic-years-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название</th>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- ===== Dashboard Page ===== -->
|
||||
<div class="dashboard-container" style="display: flex; flex-direction: column; gap: 1.5rem;">
|
||||
<div class="dashboard-container">
|
||||
<!-- Метрики (Информационные карточки) -->
|
||||
<div class="metrics-grid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.25rem;">
|
||||
<div class="metric-card card" style="display: flex; align-items: center; gap: 1rem; padding: 1.25rem; border-left: 4px solid var(--primary);">
|
||||
<div class="metric-icon" style="background: rgba(99, 102, 241, 0.1); color: var(--primary); padding: 0.75rem; border-radius: 12px; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="dashboard-metrics-grid">
|
||||
<div class="card dashboard-metric-card dashboard-metric-card--groups">
|
||||
<div class="metric-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
@@ -17,8 +17,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card card" style="display: flex; align-items: center; gap: 1rem; padding: 1.25rem; border-left: 4px solid #10b981;">
|
||||
<div class="metric-icon" style="background: rgba(16, 185, 129, 0.1); color: #10b981; padding: 0.75rem; border-radius: 12px; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="card dashboard-metric-card dashboard-metric-card--students">
|
||||
<div class="metric-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
@@ -32,8 +32,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card card" style="display: flex; align-items: center; gap: 1rem; padding: 1.25rem; border-left: 4px solid #f59e0b;">
|
||||
<div class="metric-icon" style="background: rgba(245, 158, 11, 0.1); color: #f59e0b; padding: 0.75rem; border-radius: 12px; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="card dashboard-metric-card dashboard-metric-card--teachers">
|
||||
<div class="metric-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
@@ -45,8 +45,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-card card" style="display: flex; align-items: center; gap: 1rem; padding: 1.25rem; border-left: 4px solid #3b82f6;">
|
||||
<div class="metric-icon" style="background: rgba(59, 130, 246, 0.1); color: #3b82f6; padding: 0.75rem; border-radius: 12px; display: flex; align-items: center; justify-content: center;">
|
||||
<div class="card dashboard-metric-card dashboard-metric-card--classrooms">
|
||||
<div class="metric-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M9 3v18M15 3v18M3 9h18M3 15h18" />
|
||||
@@ -60,14 +60,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Мониторинг в реальном времени и Быстрый переход -->
|
||||
<div class="dashboard-middle" style="display: grid; grid-template-columns: 2fr 1fr; gap: 1.25rem; align-items: start;">
|
||||
<div class="dashboard-middle">
|
||||
<!-- Идет сейчас -->
|
||||
<div class="card" style="min-height: 250px; display: flex; flex-direction: column;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;">
|
||||
<div class="card dashboard-monitor-card">
|
||||
<div class="dashboard-card-header">
|
||||
<h2 style="margin: 0;">Мониторинг пар (Идет сейчас)</h2>
|
||||
<div id="current-time-slot" style="background: var(--bg-secondary); padding: 0.35rem 0.75rem; border-radius: 20px; font-size: 0.85rem; font-weight: 600; color: var(--primary);">—</div>
|
||||
<div id="current-time-slot" class="dashboard-time-slot">—</div>
|
||||
</div>
|
||||
<div class="table-wrap" style="flex: 1;">
|
||||
<div class="table-wrap dashboard-monitor-table">
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -87,20 +87,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Свободные аудитории и Быстрые действия -->
|
||||
<div style="display: flex; flex-direction: column; gap: 1.25rem;">
|
||||
<div class="dashboard-side-stack">
|
||||
<!-- Свободные аудитории -->
|
||||
<div class="card" style="padding: 1.25rem; display: flex; flex-direction: column; gap: 0.75rem;">
|
||||
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 600;">Свободные аудитории (ближайшие)</h3>
|
||||
<div id="free-classrooms-list" style="display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<div class="card dashboard-compact-card">
|
||||
<h3 class="dashboard-card-title">Свободные аудитории (ближайшие)</h3>
|
||||
<div id="free-classrooms-list" class="dashboard-badge-list">
|
||||
<div style="color: var(--text-secondary); font-size: 0.9rem;">Загрузка...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Быстрые действия -->
|
||||
<div class="card" style="padding: 1.25rem; display: flex; flex-direction: column; gap: 0.75rem;">
|
||||
<h3 style="margin: 0; font-size: 1.1rem; font-weight: 600;">Быстрый переход</h3>
|
||||
<div style="display: grid; grid-template-columns: 1fr; gap: 0.5rem; margin-top: 0.5rem;">
|
||||
<a href="#schedule" class="btn-primary" style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; text-decoration: none; text-align: center; font-size: 0.9rem; padding: 0.6rem;">
|
||||
<div class="card dashboard-compact-card">
|
||||
<h3 class="dashboard-card-title">Быстрый переход</h3>
|
||||
<div class="dashboard-actions">
|
||||
<a href="#schedule" class="btn-primary dashboard-action-link">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||||
@@ -109,7 +109,7 @@
|
||||
</svg>
|
||||
Конструктор расписания
|
||||
</a>
|
||||
<a href="#schedule-view" class="btn-secondary" style="display: flex; align-items: center; justify-content: center; gap: 0.5rem; text-decoration: none; text-align: center; font-size: 0.9rem; padding: 0.6rem;">
|
||||
<a href="#schedule-view" class="btn-secondary dashboard-action-link">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
|
||||
<div class="department-workspace-grid">
|
||||
<div class="card create-card">
|
||||
<div class="card create-card department-subject-import-card">
|
||||
<div class="card-header-row">
|
||||
<h2>Загрузка дисциплин</h2>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card department-teachers-card">
|
||||
<div class="card-header-row">
|
||||
<h2>Преподаватели и нагрузка</h2>
|
||||
</div>
|
||||
|
||||
@@ -155,6 +155,8 @@
|
||||
setTimeout(() => { window.location.href = redirect; }, 400);
|
||||
} else {
|
||||
showAlert(data.message || 'Неверное имя пользователя или пароль', 'error');
|
||||
passwordInput.value = '';
|
||||
passwordInput.focus();
|
||||
}
|
||||
} catch (err) {
|
||||
showAlert('Ошибка соединения с сервером', 'error');
|
||||
|
||||
Reference in New Issue
Block a user