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
This commit is contained in:
131
frontend/react/admin/admin-service.js
Normal file
131
frontend/react/admin/admin-service.js
Normal file
@@ -0,0 +1,131 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user