Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на 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
132 lines
5.3 KiB
JavaScript
132 lines
5.3 KiB
JavaScript
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);
|
||
}
|