Много разных изменений

This commit is contained in:
Zuev
2026-08-20 00:55:27 +03:00
parent 3599d850f6
commit a1c4ee4298
78 changed files with 4698 additions and 428 deletions

View File

@@ -1,13 +1,29 @@
import { api } from '../api.js';
import {
academicYearsFromSemesters,
resolveAcademicPeriod,
semestersForAcademicYear
} from '../academic-period.js';
import { escapeHtml } from '../utils.js';
import { trackUxEvent } from '../../../telemetry.js';
let qualityState = null;
export async function initScheduleQuality() {
const root = document.querySelector('.quality-lab');
if (!root) return;
qualityState = { root, analysis: null, versions: [], visibleProblems: [], selectedProblemId: null, applying: false };
qualityState = {
root,
analysis: null,
semesters: [],
academicYearId: '',
versions: [],
visibleProblems: [],
selectedProblemId: null,
applying: false
};
root.querySelector('#quality-run')?.addEventListener('click', runAnalysis);
root.querySelector('#quality-year')?.addEventListener('change', handleYearChange);
root.querySelector('#quality-semester')?.addEventListener('change', loadVersions);
root.querySelector('#quality-severity-filter')?.addEventListener('change', renderProblems);
root.querySelector('#quality-type-filter')?.addEventListener('change', renderProblems);
@@ -20,26 +36,18 @@ async function loadSemesters() {
const select = qualityState?.root.querySelector('#quality-semester');
if (!select) return;
try {
const semesters = await api.get('/api/schedule/semesters');
select.innerHTML = (semesters || []).map(semester => `
<option value="${escapeHtml(String(semester.id))}">
${escapeHtml(semesterLabel(semester))}
</option>
`).join('');
if (!semesters?.length) {
select.innerHTML = '<option value="">Семестры не настроены</option>';
qualityState.semesters = await api.get('/api/schedule/semesters');
if (!qualityState.semesters?.length) {
renderPeriodSelects('', '');
qualityStatus('Сначала создайте учебный год и семестр.', 'warning');
return;
}
const today = localIsoDate(new Date());
const current = semesters.find(semester =>
semester.startDate <= today && semester.endDate >= today
);
select.value = String(current?.id ?? semesters[0].id);
const savedSemesterId = localStorage.getItem('magistr.quality.semesterId');
if (semesters.some(semester => String(semester.id) === savedSemesterId)) {
select.value = savedSemesterId;
}
const selected = resolveAcademicPeriod(qualityState.semesters, {
academicYearId: localStorage.getItem('magistr.quality.yearId'),
semesterId: localStorage.getItem('magistr.quality.semesterId'),
date: localIsoDate(new Date())
});
renderPeriodSelects(selected.academicYearId, selected.semesterId);
await loadVersions();
} catch (error) {
select.innerHTML = '<option value="">Ошибка загрузки</option>';
@@ -47,6 +55,39 @@ async function loadSemesters() {
}
}
async function handleYearChange() {
const yearId = qualityState?.root.querySelector('#quality-year')?.value || '';
const selected = resolveAcademicPeriod(qualityState.semesters, {
academicYearId: yearId,
date: localIsoDate(new Date())
});
renderPeriodSelects(yearId, selected.semesterId);
await loadVersions();
}
function renderPeriodSelects(academicYearId, semesterId) {
const yearSelect = qualityState?.root.querySelector('#quality-year');
const semesterSelect = qualityState?.root.querySelector('#quality-semester');
if (!yearSelect || !semesterSelect) return;
const years = academicYearsFromSemesters(qualityState.semesters);
qualityState.academicYearId = String(academicYearId || years[0]?.id || '');
yearSelect.innerHTML = years.length
? years.map(year => `<option value="${escapeHtml(year.id)}">${escapeHtml(year.title)}</option>`).join('')
: '<option value="">Учебные годы не настроены</option>';
yearSelect.value = qualityState.academicYearId;
const yearSemesters = semestersForAcademicYear(qualityState.semesters, qualityState.academicYearId);
semesterSelect.innerHTML = yearSemesters.length
? yearSemesters.map(semester => `
<option value="${escapeHtml(String(semester.id))}">${escapeHtml(semesterLabel(semester))}</option>
`).join('')
: '<option value="">Семестры не настроены</option>';
const selectedSemester = yearSemesters.find(semester => String(semester.id) === String(semesterId))
|| yearSemesters[0];
semesterSelect.value = String(selectedSemester?.id || '');
localStorage.setItem('magistr.quality.yearId', qualityState.academicYearId);
}
async function loadVersions() {
const semesterId = qualityState?.root.querySelector('#quality-semester')?.value;
const select = qualityState?.root.querySelector('#quality-version');
@@ -69,6 +110,7 @@ async function loadVersions() {
const saved = qualityState.versions.find(version => String(version.id) === savedVersionId);
const published = qualityState.versions.find(version => version.status === 'PUBLISHED');
select.value = String((saved || published || qualityState.versions[0]).id);
localStorage.setItem('magistr.quality.yearId', qualityState.academicYearId);
localStorage.setItem('magistr.quality.semesterId', semesterId);
} catch (error) {
select.innerHTML = '<option value="">Ошибка загрузки</option>';
@@ -100,6 +142,11 @@ async function runAnalysis() {
);
qualityState.selectedProblemId = null;
renderAnalysis();
trackUxEvent('качество_расписания_проверено', {
semesterId,
versionId,
score: Number(qualityState.analysis.score || 0)
});
qualityStatus(
qualityState.analysis.scheduleVersionStatus === 'DRAFT'
? 'Черновик проверен. Проблемные правила можно открыть в конструкторе до публикации.'
@@ -261,6 +308,7 @@ function handleRulePanelClick(event) {
function openRuleInConstructor(problem) {
if (!problem?.scheduleRuleId || qualityState?.analysis?.scheduleVersionStatus === 'ARCHIVED') return;
localStorage.setItem('magistr.schedule.semesterId', String(qualityState.analysis.semesterId));
localStorage.setItem('magistr.schedule.openVersionId', String(qualityState.analysis.scheduleVersionId));
localStorage.setItem('magistr.schedule.openRuleId', String(problem.scheduleRuleId));
document.querySelector('.nav-item[data-tab="schedule"]')?.click();