исправил качество правил и изменение расписания (не черновика)
This commit is contained in:
@@ -34,6 +34,17 @@ export function scheduleSlotActionLabel(slotCount) {
|
||||
return slotCount === 1 ? 'Очистить' : 'Удалить';
|
||||
}
|
||||
|
||||
export function chooseScheduleVersion(versions, semesterId = '', requestedVersionId = '') {
|
||||
const available = (versions || []).filter(version => ['PUBLISHED', 'DRAFT'].includes(version.status));
|
||||
return available.find(version => String(version.id) === String(requestedVersionId))
|
||||
|| available.find(version => version.status === 'PUBLISHED'
|
||||
&& String(version.semesterId) === String(semesterId))
|
||||
|| available.find(version => version.status === 'PUBLISHED')
|
||||
|| available.find(version => String(version.semesterId) === String(semesterId))
|
||||
|| available[0]
|
||||
|| null;
|
||||
}
|
||||
|
||||
const LESSON_TYPE_LIMITS = [
|
||||
{
|
||||
key: 'lecture',
|
||||
@@ -68,8 +79,20 @@ export async function initSchedule() {
|
||||
const ruleSubjectSelect = document.getElementById('schedule-rule-subject');
|
||||
const ruleSemesterSelect = document.getElementById('schedule-rule-semester');
|
||||
const scheduleVersionSelect = document.getElementById('schedule-version-select');
|
||||
const scheduleVersionContext = document.getElementById('schedule-version-context');
|
||||
const scheduleVersionEyebrow = document.getElementById('schedule-version-eyebrow');
|
||||
const scheduleVersionTitle = document.getElementById('schedule-version-title');
|
||||
const scheduleVersionNote = document.getElementById('schedule-version-note');
|
||||
const scheduleVersionCreateDraft = document.getElementById('schedule-version-create-draft');
|
||||
const scheduleVersionManage = document.getElementById('schedule-version-manage');
|
||||
const draftModal = document.getElementById('schedule-draft-modal');
|
||||
const draftForm = document.getElementById('schedule-draft-form');
|
||||
const draftNameInput = document.getElementById('schedule-draft-name');
|
||||
const draftCopy = document.getElementById('schedule-draft-copy');
|
||||
const draftSummary = document.getElementById('schedule-draft-summary');
|
||||
const draftCloseButton = document.getElementById('schedule-draft-close');
|
||||
const draftCancelButton = document.getElementById('schedule-draft-cancel');
|
||||
const draftSubmitButton = document.getElementById('schedule-draft-submit');
|
||||
const ruleTypeInputs = Object.fromEntries(LESSON_TYPE_LIMITS.map(config => [config.key, {
|
||||
hours: document.getElementById(config.hoursInputId),
|
||||
startWeek: document.getElementById(config.startWeekInputId)
|
||||
@@ -138,6 +161,7 @@ export async function initSchedule() {
|
||||
let pendingConflictResolution = null;
|
||||
let visualContextTarget = null;
|
||||
let pendingSlotMove = null;
|
||||
let creatingDraft = false;
|
||||
|
||||
bindEvents();
|
||||
|
||||
@@ -159,13 +183,21 @@ export async function initSchedule() {
|
||||
if (scheduleVersionSelect.value) {
|
||||
localStorage.setItem('magistr.schedule.versionId', scheduleVersionSelect.value);
|
||||
}
|
||||
updateVersionContext();
|
||||
resetRuleForm();
|
||||
syncRuleSemesterWithVersion();
|
||||
await loadRules();
|
||||
});
|
||||
scheduleVersionCreateDraft?.addEventListener('click', openDraftModal);
|
||||
scheduleVersionManage?.addEventListener('click', () => {
|
||||
document.querySelector('.nav-item[data-tab="schedule-versions"]')?.click();
|
||||
});
|
||||
draftCloseButton?.addEventListener('click', closeDraftModal);
|
||||
draftCancelButton?.addEventListener('click', closeDraftModal);
|
||||
draftModal?.addEventListener('click', event => {
|
||||
if (event.target === draftModal) closeDraftModal();
|
||||
});
|
||||
draftForm?.addEventListener('submit', createDraftFromSelectedVersion);
|
||||
ruleResetButton?.addEventListener('click', resetRuleForm);
|
||||
slotAddButton?.addEventListener('click', () => renderRuleSlots([...readRuleSlots(false), {}]));
|
||||
visualToggleButton?.addEventListener('click', toggleVisualDrawer);
|
||||
@@ -211,6 +243,10 @@ export async function initSchedule() {
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key !== 'Escape') return;
|
||||
if (draftModal && !draftModal.hidden) {
|
||||
closeDraftModal();
|
||||
return;
|
||||
}
|
||||
if (moveSlotModal?.classList.contains('open')) {
|
||||
closeMoveSlotModal();
|
||||
return;
|
||||
@@ -302,31 +338,123 @@ export async function initSchedule() {
|
||||
updatePreferenceHints();
|
||||
}
|
||||
|
||||
async function loadScheduleVersions() {
|
||||
async function loadScheduleVersions(preferredVersionId = null) {
|
||||
const responses = await Promise.all(semesters.map(semester =>
|
||||
api.get(`/api/edu-office/schedule/versions?semesterId=${encodeURIComponent(semester.id)}`)
|
||||
));
|
||||
scheduleVersions = responses.flat().filter(version => version.status === 'DRAFT');
|
||||
const storedId = localStorage.getItem('magistr.schedule.versionId');
|
||||
const stored = scheduleVersions.find(version => String(version.id) === storedId);
|
||||
const selected = stored || scheduleVersions[0] || null;
|
||||
scheduleVersions = responses.flat().filter(version => ['PUBLISHED', 'DRAFT'].includes(version.status));
|
||||
const requestedVersionId = preferredVersionId || localStorage.getItem('magistr.schedule.openVersionId') || '';
|
||||
localStorage.removeItem('magistr.schedule.openVersionId');
|
||||
const selected = chooseScheduleVersion(
|
||||
scheduleVersions,
|
||||
ruleSemesterSelect.value || defaultScheduleSemesterId(),
|
||||
requestedVersionId
|
||||
);
|
||||
const published = scheduleVersions.filter(version => version.status === 'PUBLISHED');
|
||||
const drafts = scheduleVersions.filter(version => version.status === 'DRAFT');
|
||||
scheduleVersionSelect.innerHTML = scheduleVersions.length
|
||||
? scheduleVersions.map(version => `
|
||||
<option value="${version.id}">
|
||||
${escapeHtml(version.semesterLabel)} · v${escapeHtml(String(version.versionNumber))} · ${escapeHtml(version.name)}
|
||||
</option>
|
||||
`).join('')
|
||||
: '<option value="">Черновики не созданы</option>';
|
||||
? [
|
||||
versionOptionGroup('Опубликованные расписания', published),
|
||||
versionOptionGroup('Черновики', drafts)
|
||||
].filter(Boolean).join('')
|
||||
: '<option value="">Расписания не созданы</option>';
|
||||
scheduleVersionSelect.value = selected ? String(selected.id) : '';
|
||||
if (selected) {
|
||||
localStorage.setItem('magistr.schedule.versionId', String(selected.id));
|
||||
scheduleVersionNote.textContent = `Версия ${selected.versionNumber} · ${selected.name}. Изменения не видны пользователям до публикации.`;
|
||||
} else {
|
||||
scheduleVersionNote.textContent = 'Черновика нет. Создайте его в разделе «Версии расписания».';
|
||||
}
|
||||
updateVersionContext();
|
||||
setRuleEditorEnabled(Boolean(selected));
|
||||
}
|
||||
|
||||
function versionOptionGroup(label, versions) {
|
||||
if (!versions.length) return '';
|
||||
return `<optgroup label="${escapeHtml(label)}">${versions.map(version => `
|
||||
<option value="${version.id}">
|
||||
${escapeHtml(version.semesterLabel)} · v${escapeHtml(String(version.versionNumber))} · ${escapeHtml(version.name)}
|
||||
</option>
|
||||
`).join('')}</optgroup>`;
|
||||
}
|
||||
|
||||
function defaultScheduleSemesterId() {
|
||||
const savedSemesterId = localStorage.getItem('magistr.schedule.semesterId');
|
||||
const saved = semesters.find(semester => String(semester.id) === String(savedSemesterId));
|
||||
if (saved) return saved.id;
|
||||
const today = localIsoDate(new Date());
|
||||
const current = semesters.find(semester => semester.startDate <= today && semester.endDate >= today);
|
||||
return current?.id || semesters[0]?.id || '';
|
||||
}
|
||||
|
||||
function updateVersionContext() {
|
||||
const version = selectedScheduleVersion();
|
||||
const isDraft = version?.status === 'DRAFT';
|
||||
if (scheduleVersionContext) scheduleVersionContext.dataset.status = isDraft ? 'DRAFT' : 'PUBLISHED';
|
||||
if (scheduleVersionEyebrow) scheduleVersionEyebrow.textContent = isDraft ? 'Безопасная рабочая копия' : 'Основной режим';
|
||||
if (scheduleVersionTitle) scheduleVersionTitle.textContent = isDraft ? 'Редактирование черновика' : 'Опубликованное расписание';
|
||||
if (!version) {
|
||||
scheduleVersionNote.textContent = 'Расписание для редактирования не найдено.';
|
||||
return;
|
||||
}
|
||||
scheduleVersionNote.textContent = isDraft
|
||||
? `Версия ${version.versionNumber} · ${version.name}. Изменения не видны пользователям до публикации.`
|
||||
: `Версия ${version.versionNumber} · ${version.name}. Сохранённые изменения сразу видны пользователям.`;
|
||||
}
|
||||
|
||||
function openDraftModal() {
|
||||
const base = selectedScheduleVersion();
|
||||
if (!base || creatingDraft) return;
|
||||
draftForm?.reset();
|
||||
if (draftCopy) {
|
||||
draftCopy.textContent = `За основу будет взято «${base.name}» — все его правила и слоты будут скопированы.`;
|
||||
}
|
||||
if (draftSummary) {
|
||||
draftSummary.textContent = `${base.semesterLabel} · версия ${base.versionNumber}`;
|
||||
draftSummary.dataset.tone = '';
|
||||
}
|
||||
draftModal.hidden = false;
|
||||
draftNameInput?.focus();
|
||||
}
|
||||
|
||||
function closeDraftModal() {
|
||||
if (!draftModal || creatingDraft) return;
|
||||
draftModal.hidden = true;
|
||||
}
|
||||
|
||||
async function createDraftFromSelectedVersion(event) {
|
||||
event.preventDefault();
|
||||
const base = selectedScheduleVersion();
|
||||
const name = draftNameInput?.value.trim();
|
||||
if (!base || !name || creatingDraft) return;
|
||||
creatingDraft = true;
|
||||
if (draftSubmitButton) {
|
||||
draftSubmitButton.disabled = true;
|
||||
draftSubmitButton.textContent = 'Создаём…';
|
||||
}
|
||||
try {
|
||||
const created = await api.post('/api/edu-office/schedule/versions', {
|
||||
semesterId: Number(base.semesterId),
|
||||
name,
|
||||
basedOnVersionId: Number(base.id)
|
||||
});
|
||||
draftModal.hidden = true;
|
||||
await loadScheduleVersions(created.id);
|
||||
resetRuleForm();
|
||||
syncRuleSemesterWithVersion();
|
||||
await loadRules();
|
||||
showAlert('schedule-rule-alert', `Черновик «${created.name}» создан и открыт.`, 'success');
|
||||
} catch (error) {
|
||||
if (draftSummary) {
|
||||
draftSummary.textContent = error.message || 'Не удалось создать черновик.';
|
||||
draftSummary.dataset.tone = 'error';
|
||||
}
|
||||
} finally {
|
||||
creatingDraft = false;
|
||||
if (draftSubmitButton) {
|
||||
draftSubmitButton.disabled = false;
|
||||
draftSubmitButton.textContent = 'Создать и открыть';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectedScheduleVersion() {
|
||||
return scheduleVersions.find(version => String(version.id) === String(scheduleVersionSelect?.value)) || null;
|
||||
}
|
||||
@@ -352,8 +480,8 @@ export async function initSchedule() {
|
||||
const version = selectedScheduleVersion();
|
||||
if (!version) {
|
||||
rules = [];
|
||||
rulesTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Создайте черновик в разделе «Версии расписания»</td></tr>';
|
||||
setVisualState('Нет выбранного черновика');
|
||||
rulesTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Нет расписания для редактирования</td></tr>';
|
||||
setVisualState('Нет выбранного расписания');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -1390,7 +1518,7 @@ export async function initSchedule() {
|
||||
const groupIds = selectedRuleGroupIds();
|
||||
const slots = readRuleSlots(true);
|
||||
if (!ruleSubjectSelect.value) throw new Error('Выберите дисциплину');
|
||||
if (!scheduleVersionSelect.value) throw new Error('Выберите черновик расписания');
|
||||
if (!scheduleVersionSelect.value) throw new Error('Выберите версию расписания');
|
||||
if (!ruleSemesterSelect.value) throw new Error('Выберите семестр');
|
||||
const typeLimits = readTypeLimits();
|
||||
validateTypeLimits(typeLimits, slots);
|
||||
@@ -1456,7 +1584,7 @@ export async function initSchedule() {
|
||||
const actionLabel = scheduleSlotActionLabel(normalizedSlots.length);
|
||||
const actionClass = isSingleSlot ? 'btn-ghost' : 'btn-danger';
|
||||
ruleSlotsContainer.innerHTML = normalizedSlots.map((slot, index) => `
|
||||
<div class="schedule-slot-row ${isLaboratorySlot(slot) ? 'has-subgroup' : ''}" data-index="${index}">
|
||||
<div class="schedule-slot-row ${isLaboratorySlot(slot) ? 'has-subgroup' : ''}" data-index="${index}" data-slot-id="${escapeHtml(String(slot.id || ''))}">
|
||||
<div class="form-group"><label>День</label><select class="slot-day" required>${options(DAY_OPTIONS, slot.dayOfWeek)}</select></div>
|
||||
<div class="form-group"><label>Чётность</label><select class="slot-parity" required>${options(Object.entries(PARITY_LABELS).map(([value, label]) => ({ value, label })), slot.parity || 'BOTH')}</select></div>
|
||||
<div class="form-group"><label>Пара</label><select class="slot-time" required>${options(timeSlots.map(toTimeSlotOption), slot.timeSlotId)}</select></div>
|
||||
@@ -1521,8 +1649,9 @@ export async function initSchedule() {
|
||||
const rows = Array.from(ruleSlotsContainer.querySelectorAll('.schedule-slot-row'));
|
||||
return rows.map((row, index) => {
|
||||
const subgroupIds = readSlotSubgroupIds(row);
|
||||
const slotId = Number(row.dataset.slotId);
|
||||
const slot = {
|
||||
id: null,
|
||||
id: slotId || null,
|
||||
dayOfWeek: Number(row.querySelector('.slot-day').value),
|
||||
parity: row.querySelector('.slot-parity').value,
|
||||
timeSlotId: Number(row.querySelector('.slot-time').value),
|
||||
|
||||
Reference in New Issue
Block a user