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

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

@@ -3,6 +3,9 @@ import { bindDateInputMask, readDateInputIso, setDateInputIso } from '../date-in
import { escapeHtml, formatLocalDate, showAlert, hideAlert } from '../utils.js';
import { academicCalendarWeekNumber } from './academic-calendar-grid.js';
import { buildAcademicCalendarTitle } from './academic-calendar-title.js';
import { confirmAction } from '../dialog.js';
import { confirmAbandonDirtyChanges, createDirtyScope } from '../dirty-state.js';
import { trackUxEvent } from '../../../telemetry.js';
const DAY_OPTIONS = [
{ value: 1, label: 'Понедельник', shortLabel: 'Пн' },
@@ -219,6 +222,10 @@ export async function initAcademicCalendar() {
let isSyncingCalendarDayDialog = false;
let calendarDayTooltip = null;
let calendarDayTooltipPositionAnimation = null;
let loadedGridCalendarId = '';
let loadedSubjectCalendarId = '';
const gridDirtyScope = createDirtyScope('academic-calendar-grid', 'В календарной сетке есть несохранённые изменения. Загрузить другой график и потерять их?');
const subjectDirtyScope = createDirtyScope('academic-calendar-subjects', 'В привязках дисциплин есть несохранённые изменения. Продолжить без сохранения?');
bindEvents();
@@ -248,7 +255,10 @@ export async function initAcademicCalendar() {
].forEach(bindDateInputMask);
calendarTabButtons.forEach(button => {
button.addEventListener('click', () => activateCalendarTab(button.dataset.calendarTab));
button.addEventListener('click', async () => {
if (!await confirmAbandonDirtyChanges()) return;
activateCalendarTab(button.dataset.calendarTab);
});
});
academicYearsRefreshButton?.addEventListener('click', loadYears);
calendarsRefreshButton?.addEventListener('click', loadCalendars);
@@ -269,6 +279,7 @@ export async function initAcademicCalendar() {
if (event.target.matches('.calendar-day-activity')) {
updateCalendarDayVisual(event.target.closest('.calendar-day'));
renderCalendarTotals();
markGridDirty();
}
});
calendarGrid?.addEventListener('mousedown', (event) => {
@@ -320,6 +331,7 @@ export async function initAcademicCalendar() {
});
updateCalendarDayDialogSummaryForSelection(days);
renderCalendarTotals();
markGridDirty();
});
calendarDayDialogTimeSaveButton?.addEventListener('click', closeCalendarDayDialog);
calendarDayDialogTimeClearButton?.addEventListener('click', clearTimeSlotDateAssignment);
@@ -441,7 +453,13 @@ export async function initAcademicCalendar() {
}
if (deleteYearButton) {
if (!confirm('Удалить учебный год вместе с семестрами и календарными графиками?')) return;
const confirmed = await confirmAction({
title: 'Удаление учебного года',
message: 'Удалить учебный год вместе с семестрами и календарными графиками?',
confirmLabel: 'Удалить',
danger: true
});
if (!confirmed) return;
try {
await api.delete('/api/admin/calendar/years/' + deleteYearButton.dataset.id);
showAlert('academic-year-alert', 'Учебный год удалён', 'success');
@@ -653,7 +671,13 @@ export async function initAcademicCalendar() {
}
if (deleteButton) {
if (!confirm('Удалить календарный график?')) return;
const confirmed = await confirmAction({
title: 'Удаление календарного графика',
message: 'Удалить календарный график? Связанные настройки больше не будут доступны.',
confirmLabel: 'Удалить',
danger: true
});
if (!confirmed) return;
try {
await api.delete('/api/admin/academic-calendars/' + deleteButton.dataset.id);
showAlert('academic-calendar-alert', 'Календарный график удалён', 'success');
@@ -691,9 +715,16 @@ export async function initAcademicCalendar() {
async function loadCalendarSubjects() {
hideAlert('calendar-subject-alert');
const calendar = selectedSubjectCalendar();
if (subjectDirtyScope.isDirty() && !await confirmAbandonDirtyChanges()) {
subjectCalendarSelect.value = loadedSubjectCalendarId;
refreshSelectVisuals(subjectCalendarSelect);
return;
}
populateSubjectSemesterSelect();
calendarSubjectRows = [];
if (!calendar) {
loadedSubjectCalendarId = '';
clearSubjectDirty();
renderCalendarSubjects();
return;
}
@@ -701,6 +732,8 @@ export async function initAcademicCalendar() {
try {
const rows = await api.get(`/api/admin/academic-calendars/${calendar.id}/subjects`);
calendarSubjectRows = sortCalendarSubjectRows(rows || []);
loadedSubjectCalendarId = String(calendar.id);
clearSubjectDirty();
renderCalendarSubjects();
} catch (error) {
subjectSummary.innerHTML = `<div class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</div>`;
@@ -741,6 +774,7 @@ export async function initAcademicCalendar() {
}
]);
renderCalendarSubjects();
markSubjectDirty();
showAlert('calendar-subject-alert', 'Дисциплина добавлена. Сохраните привязки.', 'success');
}
@@ -759,6 +793,8 @@ export async function initAcademicCalendar() {
const saved = await api.put(`/api/admin/academic-calendars/${calendar.id}/subjects`, payload);
calendarSubjectRows = sortCalendarSubjectRows(saved || []);
renderCalendarSubjects();
clearSubjectDirty();
trackUxEvent('calendar_subjects_saved', { calendarId: calendar.id, count: calendarSubjectRows.length });
showAlert('calendar-subject-alert', 'Привязки дисциплин сохранены', 'success');
} catch (error) {
showAlert('calendar-subject-alert', error.message || 'Ошибка сохранения дисциплин графика', 'error');
@@ -774,6 +810,7 @@ export async function initAcademicCalendar() {
Number(row.semesterNumber) !== semesterNumber || Number(row.subjectId) !== subjectId
);
renderCalendarSubjects();
markSubjectDirty();
showAlert('calendar-subject-alert', 'Дисциплина убрана. Сохраните привязки.', 'success');
}
@@ -820,6 +857,11 @@ export async function initAcademicCalendar() {
async function loadCalendarGrid() {
hideAlert('calendar-editor-alert');
const calendar = selectedEditorCalendar();
if (gridDirtyScope.isDirty() && !await confirmAbandonDirtyChanges()) {
editorCalendarSelect.value = loadedGridCalendarId;
refreshSelectVisuals(editorCalendarSelect);
return;
}
if (!calendar) {
showAlert('calendar-editor-alert', 'Выберите календарный график', 'error');
return;
@@ -829,6 +871,9 @@ export async function initAcademicCalendar() {
const rows = await api.get(`/api/admin/academic-calendars/${calendar.id}/grid`);
const gridRows = rows.length ? rows : buildDefaultGrid(calendar);
renderCalendarGrid(gridRows);
loadedGridCalendarId = String(calendar.id);
clearGridDirty();
trackUxEvent('calendar_grid_loaded', { calendarId: calendar.id, hasSavedRows: rows.length > 0 });
showAlert('calendar-editor-alert', rows.length ? 'Сетка загружена' : 'Подготовлена новая сетка по датам учебного года', 'success');
} catch (error) {
showAlert('calendar-editor-alert', error.message || 'Ошибка загрузки сетки', 'error');
@@ -856,6 +901,8 @@ export async function initAcademicCalendar() {
}
try {
await api.put(`/api/admin/academic-calendars/${calendar.id}/grid`, rows);
clearGridDirty();
trackUxEvent('calendar_grid_saved', { calendarId: calendar.id, cells: rows.length });
showAlert('calendar-editor-alert', 'Календарный учебный график сохранён', 'success');
await loadCalendarGrid();
} catch (error) {
@@ -895,9 +942,30 @@ export async function initAcademicCalendar() {
}
});
renderCalendarTotals();
if (changed) markGridDirty();
showAlert('calendar-editor-alert', `Обновлено ячеек: ${changed}`, 'success');
}
function markGridDirty() {
gridDirtyScope.mark();
if (editorSaveButton) editorSaveButton.textContent = 'Сохранить сетку •';
}
function clearGridDirty() {
gridDirtyScope.clear();
if (editorSaveButton) editorSaveButton.textContent = 'Сохранить сетку';
}
function markSubjectDirty() {
subjectDirtyScope.mark();
if (subjectSaveButton) subjectSaveButton.textContent = 'Сохранить привязки •';
}
function clearSubjectDirty() {
subjectDirtyScope.clear();
if (subjectSaveButton) subjectSaveButton.textContent = 'Сохранить привязки';
}
function renderCalendarGrid(rows) {
activeCalendarDay = null;
selectedCalendarDayKey = null;