Files
magistr/frontend/react/admin/admin-service.js
dipatrik10 97bf3cc50a feat: завершить миграцию фронтенда на React (этапы 1-5)
Админка, настройки, логин, кабинеты преподавателя и студента
полностью переведены на 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
2026-09-28 23:15:04 +03:00

132 lines
5.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { apiClient } from '../shared/api-client.js';
import { capabilitiesForRole } from '../../admin/js/role-capabilities.js';
// Вкладки, перенесённые на React (см. react-tabs.js), хранят только заголовок:
// их содержимое рендерят React-компоненты, HTML-фрагменты не загружаются.
export const ADMIN_ROUTES = Object.freeze({
dashboard: { title: 'Дашборд' },
users: { title: 'Управление пользователями' },
'teacher-requests': { title: 'Заявки' },
groups: { title: 'Управление группами' },
classrooms: { title: 'Аудиторный фонд' },
subjects: { title: 'Дисциплины' },
'university-structure': { title: 'Структура вуза' },
'department-workspace': { title: 'Кабинет кафедры' },
'schedule-view': { title: 'Просмотр расписаний' },
schedule: { title: 'Конструктор правил' },
'schedule-versions': { title: 'Версии расписания' },
'schedule-quality': { title: 'Анализ качества расписания' },
'academic-calendar': { title: 'Календарный учебный график' },
'auditorium-workload': { title: 'Загруженность' },
'teacher-absences': { title: 'Запросы преподавателей' }
});
const NAV_ITEMS = Object.freeze({
dashboard: { navLabel: 'Дашборд' },
'teacher-requests': { navLabel: 'Заявки' },
'department-workspace': { navLabel: 'Кабинет кафедры' },
schedule: { navLabel: 'Конструктор правил' },
'schedule-versions': { navLabel: 'Версии расписания' },
'schedule-quality': { navLabel: 'Качество расписания' },
'academic-calendar': { navLabel: 'Календарный график' },
'auditorium-workload': { navLabel: 'Загруженность' },
'schedule-view': { navLabel: 'Просмотр расписаний' },
'teacher-absences': { navLabel: 'Запросы преподавателей' },
classrooms: { navLabel: 'Аудиторный фонд' },
groups: { navLabel: 'Группы обучения' },
'university-structure': { navLabel: 'Структура вуза' },
subjects: { navLabel: 'Дисциплины' },
users: { navLabel: 'Пользователи' }
});
const NAV_SECTIONS = Object.freeze([
{ title: '', items: Object.freeze(['dashboard', 'teacher-requests', 'department-workspace']) },
{
title: 'Расписание',
items: Object.freeze([
'schedule',
'schedule-versions',
'schedule-quality',
'academic-calendar',
'auditorium-workload',
'schedule-view',
'teacher-absences'
])
},
{
title: 'Ресурсы',
items: Object.freeze(['classrooms', 'groups', 'university-structure', 'subjects'])
},
{ title: 'Система', items: Object.freeze(['users']) }
]);
const LOGO_TITLES = Object.freeze({
ADMIN: 'Magistr',
EDUCATION_OFFICE: 'Учебный отдел',
DEPARTMENT: 'Кафедра',
SCHEDULE_VIEWER: 'Расписания'
});
export function allowedAdminTabs(role) {
const capabilities = capabilitiesForRole(role);
return new Set(capabilities ? capabilities.adminTabs : []);
}
export function navSectionsForRole(role) {
const allowed = allowedAdminTabs(role);
return NAV_SECTIONS
.map(section => ({
title: section.title,
items: section.items
.filter(tab => allowed.has(tab))
.map(tab => ({ id: tab, label: NAV_ITEMS[tab].navLabel }))
}))
.filter(section => section.items.length > 0);
}
export function logoTitleForRole(role) {
return LOGO_TITLES[role] || 'Magistr';
}
export function defaultAdminTabForRole(role) {
const capabilities = capabilitiesForRole(role);
return capabilities ? capabilities.defaultAdminTab : null;
}
export function tabFromHash(hash, role) {
const allowed = allowedAdminTabs(role);
const candidate = String(hash || '').replace('#', '');
if (candidate && allowed.has(candidate)) return candidate;
return defaultAdminTabForRole(role);
}
export function requestCountLabel(count) {
const value = Number(count) || 0;
const mod10 = value % 10;
const mod100 = value % 100;
if (mod10 === 1 && mod100 !== 11) return `${value} заявка`;
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return `${value} заявки`;
return `${value} заявок`;
}
export function navBadgeLabel(count) {
const value = Number(count) || 0;
return value > 99 ? '99+' : String(value);
}
export function badgeVisibility(count) {
const value = Number(count) || 0;
return {
hidden: value <= 0,
ariaLabel: value > 0 ? `Заявок в очереди: ${value}` : 'Заявок в очереди нет'
};
}
export async function fetchPendingRequestsCount(client = apiClient) {
const page = await client.get(
'/api/teacher-requests/page?status=PENDING&page=0&size=1',
{ errorMessage: 'Не удалось загрузить счётчик заявок' }
);
return Number(page?.totalItems || 0);
}