много всего и тестовые данные
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user