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 };