Исправил часть косяков в конструкторе правил
This commit is contained in:
@@ -35,6 +35,7 @@ const CELL_PARITY_LAYOUT = [
|
||||
{ parity: 'EVEN', label: 'Чётная' }
|
||||
];
|
||||
const MOBILE_QUERY = '(max-width: 760px)';
|
||||
const DEFAULT_SEMESTER_WEEKS = 22;
|
||||
|
||||
export async function initScheduleView() {
|
||||
const role = localStorage.getItem('role');
|
||||
@@ -621,11 +622,43 @@ export async function initScheduleView() {
|
||||
|
||||
if (startWeek <= 0) startWeek = 1;
|
||||
if (endWeek < startWeek) endWeek = startWeek;
|
||||
|
||||
if (startWeek === endWeek) {
|
||||
return `${startWeek} нед.`;
|
||||
}
|
||||
return `с ${startWeek} по ${endWeek} нед.`;
|
||||
|
||||
return formatLessonWeeksText(startWeek, endWeek, parity, semesterWeeksForLesson(lesson));
|
||||
}
|
||||
|
||||
function formatLessonWeeksText(startWeek, endWeek, parity, semesterWeeks) {
|
||||
const semesterEndWeek = lastApplicableSemesterWeek(semesterWeeks, parity);
|
||||
const semesterStartWeek = firstApplicableSemesterWeek(parity);
|
||||
if (semesterEndWeek && startWeek <= semesterStartWeek && endWeek >= semesterEndWeek) return '';
|
||||
if (semesterEndWeek && endWeek >= semesterEndWeek) return `(с ${startWeek} нед.)`;
|
||||
return startWeek === endWeek
|
||||
? `(${startWeek} нед.)`
|
||||
: `(с ${startWeek} по ${endWeek} нед.)`;
|
||||
}
|
||||
|
||||
function semesterWeeksForLesson(lesson) {
|
||||
const semester = (state.dictionaries.semesters || []).find(item => dateInRange(lesson.date, item.startDate, item.endDate));
|
||||
if (!semester?.startDate || !semester?.endDate) return DEFAULT_SEMESTER_WEEKS;
|
||||
const start = parseIsoDate(semester.startDate);
|
||||
const end = parseIsoDate(semester.endDate);
|
||||
const days = Math.max(1, Math.round((end - start) / 86400000) + 1);
|
||||
return Math.max(1, Math.ceil(days / 7));
|
||||
}
|
||||
|
||||
function dateInRange(date, startDate, endDate) {
|
||||
return Boolean(date && startDate && endDate && date >= startDate && date <= endDate);
|
||||
}
|
||||
|
||||
function firstApplicableSemesterWeek(parity) {
|
||||
return parity === 'EVEN' ? 2 : 1;
|
||||
}
|
||||
|
||||
function lastApplicableSemesterWeek(maxWeeks, parity) {
|
||||
const totalWeeks = Number(maxWeeks || 0);
|
||||
if (!totalWeeks) return 0;
|
||||
if (parity === 'ODD' && totalWeeks % 2 === 0) return totalWeeks - 1;
|
||||
if (parity === 'EVEN' && totalWeeks % 2 !== 0) return totalWeeks - 1;
|
||||
return totalWeeks;
|
||||
}
|
||||
|
||||
function renderLessonCard(lesson) {
|
||||
@@ -694,13 +727,14 @@ export async function initScheduleView() {
|
||||
}
|
||||
|
||||
async function loadDictionaries(role, userDepartmentId) {
|
||||
const [groups, teachers, classrooms, departments, subjects, lessonTypes] = await Promise.all([
|
||||
const [groups, teachers, classrooms, departments, subjects, lessonTypes, academicYears] = await Promise.all([
|
||||
api.get('/api/groups'),
|
||||
api.get('/api/users/teachers'),
|
||||
api.get('/api/classrooms'),
|
||||
api.get('/api/departments'),
|
||||
api.get('/api/subjects'),
|
||||
api.get('/api/lesson-types')
|
||||
api.get('/api/lesson-types'),
|
||||
api.get('/api/admin/calendar/years').catch(() => [])
|
||||
]);
|
||||
|
||||
const visibleGroups = role === ROLE_DEPARTMENT && userDepartmentId
|
||||
@@ -718,7 +752,8 @@ async function loadDictionaries(role, userDepartmentId) {
|
||||
groups,
|
||||
teachers,
|
||||
classrooms,
|
||||
departments
|
||||
departments,
|
||||
semesters: (academicYears || []).flatMap(year => year.semesters || [])
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,8 @@ export async function initSchedule() {
|
||||
const visualBackdrop = document.getElementById('schedule-visual-backdrop');
|
||||
const visualStage = document.getElementById('schedule-visual-stage');
|
||||
const visualSummary = document.getElementById('schedule-visual-summary');
|
||||
const visualYearSelect = document.getElementById('schedule-visual-year');
|
||||
const visualSemesterSelect = document.getElementById('schedule-visual-semester');
|
||||
const visualGroupFilter = document.getElementById('schedule-visual-group-filter');
|
||||
const visualUseFormButton = document.getElementById('schedule-visual-use-form');
|
||||
const visualSelectAllButton = document.getElementById('schedule-visual-select-all');
|
||||
@@ -94,9 +96,14 @@ export async function initSchedule() {
|
||||
let lessonTypes = [];
|
||||
let subgroups = [];
|
||||
let timeSlots = [];
|
||||
let academicYears = [];
|
||||
let semesters = [];
|
||||
let selectedVisualYearId = '';
|
||||
let selectedVisualSemesterId = '';
|
||||
let selectedVisualGroupIds = new Set();
|
||||
let visualGroupFilterTouched = false;
|
||||
let visualPeriodTouched = false;
|
||||
let syncingVisualPeriod = false;
|
||||
|
||||
bindEvents();
|
||||
|
||||
@@ -116,17 +123,30 @@ export async function initSchedule() {
|
||||
visualToggleButton?.addEventListener('click', toggleVisualDrawer);
|
||||
visualCloseButton?.addEventListener('click', closeVisualDrawer);
|
||||
visualBackdrop?.addEventListener('click', closeVisualDrawer);
|
||||
visualYearSelect?.addEventListener('change', handleVisualYearChange);
|
||||
visualSemesterSelect?.addEventListener('change', handleVisualSemesterChange);
|
||||
visualGroupFilter?.addEventListener('change', handleVisualGroupFilterChange);
|
||||
visualStage?.addEventListener('click', handleVisualStageClick);
|
||||
visualUseFormButton?.addEventListener('click', () => {
|
||||
const formSemesterId = ruleSemesterSelect.value;
|
||||
if (formSemesterId) {
|
||||
visualPeriodTouched = true;
|
||||
setVisualPeriodBySemesterId(formSemesterId);
|
||||
}
|
||||
visualGroupFilterTouched = false;
|
||||
selectedVisualGroupIds = new Set();
|
||||
renderVisualSchedule();
|
||||
});
|
||||
visualSelectAllButton?.addEventListener('click', () => {
|
||||
selectedVisualGroupIds = new Set(collectVisualGroups().map(group => String(group.id)));
|
||||
selectedVisualGroupIds = new Set(collectVisualGroups(visualRulesForSelectedPeriod()).map(group => String(group.id)));
|
||||
visualGroupFilterTouched = true;
|
||||
renderVisualSchedule();
|
||||
});
|
||||
ruleSemesterSelect?.addEventListener('change', () => {
|
||||
if (visualPeriodTouched) return;
|
||||
selectDefaultVisualPeriod();
|
||||
renderVisualSchedule();
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') closeVisualDrawer();
|
||||
});
|
||||
@@ -190,15 +210,19 @@ export async function initSchedule() {
|
||||
}
|
||||
|
||||
async function loadYears() {
|
||||
const academicYears = await api.get('/api/admin/calendar/years');
|
||||
academicYears = await api.get('/api/admin/calendar/years');
|
||||
semesters = academicYears.flatMap(year => year.semesters || []);
|
||||
populateSemesterSelects();
|
||||
selectDefaultVisualPeriod();
|
||||
}
|
||||
|
||||
async function loadRules() {
|
||||
rulesTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Загрузка...</td></tr>';
|
||||
try {
|
||||
rules = await api.get('/api/admin/schedule-rules');
|
||||
if (!visualPeriodTouched) {
|
||||
selectDefaultVisualPeriod();
|
||||
}
|
||||
renderRules();
|
||||
renderVisualSchedule();
|
||||
} catch (error) {
|
||||
@@ -222,8 +246,10 @@ export async function initSchedule() {
|
||||
<td>${renderRuleLoadSummary(rule)}</td>
|
||||
<td>${renderRuleSlotsSummary(rule.slots)}</td>
|
||||
<td>
|
||||
<button class="btn-edit-classroom btn-edit-rule" data-id="${rule.id}">Изменить</button>
|
||||
<button class="btn-delete btn-delete-rule" data-id="${rule.id}">Удалить</button>
|
||||
<div class="schedule-rule-table-actions">
|
||||
<button type="button" class="schedule-rule-table-button schedule-rule-table-button-edit btn-edit-rule" data-id="${rule.id}">Изменить</button>
|
||||
<button type="button" class="schedule-rule-table-button schedule-rule-table-button-delete btn-delete-rule" data-id="${rule.id}">Удалить</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
@@ -262,16 +288,32 @@ export async function initSchedule() {
|
||||
|
||||
function renderVisualSchedule() {
|
||||
if (!visualStage || !visualSummary) return;
|
||||
if (!selectedVisualSemesterId) {
|
||||
syncVisualGroupFilter([]);
|
||||
visualSummary.textContent = 'Выберите учебный год и семестр';
|
||||
setVisualState('Выберите период, чтобы увидеть правила конкретного семестра');
|
||||
return;
|
||||
}
|
||||
if (!rules.length) {
|
||||
syncVisualGroupFilter([]);
|
||||
visualSummary.textContent = 'Нет сохранённых правил';
|
||||
setVisualState('Сохраните правила расписания, чтобы увидеть матрицу по группам');
|
||||
return;
|
||||
}
|
||||
|
||||
const allGroups = collectVisualGroups();
|
||||
const visualRules = visualRulesForSelectedPeriod();
|
||||
const periodLabel = visualPeriodLabel();
|
||||
if (!visualRules.length) {
|
||||
syncVisualGroupFilter([]);
|
||||
visualSummary.textContent = `${periodLabel} · правил нет`;
|
||||
setVisualState('В выбранном семестре нет сохранённых правил расписания');
|
||||
return;
|
||||
}
|
||||
|
||||
const allGroups = collectVisualGroups(visualRules);
|
||||
syncVisualGroupFilter(allGroups);
|
||||
const columns = allGroups.filter(group => selectedVisualGroupIds.has(String(group.id)));
|
||||
const slots = timeSlots.length ? timeSlots.map(visualSlotFromTimeSlot) : collectVisualSlotsFromRules();
|
||||
const slots = timeSlots.length ? timeSlots.map(visualSlotFromTimeSlot) : collectVisualSlotsFromRules(visualRules);
|
||||
if (!allGroups.length || !slots.length) {
|
||||
visualSummary.textContent = 'Недостаточно данных для построения';
|
||||
setVisualState('У правил должны быть группы и слоты занятий');
|
||||
@@ -283,15 +325,15 @@ export async function initSchedule() {
|
||||
return;
|
||||
}
|
||||
|
||||
const lessonsByCell = buildVisualLessonsByCell(selectedVisualGroupIds);
|
||||
const lessonsByCell = buildVisualLessonsByCell(selectedVisualGroupIds, visualRules);
|
||||
const rows = buildVisualRows(columns, slots, lessonsByCell);
|
||||
if (!rows.length) {
|
||||
visualSummary.textContent = `${columns.length} ${groupCountLabel(columns.length)} · пар не найдено`;
|
||||
setVisualState('Для выбранных групп нет активных пар в сохранённых правилах');
|
||||
visualSummary.textContent = `${periodLabel} · ${columns.length} ${groupCountLabel(columns.length)} · пар не найдено`;
|
||||
setVisualState('Для выбранных групп нет активных пар в правилах выбранного семестра');
|
||||
return;
|
||||
}
|
||||
|
||||
visualSummary.textContent = `${rules.length} ${ruleCountLabel(rules.length)} · ${columns.length} ${groupCountLabel(columns.length)} · ${rows.length} ${rowCountLabel(rows.length)}`;
|
||||
visualSummary.textContent = `${periodLabel} · ${visualRules.length} ${ruleCountLabel(visualRules.length)} · ${columns.length} ${groupCountLabel(columns.length)} · ${rows.length} ${rowCountLabel(rows.length)}`;
|
||||
visualStage.innerHTML = `
|
||||
<div class="schedule-visual-table-wrap">
|
||||
<table class="schedule-visual-table">
|
||||
@@ -348,10 +390,10 @@ export async function initSchedule() {
|
||||
`;
|
||||
}
|
||||
|
||||
function buildVisualLessonsByCell(allowedGroupIds) {
|
||||
function buildVisualLessonsByCell(allowedGroupIds, visualRules) {
|
||||
const grouped = new Map();
|
||||
const activeWeeks = buildActiveWeeksByRuleSlot();
|
||||
rules.forEach(rule => {
|
||||
const activeWeeks = buildActiveWeeksByRuleSlot(visualRules);
|
||||
visualRules.forEach(rule => {
|
||||
(rule.slots || []).forEach((slot, index) => {
|
||||
const slotKey = visualRuleSlotKey(slot, index);
|
||||
(rule.groupIds || []).forEach((groupId, groupIndex) => {
|
||||
@@ -360,6 +402,7 @@ export async function initSchedule() {
|
||||
if (!weeks.length) return;
|
||||
const lesson = {
|
||||
id: `${rule.id}:${slotKey}:${groupId}`,
|
||||
ruleId: rule.id,
|
||||
parity: slot.parity || 'BOTH',
|
||||
subjectName: rule.subjectName || 'Без дисциплины',
|
||||
teacherName: slot.teacherName || '',
|
||||
@@ -367,7 +410,7 @@ export async function initSchedule() {
|
||||
lessonTypeName: slot.lessonTypeName || '',
|
||||
lessonFormat: slot.lessonFormat || '',
|
||||
subgroupNames: slotSubgroupNames(slot),
|
||||
weekText: compactWeekRanges(weeks),
|
||||
weekText: compactWeekRanges(weeks, maxWeeksForRule(rule), slot.parity),
|
||||
groupName: (rule.groupNames || [])[groupIndex] || groupNameById(groupId)
|
||||
};
|
||||
const key = visualCellKey(groupId, slot.dayOfWeek, visualSlotKey(slot));
|
||||
@@ -380,9 +423,9 @@ export async function initSchedule() {
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function buildActiveWeeksByRuleSlot() {
|
||||
function buildActiveWeeksByRuleSlot(visualRules) {
|
||||
const activeWeeks = new Map();
|
||||
rules.forEach(rule => {
|
||||
visualRules.forEach(rule => {
|
||||
const maxWeeks = maxWeeksForRule(rule);
|
||||
(rule.groupIds || []).forEach(groupId => {
|
||||
LESSON_TYPE_LIMITS.forEach(config => {
|
||||
@@ -454,13 +497,18 @@ export async function initSchedule() {
|
||||
<strong>${escapeHtml(lesson.subjectName)}${lesson.weekText ? ` <span>${escapeHtml(lesson.weekText)}</span>` : ''}</strong>
|
||||
${subgroupText ? `<small>${escapeHtml(subgroupText)}</small>` : ''}
|
||||
${meta ? `<small>${escapeHtml(meta)}</small>` : ''}
|
||||
<button type="button"
|
||||
class="schedule-visual-edit-rule"
|
||||
data-rule-id="${escapeHtml(String(lesson.ruleId))}"
|
||||
title="Изменить правило"
|
||||
aria-label="${escapeHtml(`Изменить правило: ${lesson.subjectName}`)}">⋮</button>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function collectVisualGroups() {
|
||||
function collectVisualGroups(visualRules = rules) {
|
||||
const byId = new Map();
|
||||
rules.forEach(rule => {
|
||||
visualRules.forEach(rule => {
|
||||
(rule.groupIds || []).forEach((id, index) => {
|
||||
if (!byId.has(String(id))) {
|
||||
byId.set(String(id), {
|
||||
@@ -473,9 +521,9 @@ export async function initSchedule() {
|
||||
return Array.from(byId.values()).sort((a, b) => naturalCompare(a.name, b.name));
|
||||
}
|
||||
|
||||
function collectVisualSlotsFromRules() {
|
||||
function collectVisualSlotsFromRules(visualRules = rules) {
|
||||
const byKey = new Map();
|
||||
rules.flatMap(rule => rule.slots || []).forEach(slot => {
|
||||
visualRules.flatMap(rule => rule.slots || []).forEach(slot => {
|
||||
const key = visualSlotKey(slot);
|
||||
if (!byKey.has(key)) {
|
||||
byKey.set(key, {
|
||||
@@ -511,6 +559,39 @@ export async function initSchedule() {
|
||||
renderVisualSchedule();
|
||||
}
|
||||
|
||||
function handleVisualYearChange() {
|
||||
if (syncingVisualPeriod) return;
|
||||
selectedVisualYearId = visualYearSelect?.value || '';
|
||||
selectedVisualSemesterId = firstSemesterForYear(selectedVisualYearId)?.id
|
||||
? String(firstSemesterForYear(selectedVisualYearId).id)
|
||||
: '';
|
||||
visualPeriodTouched = true;
|
||||
visualGroupFilterTouched = false;
|
||||
populateVisualSemesterSelect();
|
||||
renderVisualSchedule();
|
||||
}
|
||||
|
||||
function handleVisualSemesterChange() {
|
||||
if (syncingVisualPeriod) return;
|
||||
selectedVisualSemesterId = visualSemesterSelect?.value || '';
|
||||
visualPeriodTouched = true;
|
||||
visualGroupFilterTouched = false;
|
||||
renderVisualSchedule();
|
||||
}
|
||||
|
||||
function handleVisualStageClick(event) {
|
||||
const editButton = event.target.closest('.schedule-visual-edit-rule');
|
||||
if (!editButton) return;
|
||||
const rule = rules.find(item => String(item.id) === String(editButton.dataset.ruleId));
|
||||
if (!rule) {
|
||||
showAlert('schedule-rule-alert', 'Правило не найдено. Обновите список правил.', 'error');
|
||||
return;
|
||||
}
|
||||
fillRuleForm(rule);
|
||||
closeVisualDrawer();
|
||||
showAlert('schedule-rule-alert', 'Правило открыто для редактирования', 'success');
|
||||
}
|
||||
|
||||
function syncVisualGroupFilter(allGroups) {
|
||||
ensureVisualGroupSelection(allGroups);
|
||||
if (!visualGroupFilter) return;
|
||||
@@ -547,6 +628,92 @@ export async function initSchedule() {
|
||||
}
|
||||
}
|
||||
|
||||
function selectDefaultVisualPeriod() {
|
||||
const formSemester = semesterById(ruleSemesterSelect?.value);
|
||||
const ruleSemester = firstSemesterWithRules();
|
||||
const fallbackSemester = semesters[0];
|
||||
const semester = formSemester || ruleSemester || fallbackSemester;
|
||||
setVisualPeriodBySemesterId(semester?.id || '');
|
||||
}
|
||||
|
||||
function setVisualPeriodBySemesterId(semesterId) {
|
||||
const semester = semesterById(semesterId);
|
||||
selectedVisualSemesterId = semester ? String(semester.id) : '';
|
||||
selectedVisualYearId = semester ? String(semester.academicYearId) : '';
|
||||
populateVisualYearSelect();
|
||||
populateVisualSemesterSelect();
|
||||
}
|
||||
|
||||
function populateVisualYearSelect() {
|
||||
if (!visualYearSelect) return;
|
||||
visualYearSelect.innerHTML = '<option value="">Выберите учебный год</option>' +
|
||||
academicYears.map(year => `<option value="${year.id}">${escapeHtml(year.title)}</option>`).join('');
|
||||
if (selectedVisualYearId && academicYears.some(year => String(year.id) === selectedVisualYearId)) {
|
||||
visualYearSelect.value = selectedVisualYearId;
|
||||
} else if (academicYears.length) {
|
||||
selectedVisualYearId = String(academicYears[0].id);
|
||||
visualYearSelect.value = selectedVisualYearId;
|
||||
} else {
|
||||
selectedVisualYearId = '';
|
||||
visualYearSelect.value = '';
|
||||
}
|
||||
syncVisualPeriodSelects(visualYearSelect);
|
||||
}
|
||||
|
||||
function populateVisualSemesterSelect() {
|
||||
if (!visualSemesterSelect) return;
|
||||
const yearSemesters = semestersForYear(selectedVisualYearId);
|
||||
visualSemesterSelect.innerHTML = yearSemesters.length
|
||||
? '<option value="">Выберите семестр</option>' + yearSemesters
|
||||
.map(semester => `<option value="${semester.id}">${escapeHtml(shortSemesterLabel(semester))}</option>`)
|
||||
.join('')
|
||||
: '<option value="">Нет семестров</option>';
|
||||
|
||||
if (selectedVisualSemesterId && yearSemesters.some(semester => String(semester.id) === selectedVisualSemesterId)) {
|
||||
visualSemesterSelect.value = selectedVisualSemesterId;
|
||||
} else if (yearSemesters.length) {
|
||||
selectedVisualSemesterId = String(yearSemesters[0].id);
|
||||
visualSemesterSelect.value = selectedVisualSemesterId;
|
||||
} else {
|
||||
selectedVisualSemesterId = '';
|
||||
visualSemesterSelect.value = '';
|
||||
}
|
||||
syncVisualPeriodSelects(visualSemesterSelect);
|
||||
}
|
||||
|
||||
function syncVisualPeriodSelects(...selects) {
|
||||
syncingVisualPeriod = true;
|
||||
syncSelects(...selects);
|
||||
syncingVisualPeriod = false;
|
||||
}
|
||||
|
||||
function visualRulesForSelectedPeriod() {
|
||||
if (!selectedVisualSemesterId) return [];
|
||||
return rules.filter(rule => String(rule.semesterId) === selectedVisualSemesterId);
|
||||
}
|
||||
|
||||
function firstSemesterWithRules() {
|
||||
const ruleSemesterIds = new Set(rules.map(rule => String(rule.semesterId)));
|
||||
return semesters.find(semester => ruleSemesterIds.has(String(semester.id)));
|
||||
}
|
||||
|
||||
function semesterById(id) {
|
||||
return semesters.find(semester => String(semester.id) === String(id));
|
||||
}
|
||||
|
||||
function semestersForYear(yearId) {
|
||||
return semesters.filter(semester => String(semester.academicYearId) === String(yearId));
|
||||
}
|
||||
|
||||
function firstSemesterForYear(yearId) {
|
||||
return semestersForYear(yearId)[0] || null;
|
||||
}
|
||||
|
||||
function visualPeriodLabel() {
|
||||
const semester = semesterById(selectedVisualSemesterId);
|
||||
return semester ? shortSemesterLabel(semester) : 'Семестр не выбран';
|
||||
}
|
||||
|
||||
function renderRuleSlotsSummary(slots) {
|
||||
if (!slots || !slots.length) return '<span class="muted">Нет слотов</span>';
|
||||
return slots.map(slot => {
|
||||
@@ -687,7 +854,7 @@ export async function initSchedule() {
|
||||
<div class="form-group"><label>Пара</label><select class="slot-time" required>${options(timeSlots.map(toTimeSlotOption), slot.timeSlotId)}</select></div>
|
||||
<div class="form-group"><label>Преподаватель</label><select class="slot-teacher" required>${options(teachers.map(toTeacherOption), slot.teacherId)}</select></div>
|
||||
<div class="form-group"><label>Аудитория</label><select class="slot-classroom" required>${options(classrooms.map(toClassroomOption), slot.classroomId)}</select></div>
|
||||
<div class="form-group"><label>Тип</label><select class="slot-lesson-type" required>${options(lessonTypes.map(toLessonTypeOption), slot.lessonTypeId)}</select></div>
|
||||
<div class="form-group"><label>Тип</label><select class="slot-lesson-type" required>${options(orderedLessonTypes().map(toLessonTypeOption), slot.lessonTypeId)}</select></div>
|
||||
${isLaboratorySlot(slot) ? renderSubgroupField(slot, index) : ''}
|
||||
<div class="form-group"><label>Формат</label><select class="slot-format" required>${options([{ value: 'Очно', label: 'Очно' }, { value: 'Онлайн', label: 'Онлайн' }], slot.lessonFormat || 'Очно')}</select></div>
|
||||
<button type="button" class="btn-delete schedule-slot-remove" data-index="${index}">Удалить</button>
|
||||
@@ -1022,6 +1189,23 @@ export async function initSchedule() {
|
||||
return { value: lessonType.id, label: lessonType.name || lessonType.lessonType || `ID ${lessonType.id}` };
|
||||
}
|
||||
|
||||
function orderedLessonTypes() {
|
||||
return [...lessonTypes].sort(compareLessonTypes);
|
||||
}
|
||||
|
||||
function compareLessonTypes(first, second) {
|
||||
return lessonTypeRank(first) - lessonTypeRank(second)
|
||||
|| naturalCompare(toLessonTypeOption(first).label, toLessonTypeOption(second).label);
|
||||
}
|
||||
|
||||
function lessonTypeRank(lessonType) {
|
||||
const key = lessonTypeKey(lessonType);
|
||||
if (key === 'lecture') return 0;
|
||||
if (key === 'laboratory') return 1;
|
||||
if (key === 'practice') return 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
function lessonTypeKeyById(lessonTypeId) {
|
||||
const lessonType = lessonTypes.find(item => String(item.id) === String(lessonTypeId));
|
||||
return lessonType ? lessonTypeKey(lessonType) : null;
|
||||
@@ -1048,6 +1232,11 @@ export async function initSchedule() {
|
||||
return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${semester.startDate} - ${semester.endDate})`;
|
||||
}
|
||||
|
||||
function shortSemesterLabel(semester) {
|
||||
const type = normalizeSemesterType(semester.semesterType);
|
||||
return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type}`;
|
||||
}
|
||||
|
||||
function formatSemester(rule) {
|
||||
const type = normalizeSemesterType(rule.semesterType);
|
||||
return `${rule.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type || '-'}`;
|
||||
@@ -1106,16 +1295,34 @@ export async function initSchedule() {
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
function compactWeekRanges(weeks) {
|
||||
function compactWeekRanges(weeks, maxWeeks, parity) {
|
||||
const uniqueWeeks = [...new Set((weeks || []).map(Number).filter(Boolean))].sort((a, b) => a - b);
|
||||
if (!uniqueWeeks.length) return '';
|
||||
return `(${weekRangeText(uniqueWeeks[0], uniqueWeeks[uniqueWeeks.length - 1])})`;
|
||||
const start = uniqueWeeks[0];
|
||||
const end = uniqueWeeks[uniqueWeeks.length - 1];
|
||||
const semesterEndWeek = lastApplicableSemesterWeek(maxWeeks, parity);
|
||||
const semesterStartWeek = firstApplicableSemesterWeek(parity);
|
||||
if (semesterEndWeek && start <= semesterStartWeek && end >= semesterEndWeek) return '';
|
||||
if (semesterEndWeek && end >= semesterEndWeek) return `(с ${start} нед.)`;
|
||||
return weekRangeText(start, end);
|
||||
}
|
||||
|
||||
function weekRangeText(start, end) {
|
||||
return start === end
|
||||
? `${start} нед.`
|
||||
: `с ${start} по ${end} нед.`;
|
||||
? `(${start} нед.)`
|
||||
: `(с ${start} по ${end} нед.)`;
|
||||
}
|
||||
|
||||
function firstApplicableSemesterWeek(parity) {
|
||||
return parity === 'EVEN' ? 2 : 1;
|
||||
}
|
||||
|
||||
function lastApplicableSemesterWeek(maxWeeks, parity) {
|
||||
const totalWeeks = Number(maxWeeks || 0);
|
||||
if (!totalWeeks) return 0;
|
||||
if (parity === 'ODD' && totalWeeks % 2 === 0) return totalWeeks - 1;
|
||||
if (parity === 'EVEN' && totalWeeks % 2 !== 0) return totalWeeks - 1;
|
||||
return totalWeeks;
|
||||
}
|
||||
|
||||
function compareSlotItems(first, second) {
|
||||
|
||||
Reference in New Issue
Block a user