Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на 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
96 lines
4.6 KiB
JavaScript
96 lines
4.6 KiB
JavaScript
import { apiClient } from '../shared/api-client.js';
|
||
import { formatLocalDate } from '../../admin/js/utils.js';
|
||
import { fetchScheduleSemesters, fetchScheduleVersions } from './schedule-versions-service.js';
|
||
|
||
// Срез «Анализ качества расписания» (этап 5, волна 5): чистая логика и контракты
|
||
// API, перенесённые из legacy admin/js/views/schedule-quality.js без изменения поведения.
|
||
|
||
// --- Диапазоны оценки ---
|
||
|
||
export function qualityBand(score) {
|
||
if (score >= 90) {
|
||
return { code: 'excellent', label: 'Сильное расписание', summary: 'Найдены только локальные неудобства с небольшим влиянием.' };
|
||
}
|
||
if (score >= 75) {
|
||
return { code: 'stable', label: 'Устойчивое расписание', summary: 'Расписание работоспособно, но несколько перестановок заметно улучшат комфорт.' };
|
||
}
|
||
if (score >= 55) {
|
||
return { code: 'attention', label: 'Нужна доработка', summary: 'Мягкие ограничения и распределение нагрузки требуют внимания учебного отдела.' };
|
||
}
|
||
return { code: 'critical', label: 'Высокая цена неудобств', summary: 'Сначала разберите критические проблемы и перегруженные дни.' };
|
||
}
|
||
|
||
// --- Фильтрация проблем ---
|
||
|
||
export function filterQualityProblems(problems, severity = '', category = '') {
|
||
return (problems || []).filter(problem => {
|
||
const severityMatches = !severity || problem.severity === severity;
|
||
const problemTypes = problem.problemTypes?.length ? problem.problemTypes : [problem.type];
|
||
const categoryMatches = !category || problemTypes.some(type => problemCategory(type) === category);
|
||
return severityMatches && categoryMatches;
|
||
}).sort((a, b) => (Number(b.penalty) || 0) - (Number(a.penalty) || 0));
|
||
}
|
||
|
||
function problemCategory(type = '') {
|
||
if (type.includes('GAP') || type.includes('COMPACTNESS')) return 'GAPS';
|
||
if (type.includes('LOAD')) return 'LOAD';
|
||
if (type.includes('ROOM')) return 'ROOM';
|
||
return 'PREFERENCE';
|
||
}
|
||
|
||
// --- Подписи ---
|
||
|
||
export function versionStatusLabel(status) {
|
||
return { DRAFT: 'Черновик', PUBLISHED: 'Опубликована', ARCHIVED: 'Архив' }[status] || 'Версия';
|
||
}
|
||
|
||
export function severityLabel(severity) {
|
||
return { CRITICAL: 'Критично', HIGH: 'Высокая', MEDIUM: 'Средняя', LOW: 'Низкая' }[severity] || severity;
|
||
}
|
||
|
||
export function formatMetricValue(metric) {
|
||
const value = Number(metric?.value || 0);
|
||
const formatted = Number.isInteger(value)
|
||
? String(value)
|
||
: value.toLocaleString('ru-RU', { maximumFractionDigits: 1 });
|
||
return `${formatted}${metric?.unit === '%' ? '%' : metric?.unit ? ` ${metric.unit}` : ''}`;
|
||
}
|
||
|
||
export function qualitySemesterLabel(semester) {
|
||
const type = semester.semesterType === 'autumn' ? 'Осенний' : 'Весенний';
|
||
return `${semester.academicYearTitle} · ${type} · ${formatQualityDate(semester.startDate)}—${formatQualityDate(semester.endDate)}`;
|
||
}
|
||
|
||
export function formatQualityDate(value) {
|
||
if (!value) return 'дата не указана';
|
||
const [year, month, day] = String(value).split('-');
|
||
return `${day}.${month}.${year}`;
|
||
}
|
||
|
||
export function lessonWord(count) {
|
||
const mod100 = count % 100;
|
||
const mod10 = count % 10;
|
||
if (mod100 >= 11 && mod100 <= 14) return 'занятий';
|
||
if (mod10 === 1) return 'занятие';
|
||
if (mod10 >= 2 && mod10 <= 4) return 'занятия';
|
||
return 'занятий';
|
||
}
|
||
|
||
export function problemTotalLabel(analysis) {
|
||
return analysis.problemsTruncated
|
||
? `Показаны первые ${analysis.problems.length} из ${analysis.totalProblemCount} правил`
|
||
: `Правил с проблемами: ${analysis.totalProblemCount}`;
|
||
}
|
||
|
||
// --- Контракты API ---
|
||
|
||
export function scheduleQualityEndpoint(semesterId, versionId) {
|
||
return `/api/edu-office/schedule/quality?semesterId=${encodeURIComponent(semesterId)}&versionId=${encodeURIComponent(versionId)}`;
|
||
}
|
||
|
||
export async function fetchScheduleQualityAnalysis(semesterId, versionId, client = apiClient) {
|
||
return client.get(scheduleQualityEndpoint(semesterId, versionId), { errorMessage: 'Не удалось выполнить анализ качества.' });
|
||
}
|
||
|
||
export { fetchScheduleSemesters, fetchScheduleVersions, formatLocalDate };
|