много всего и тестовые данные
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -339,7 +339,7 @@ export function filterQualityProblems(problems, severity = '', category = '') {
|
||||
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 = '') {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { api } from '../api.js';
|
||||
import { renderPagination } from '../pagination.js';
|
||||
import { refreshCustomDropdowns } from '../dropdown.js';
|
||||
import {
|
||||
academicYearsFromSemesters,
|
||||
resolveAcademicPeriod,
|
||||
@@ -188,6 +190,14 @@ export async function initSchedule() {
|
||||
const moveSlotTimeSelect = document.getElementById('schedule-move-slot-time');
|
||||
|
||||
let rules = [];
|
||||
let visualRules = [];
|
||||
let rulesPage = 0;
|
||||
let rulesSize = 25;
|
||||
let rulesRequest = 0;
|
||||
let visualRequest = 0;
|
||||
let rulesVersionId = '';
|
||||
const rulesSearch = document.getElementById('schedule-rules-search');
|
||||
const rulesPagination = document.getElementById('schedule-rules-pagination');
|
||||
let subjects = [];
|
||||
let groups = [];
|
||||
let lessonTypes = [];
|
||||
@@ -237,6 +247,10 @@ export async function initSchedule() {
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
rulesSearch?.addEventListener('input', debounce(() => {
|
||||
rulesPage = 0;
|
||||
loadRules();
|
||||
}, 300));
|
||||
refreshRulesButton?.addEventListener('click', loadRules);
|
||||
scheduleVersionYearSelect?.addEventListener('change', handleVersionYearChange);
|
||||
scheduleVersionSemesterSelect?.addEventListener('change', handleVersionSemesterChange);
|
||||
@@ -522,7 +536,7 @@ export async function initSchedule() {
|
||||
scheduleVersionSemesterSelect.value = String(selectedSemester?.id || '');
|
||||
lastVersionYearValue = scheduleVersionYearSelect.value;
|
||||
lastVersionSemesterValue = scheduleVersionSemesterSelect.value;
|
||||
syncSelects(scheduleVersionYearSelect, scheduleVersionSemesterSelect);
|
||||
refreshCustomDropdowns(scheduleVersionYearSelect, scheduleVersionSemesterSelect);
|
||||
}
|
||||
|
||||
function versionOptionGroup(label, versions) {
|
||||
@@ -634,6 +648,11 @@ export async function initSchedule() {
|
||||
}
|
||||
|
||||
async function loadRules() {
|
||||
const request = ++rulesRequest;
|
||||
++visualRequest;
|
||||
visualRules = [];
|
||||
rules = [];
|
||||
rulesPagination?.replaceChildren();
|
||||
rulesTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Загрузка...</td></tr>';
|
||||
const version = selectedScheduleVersion();
|
||||
if (!version) {
|
||||
@@ -642,26 +661,43 @@ export async function initSchedule() {
|
||||
setVisualState('Нет выбранного расписания');
|
||||
return;
|
||||
}
|
||||
if (rulesVersionId !== String(version.id)) {
|
||||
rulesPage = 0;
|
||||
rulesVersionId = String(version.id);
|
||||
}
|
||||
try {
|
||||
rules = await api.get(`/api/admin/schedule-rules?versionId=${encodeURIComponent(version.id)}`);
|
||||
const params = new URLSearchParams({ versionId: version.id, page: rulesPage, size: rulesSize, query: rulesSearch?.value.trim() || '' });
|
||||
const page = await api.get(`/api/admin/schedule-rules/page?${params}`);
|
||||
if (request !== rulesRequest) return;
|
||||
if (rulesPage > 0 && rulesPage >= page.totalPages) {
|
||||
rulesPage = Math.max(0, page.totalPages - 1);
|
||||
return loadRules();
|
||||
}
|
||||
rules = page.items;
|
||||
renderPagination(rulesPagination, page, {
|
||||
label: 'правил',
|
||||
onPage: value => { rulesPage = value; loadRules(); },
|
||||
onSize: value => { rulesSize = value; rulesPage = 0; loadRules(); }
|
||||
});
|
||||
if (!visualPeriodTouched) {
|
||||
selectDefaultVisualPeriod();
|
||||
}
|
||||
renderRules();
|
||||
renderVisualSchedule();
|
||||
openRequestedRule();
|
||||
if (visualDrawer?.classList.contains('open')) await loadVisualRules();
|
||||
await openRequestedRule();
|
||||
} catch (error) {
|
||||
if (request !== rulesRequest) return;
|
||||
rulesTbody.innerHTML = `<tr><td colspan="7" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
setVisualState('Ошибка загрузки правил расписания');
|
||||
}
|
||||
}
|
||||
|
||||
function openRequestedRule() {
|
||||
async function openRequestedRule() {
|
||||
const requestedRuleId = localStorage.getItem('magistr.schedule.openRuleId');
|
||||
if (!requestedRuleId) return;
|
||||
localStorage.removeItem('magistr.schedule.openRuleId');
|
||||
const rule = rules.find(item => String(item.id) === requestedRuleId);
|
||||
if (!rule) {
|
||||
const rule = await api.get(`/api/admin/schedule-rules/${encodeURIComponent(requestedRuleId)}`);
|
||||
if (!rule || String(rule.scheduleVersionId) !== String(selectedScheduleVersion()?.id)) {
|
||||
showAlert('schedule-rule-alert', 'Не удалось найти выбранное правило в этом расписании.', 'error');
|
||||
return;
|
||||
}
|
||||
@@ -670,7 +706,7 @@ export async function initSchedule() {
|
||||
|
||||
function renderRules() {
|
||||
if (!rules.length) {
|
||||
rulesTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Правила расписания не созданы</td></tr>';
|
||||
rulesTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Правила не найдены</td></tr>';
|
||||
renderVisualSchedule();
|
||||
return;
|
||||
}
|
||||
@@ -709,6 +745,7 @@ export async function initSchedule() {
|
||||
visualToggleButton?.setAttribute('aria-expanded', 'true');
|
||||
visualToggleButton?.setAttribute('aria-label', 'Скрыть визуальное расписание');
|
||||
if (visualBackdrop) visualBackdrop.hidden = false;
|
||||
loadVisualRules();
|
||||
}
|
||||
|
||||
function closeVisualDrawer() {
|
||||
@@ -722,34 +759,49 @@ export async function initSchedule() {
|
||||
if (visualBackdrop) visualBackdrop.hidden = true;
|
||||
}
|
||||
|
||||
async function loadVisualRules() {
|
||||
const version = selectedScheduleVersion();
|
||||
if (!version) return;
|
||||
const request = ++visualRequest;
|
||||
setVisualState('Загрузка визуального расписания...');
|
||||
try {
|
||||
const loaded = await api.get(`/api/admin/schedule-rules?versionId=${encodeURIComponent(version.id)}`);
|
||||
if (request !== visualRequest) return;
|
||||
visualRules = loaded;
|
||||
renderVisualSchedule();
|
||||
} catch (error) {
|
||||
if (request === visualRequest) setVisualState(error.message || 'Ошибка загрузки визуального расписания');
|
||||
}
|
||||
}
|
||||
|
||||
function renderVisualSchedule() {
|
||||
if (!visualStage || !visualSummary) return;
|
||||
if (!visualStage || !visualSummary || !visualDrawer?.classList.contains('open')) return;
|
||||
if (!selectedVisualSemesterId) {
|
||||
syncVisualGroupFilter([]);
|
||||
visualSummary.textContent = 'Выберите учебный год и семестр';
|
||||
setVisualState('Выберите период, чтобы увидеть правила конкретного семестра');
|
||||
return;
|
||||
}
|
||||
if (!rules.length) {
|
||||
if (!visualRules.length) {
|
||||
syncVisualGroupFilter([]);
|
||||
visualSummary.textContent = 'Нет сохранённых правил';
|
||||
setVisualState('Сохраните правила расписания, чтобы увидеть матрицу по группам');
|
||||
return;
|
||||
}
|
||||
|
||||
const visualRules = visualRulesForSelectedPeriod();
|
||||
const periodRules = visualRulesForSelectedPeriod();
|
||||
const periodLabel = visualPeriodLabel();
|
||||
if (!visualRules.length) {
|
||||
if (!periodRules.length) {
|
||||
syncVisualGroupFilter([]);
|
||||
visualSummary.textContent = `${periodLabel} · правил нет`;
|
||||
setVisualState('В выбранном семестре нет сохранённых правил расписания');
|
||||
return;
|
||||
}
|
||||
|
||||
const allGroups = collectVisualGroups(visualRules);
|
||||
const allGroups = collectVisualGroups(periodRules);
|
||||
syncVisualGroupFilter(allGroups);
|
||||
const columns = allGroups.filter(group => selectedVisualGroupIds.has(String(group.id)));
|
||||
const slots = timeSlots.length ? timeSlots.map(visualSlotFromTimeSlot) : collectVisualSlotsFromRules(visualRules);
|
||||
const slots = timeSlots.length ? timeSlots.map(visualSlotFromTimeSlot) : collectVisualSlotsFromRules(periodRules);
|
||||
if (!allGroups.length || !slots.length) {
|
||||
visualSummary.textContent = 'Недостаточно данных для построения';
|
||||
setVisualState('У правил должны быть группы и слоты занятий');
|
||||
@@ -761,7 +813,7 @@ export async function initSchedule() {
|
||||
return;
|
||||
}
|
||||
|
||||
const lessonsByCell = buildVisualLessonsByCell(selectedVisualGroupIds, visualRules);
|
||||
const lessonsByCell = buildVisualLessonsByCell(selectedVisualGroupIds, periodRules);
|
||||
const rows = buildVisualRows(columns, slots, lessonsByCell);
|
||||
if (!rows.length) {
|
||||
visualSummary.textContent = `${periodLabel} · ${columns.length} ${groupCountLabel(columns.length)} · пар не найдено`;
|
||||
@@ -769,7 +821,7 @@ export async function initSchedule() {
|
||||
return;
|
||||
}
|
||||
|
||||
visualSummary.textContent = `${periodLabel} · ${visualRules.length} ${ruleCountLabel(visualRules.length)} · ${columns.length} ${groupCountLabel(columns.length)} · ${rows.length} ${rowCountLabel(rows.length)}`;
|
||||
visualSummary.textContent = `${periodLabel} · ${periodRules.length} ${ruleCountLabel(periodRules.length)} · ${columns.length} ${groupCountLabel(columns.length)} · ${rows.length} ${rowCountLabel(rows.length)}`;
|
||||
visualStage.innerHTML = `
|
||||
<div class="schedule-visual-table-wrap">
|
||||
<table class="schedule-visual-table">
|
||||
@@ -1029,7 +1081,7 @@ export async function initSchedule() {
|
||||
}
|
||||
|
||||
function openVisualContextMenu(button) {
|
||||
const rule = rules.find(item => String(item.id) === String(button.dataset.ruleId));
|
||||
const rule = [...rules, ...visualRules].find(item => String(item.id) === String(button.dataset.ruleId));
|
||||
if (!rule) {
|
||||
showAlert('schedule-rule-alert', 'Правило не найдено. Обновите список правил.', 'error');
|
||||
return;
|
||||
@@ -1074,7 +1126,7 @@ export async function initSchedule() {
|
||||
if (!actionButton || !visualContextTarget) return;
|
||||
const action = actionButton.dataset.action;
|
||||
const target = { ...visualContextTarget };
|
||||
const rule = rules.find(item => String(item.id) === String(target.ruleId));
|
||||
const rule = [...rules, ...visualRules].find(item => String(item.id) === String(target.ruleId));
|
||||
if (!rule) {
|
||||
closeVisualContextMenu();
|
||||
showAlert('schedule-rule-alert', 'Правило не найдено. Обновите список правил.', 'error');
|
||||
@@ -1117,7 +1169,7 @@ export async function initSchedule() {
|
||||
}
|
||||
|
||||
function openMoveSlotModal(target) {
|
||||
const rule = rules.find(item => String(item.id) === String(target.ruleId));
|
||||
const rule = [...rules, ...visualRules].find(item => String(item.id) === String(target.ruleId));
|
||||
const slotIndex = findRuleSlotIndex(rule, target);
|
||||
const slot = slotIndex >= 0 ? rule.slots[slotIndex] : null;
|
||||
if (!rule || !slot) {
|
||||
@@ -1160,7 +1212,7 @@ export async function initSchedule() {
|
||||
event.preventDefault();
|
||||
if (!pendingSlotMove) return;
|
||||
hideAlert('schedule-move-slot-alert');
|
||||
const rule = rules.find(item => String(item.id) === pendingSlotMove.ruleId);
|
||||
const rule = [...rules, ...visualRules].find(item => String(item.id) === pendingSlotMove.ruleId);
|
||||
const slot = rule?.slots?.[pendingSlotMove.slotIndex];
|
||||
if (!rule || !slot) {
|
||||
showAlert('schedule-move-slot-alert', 'Слот правила не найден. Обновите список правил.', 'error');
|
||||
@@ -1330,7 +1382,7 @@ export async function initSchedule() {
|
||||
|
||||
function visualRulesForSelectedPeriod() {
|
||||
if (!selectedVisualSemesterId) return [];
|
||||
return rules.filter(rule => String(rule.semesterId) === selectedVisualSemesterId);
|
||||
return visualRules.filter(rule => String(rule.semesterId) === selectedVisualSemesterId);
|
||||
}
|
||||
|
||||
function firstSemesterWithRules() {
|
||||
@@ -1392,7 +1444,7 @@ export async function initSchedule() {
|
||||
const deleteButton = event.target.closest('.btn-delete-rule');
|
||||
|
||||
if (editButton) {
|
||||
const rule = rules.find(item => item.id == editButton.dataset.id)
|
||||
const rule = [...rules, ...visualRules].find(item => item.id == editButton.dataset.id)
|
||||
|| await api.get('/api/admin/schedule-rules/' + editButton.dataset.id);
|
||||
fillRuleForm(rule);
|
||||
return;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { renderPagination } from '../pagination.js';
|
||||
import { debounce } from '../url-state.js';
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
import { confirmAction } from '../dialog.js';
|
||||
@@ -52,6 +54,29 @@ export async function initUniversityStructure() {
|
||||
let specialties = [];
|
||||
let currentProfiles = [];
|
||||
let allProfiles = [];
|
||||
const listStates = Object.fromEntries(['departments', 'specialties', 'profiles'].map(key => [key, { page: 0, size: 25 }]));
|
||||
const listRenderers = { departments: renderDepartments, specialties: renderSpecialties, profiles: renderAllProfiles };
|
||||
Object.keys(listStates).forEach(key => {
|
||||
document.getElementById(`${key}-search`)?.addEventListener('input', debounce(() => {
|
||||
listStates[key].page = 0;
|
||||
listRenderers[key]();
|
||||
}, 250));
|
||||
});
|
||||
|
||||
function listPage(key, items, fields) {
|
||||
const query = document.getElementById(`${key}-search`).value.trim().toLocaleLowerCase('ru');
|
||||
const filtered = items.filter(item => fields(item).join(' ').toLocaleLowerCase('ru').includes(query));
|
||||
const state = listStates[key];
|
||||
const totalPages = Math.ceil(filtered.length / state.size);
|
||||
state.page = Math.min(state.page, Math.max(0, totalPages - 1));
|
||||
renderPagination(document.getElementById(`${key}-pagination`), {
|
||||
...state, totalItems: filtered.length, totalPages
|
||||
}, {
|
||||
onPage: value => { state.page = value; listRenderers[key](); },
|
||||
onSize: value => { state.size = value; state.page = 0; listRenderers[key](); }
|
||||
});
|
||||
return filtered.slice(state.page * state.size, (state.page + 1) * state.size);
|
||||
}
|
||||
|
||||
// --- Логика переключения табов ---
|
||||
const structureTabs = [
|
||||
@@ -105,11 +130,12 @@ export async function initUniversityStructure() {
|
||||
}
|
||||
|
||||
function renderDepartments() {
|
||||
if (!departments || !departments.length) {
|
||||
deptTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Кафедры не созданы</td></tr>';
|
||||
const visible = listPage('departments', departments || [], d => [d.id, d.departmentName || d.name, d.departmentCode ?? d.code]);
|
||||
if (!visible.length) {
|
||||
deptTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Ничего не найдено</td></tr>';
|
||||
return;
|
||||
}
|
||||
deptTbody.innerHTML = departments.map(d => `
|
||||
deptTbody.innerHTML = visible.map(d => `
|
||||
<tr>
|
||||
<td>${d.id}</td>
|
||||
<td>${escapeHtml(d.departmentName || d.name)}</td>
|
||||
@@ -123,11 +149,12 @@ export async function initUniversityStructure() {
|
||||
}
|
||||
|
||||
function renderSpecialties() {
|
||||
if (!specialties || !specialties.length) {
|
||||
specTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Специальности не созданы</td></tr>';
|
||||
const visible = listPage('specialties', specialties || [], d => [d.id, d.specialityName || d.name, d.specialityCode || d.specialtyCode || d.specialty_code]);
|
||||
if (!visible.length) {
|
||||
specTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Ничего не найдено</td></tr>';
|
||||
return;
|
||||
}
|
||||
specTbody.innerHTML = specialties.map(s => `
|
||||
specTbody.innerHTML = visible.map(s => `
|
||||
<tr>
|
||||
<td>${s.id}</td>
|
||||
<td>${escapeHtml(s.specialityName || s.name)}</td>
|
||||
@@ -428,11 +455,12 @@ export async function initUniversityStructure() {
|
||||
}
|
||||
|
||||
function renderAllProfiles() {
|
||||
if (!allProfiles || !allProfiles.length) {
|
||||
tabProfileTbody.innerHTML = '<tr><td colspan="5" class="loading-row">Профили не созданы</td></tr>';
|
||||
const visible = listPage('profiles', allProfiles || [], d => [d.id, d.name, d.description, d.specialityCode, d.specialityName]);
|
||||
if (!visible.length) {
|
||||
tabProfileTbody.innerHTML = '<tr><td colspan="5" class="loading-row">Ничего не найдено</td></tr>';
|
||||
return;
|
||||
}
|
||||
tabProfileTbody.innerHTML = allProfiles.map(p => `
|
||||
tabProfileTbody.innerHTML = visible.map(p => `
|
||||
<tr>
|
||||
<td>${p.id}</td>
|
||||
<td>${escapeHtml(p.specialityCode)} · ${escapeHtml(p.specialityName)}</td>
|
||||
|
||||
Reference in New Issue
Block a user