много всего и тестовые данные

This commit is contained in:
Zuev
2026-09-06 19:17:06 +03:00
parent a1c4ee4298
commit e3b6797713
39 changed files with 2121 additions and 521 deletions

View File

@@ -1,4 +1,7 @@
import { api } from '../api.js';
import { AsyncCombobox } from '../async-combobox.js';
import { renderPagination } from '../pagination.js';
import { debounce } from '../url-state.js';
import { bindDateInputMask, readDateInputIso, setDateInputIso } from '../date-input.js';
import { escapeHtml, formatLocalDate, showAlert, hideAlert } from '../utils.js';
import { academicCalendarWeekNumber } from './academic-calendar-grid.js';
@@ -209,6 +212,14 @@ export async function initAcademicCalendar() {
let studyForms = [];
let activityTypes = [];
let calendars = [];
let calendarPage = 0;
let calendarSize = 25;
let calendarRequest = 0;
const calendarCache = new Map();
const calendarComboboxes = new Map();
const calendarSearch = document.getElementById('academic-calendars-search');
const calendarYearFilter = document.getElementById('academic-calendars-year-filter');
const calendarPagination = document.getElementById('academic-calendars-pagination');
let subjects = [];
let calendarSubjectRows = [];
let timeSlotScopes = [];
@@ -224,9 +235,27 @@ export async function initAcademicCalendar() {
let calendarDayTooltipPositionAnimation = null;
let loadedGridCalendarId = '';
let loadedSubjectCalendarId = '';
let subjectLoadRequest = 0;
let gridLoadRequest = 0;
const gridDirtyScope = createDirtyScope('academic-calendar-grid', 'В календарной сетке есть несохранённые изменения. Загрузить другой график и потерять их?');
const subjectDirtyScope = createDirtyScope('academic-calendar-subjects', 'В привязках дисциплин есть несохранённые изменения. Продолжить без сохранения?');
[editorCalendarSelect, subjectCalendarSelect].filter(Boolean).forEach(select => {
select.innerHTML = '<option value="">Выберите график</option>';
calendarComboboxes.set(select, new AsyncCombobox(select, {
placeholder: 'Поиск по направлению, профилю, форме или году',
optionLabel: calendar => buildAcademicCalendarTitle(calendar) || calendar.title,
optionDescription: calendar => calendar.specialtyName || '',
load: async (query, selectedId) => {
const params = new URLSearchParams({ query, limit: 20 });
if (selectedId) params.set('selectedId', selectedId);
const items = await api.get(`/api/admin/academic-calendars/options?${params}`);
items.forEach(calendar => calendarCache.set(String(calendar.id), calendar));
return items;
}
}));
});
bindEvents();
try {
@@ -261,6 +290,14 @@ export async function initAcademicCalendar() {
});
});
academicYearsRefreshButton?.addEventListener('click', loadYears);
calendarSearch?.addEventListener('input', debounce(() => {
calendarPage = 0;
loadCalendars();
}, 300));
calendarYearFilter?.addEventListener('change', () => {
calendarPage = 0;
loadCalendars();
});
calendarsRefreshButton?.addEventListener('click', loadCalendars);
academicYearResetButton?.addEventListener('click', resetAcademicYearForm);
academicYearStartInput?.addEventListener('input', handleAcademicYearStartInput);
@@ -363,6 +400,11 @@ export async function initAcademicCalendar() {
academicYears = await api.get('/api/admin/calendar/years');
renderAcademicYears();
populateYearSelects();
const selectedYear = calendarYearFilter.value;
calendarYearFilter.innerHTML = '<option value="">Все учебные годы</option>' + academicYears.map(year =>
`<option value="${year.id}">${escapeHtml(year.title)}</option>`).join('');
calendarYearFilter.value = selectedYear;
refreshSelectVisuals(calendarYearFilter);
} catch (error) {
academicYearsTbody.innerHTML = `<tr><td colspan="4" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
throw error;
@@ -566,13 +608,28 @@ export async function initAcademicCalendar() {
}
async function loadCalendars() {
const request = ++calendarRequest;
calendarPagination?.replaceChildren();
calendarsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Загрузка...</td></tr>';
try {
calendars = await api.get('/api/admin/academic-calendars');
const params = new URLSearchParams({ page: calendarPage, size: calendarSize, query: calendarSearch?.value.trim() || '' });
if (calendarYearFilter?.value) params.set('academicYearId', calendarYearFilter.value);
const page = await api.get(`/api/admin/academic-calendars/page?${params}`);
if (request !== calendarRequest) return;
if (calendarPage > 0 && calendarPage >= page.totalPages) {
calendarPage = Math.max(0, page.totalPages - 1);
return loadCalendars();
}
calendars = page.items;
calendars.forEach(calendar => calendarCache.set(String(calendar.id), calendar));
renderCalendars();
populateEditorCalendarSelect();
populateSubjectCalendarSelect();
renderPagination(calendarPagination, page, {
label: 'графиков',
onPage: value => { calendarPage = value; loadCalendars(); },
onSize: value => { calendarSize = value; calendarPage = 0; loadCalendars(); }
});
} catch (error) {
if (request !== calendarRequest) return;
calendarsTbody.innerHTML = `<tr><td colspan="6" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
throw error;
}
@@ -580,7 +637,7 @@ export async function initAcademicCalendar() {
function renderCalendars() {
if (!calendars.length) {
calendarsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Календарные графики не созданы</td></tr>';
calendarsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Календарные графики не найдены</td></tr>';
return;
}
calendarsTbody.innerHTML = calendars.map(calendar => `
@@ -620,6 +677,10 @@ export async function initAcademicCalendar() {
const saved = id
? await api.put('/api/admin/academic-calendars/' + id, payload)
: await api.post('/api/admin/academic-calendars', payload);
calendarCache.set(String(saved.id), saved);
calendarComboboxes.forEach((combobox, select) => {
if (select.value === String(saved.id)) setCalendarSelection(select, saved.id);
});
const savedTitle = buildAcademicCalendarTitle(saved);
showAlert('academic-calendar-alert', `График "${escapeHtml(savedTitle)}" сохранён`, 'success');
resetCalendarForm();
@@ -654,7 +715,7 @@ export async function initAcademicCalendar() {
if (gridButton) {
activateCalendarTab('grid');
editorCalendarSelect.value = gridButton.dataset.id;
setCalendarSelection(editorCalendarSelect, gridButton.dataset.id);
refreshSelectVisuals(editorCalendarSelect);
await loadCalendarGrid();
calendarGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -663,7 +724,7 @@ export async function initAcademicCalendar() {
if (subjectsButton) {
activateCalendarTab('subjects');
subjectCalendarSelect.value = subjectsButton.dataset.id;
setCalendarSelection(subjectCalendarSelect, subjectsButton.dataset.id);
refreshSelectVisuals(subjectCalendarSelect);
await loadCalendarSubjects();
subjectSummary.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -679,7 +740,22 @@ export async function initAcademicCalendar() {
});
if (!confirmed) return;
try {
await api.delete('/api/admin/academic-calendars/' + deleteButton.dataset.id);
const deletedId = deleteButton.dataset.id;
await api.delete('/api/admin/academic-calendars/' + deletedId);
calendarCache.delete(deletedId);
calendarComboboxes.forEach((combobox, select) => {
if (select.value === deletedId) {
setCalendarSelection(select, '');
if (select === editorCalendarSelect) {
loadedGridCalendarId = '';
calendarGrid.replaceChildren();
calendarTotals.replaceChildren();
clearGridDirty();
} else {
loadCalendarSubjects();
}
}
});
showAlert('academic-calendar-alert', 'Календарный график удалён', 'success');
await loadCalendars();
} catch (error) {
@@ -713,15 +789,18 @@ export async function initAcademicCalendar() {
}
async function loadCalendarSubjects() {
const request = ++subjectLoadRequest;
hideAlert('calendar-subject-alert');
const calendar = selectedSubjectCalendar();
if (subjectDirtyScope.isDirty() && !await confirmAbandonDirtyChanges()) {
subjectCalendarSelect.value = loadedSubjectCalendarId;
setCalendarSelection(subjectCalendarSelect, loadedSubjectCalendarId);
refreshSelectVisuals(subjectCalendarSelect);
return;
}
if (request !== subjectLoadRequest) return;
populateSubjectSemesterSelect();
calendarSubjectRows = [];
loadedSubjectCalendarId = '';
if (!calendar) {
loadedSubjectCalendarId = '';
clearSubjectDirty();
@@ -731,11 +810,13 @@ export async function initAcademicCalendar() {
subjectSummary.innerHTML = '<div class="loading-row">Загрузка...</div>';
try {
const rows = await api.get(`/api/admin/academic-calendars/${calendar.id}/subjects`);
if (request !== subjectLoadRequest || String(calendar.id) !== subjectCalendarSelect.value) return;
calendarSubjectRows = sortCalendarSubjectRows(rows || []);
loadedSubjectCalendarId = String(calendar.id);
clearSubjectDirty();
renderCalendarSubjects();
} catch (error) {
if (request !== subjectLoadRequest) return;
subjectSummary.innerHTML = `<div class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</div>`;
showAlert('calendar-subject-alert', error.message || 'Ошибка загрузки дисциплин графика', 'error');
}
@@ -744,6 +825,10 @@ export async function initAcademicCalendar() {
function addCalendarSubject() {
hideAlert('calendar-subject-alert');
const calendar = selectedSubjectCalendar();
if (calendar && String(calendar.id) !== loadedSubjectCalendarId) {
showAlert('calendar-subject-alert', 'Дождитесь загрузки дисциплин выбранного графика', 'error');
return;
}
const semesterNumber = Number(subjectSemesterSelect.value);
const subjectId = Number(subjectSelect.value);
if (!calendar || !semesterNumber || !subjectId) {
@@ -781,6 +866,10 @@ export async function initAcademicCalendar() {
async function saveCalendarSubjects() {
hideAlert('calendar-subject-alert');
const calendar = selectedSubjectCalendar();
if (calendar && String(calendar.id) !== loadedSubjectCalendarId) {
showAlert('calendar-subject-alert', 'Дождитесь загрузки дисциплин выбранного графика', 'error');
return;
}
if (!calendar) {
showAlert('calendar-subject-alert', 'Выберите календарный график', 'error');
return;
@@ -855,10 +944,11 @@ export async function initAcademicCalendar() {
}
async function loadCalendarGrid() {
const request = ++gridLoadRequest;
hideAlert('calendar-editor-alert');
const calendar = selectedEditorCalendar();
if (gridDirtyScope.isDirty() && !await confirmAbandonDirtyChanges()) {
editorCalendarSelect.value = loadedGridCalendarId;
setCalendarSelection(editorCalendarSelect, loadedGridCalendarId);
refreshSelectVisuals(editorCalendarSelect);
return;
}
@@ -869,6 +959,7 @@ export async function initAcademicCalendar() {
try {
await loadTimeSlotDateAssignments();
const rows = await api.get(`/api/admin/academic-calendars/${calendar.id}/grid`);
if (request !== gridLoadRequest || String(calendar.id) !== editorCalendarSelect.value) return;
const gridRows = rows.length ? rows : buildDefaultGrid(calendar);
renderCalendarGrid(gridRows);
loadedGridCalendarId = String(calendar.id);
@@ -876,6 +967,7 @@ export async function initAcademicCalendar() {
trackUxEvent('calendar_grid_loaded', { calendarId: calendar.id, hasSavedRows: rows.length > 0 });
showAlert('calendar-editor-alert', rows.length ? 'Сетка загружена' : 'Подготовлена новая сетка по датам учебного года', 'success');
} catch (error) {
if (request !== gridLoadRequest) return;
showAlert('calendar-editor-alert', error.message || 'Ошибка загрузки сетки', 'error');
}
}
@@ -887,6 +979,10 @@ export async function initAcademicCalendar() {
showAlert('calendar-editor-alert', 'Выберите календарный график', 'error');
return;
}
if (String(calendar.id) !== loadedGridCalendarId) {
showAlert('calendar-editor-alert', 'Сначала загрузите сетку выбранного графика', 'error');
return;
}
const rows = Array.from(calendarGrid.querySelectorAll('.calendar-day')).map(day => ({
calendarId: calendar.id,
courseNumber: Number(day.dataset.course),
@@ -1535,31 +1631,12 @@ export async function initAcademicCalendar() {
syncSelects(fillActivitySelect);
}
function populateEditorCalendarSelect() {
const current = editorCalendarSelect.value;
editorCalendarSelect.innerHTML = '<option value="">Выберите график</option>' +
calendars.map(calendar =>
`<option value="${calendar.id}">${escapeHtml(buildAcademicCalendarTitle(calendar) || '-')}</option>`
).join('');
if (current && calendars.some(calendar => String(calendar.id) === String(current))) {
editorCalendarSelect.value = current;
}
syncSelects(editorCalendarSelect);
}
function populateSubjectCalendarSelect() {
if (!subjectCalendarSelect) return;
const current = subjectCalendarSelect.value;
subjectCalendarSelect.innerHTML = calendars.length
? '<option value="">Выберите график</option>' + calendars.map(calendar =>
`<option value="${calendar.id}">${escapeHtml(buildAcademicCalendarTitle(calendar) || '-')}</option>`
).join('')
: '<option value="">Нет графиков</option>';
if (current && calendars.some(calendar => String(calendar.id) === String(current))) {
subjectCalendarSelect.value = current;
}
populateSubjectSemesterSelect();
syncSelects(subjectCalendarSelect);
function setCalendarSelection(select, id) {
const calendar = calendarCache.get(String(id));
select.innerHTML = calendar
? `<option value="${calendar.id}" selected>${escapeHtml(buildAcademicCalendarTitle(calendar) || calendar.title)}</option>`
: '<option value="">Выберите график</option>';
calendarComboboxes.get(select)?.syncFromSelect();
}
function populateSubjectSemesterSelect() {
@@ -1603,11 +1680,11 @@ export async function initAcademicCalendar() {
}
function selectedEditorCalendar() {
return calendars.find(calendar => String(calendar.id) === String(editorCalendarSelect.value));
return calendarCache.get(String(editorCalendarSelect.value));
}
function selectedSubjectCalendar() {
return calendars.find(calendar => String(calendar.id) === String(subjectCalendarSelect?.value));
return calendarCache.get(String(subjectCalendarSelect?.value));
}
function calendarSemesterCount(calendar) {
@@ -1795,6 +1872,7 @@ export async function initAcademicCalendar() {
function refreshSelectVisuals(...selects) {
selects.filter(Boolean).forEach(select => {
calendarComboboxes.get(select)?.syncFromSelect();
if (select.customSelectInstance) {
select.customSelectInstance.rebuildMenu();
}