подправил создание правил расписания
This commit is contained in:
@@ -22,6 +22,8 @@ const SEMESTER_LABELS = {
|
||||
spring: 'весенний'
|
||||
};
|
||||
|
||||
const WHOLE_GROUP_SUBGROUP_VALUE = '__whole_group__';
|
||||
|
||||
const LESSON_TYPE_LIMITS = [
|
||||
{
|
||||
key: 'lecture',
|
||||
@@ -59,7 +61,10 @@ export async function initSchedule() {
|
||||
hours: document.getElementById(config.hoursInputId),
|
||||
startWeek: document.getElementById(config.startWeekInputId)
|
||||
}]));
|
||||
const ruleGroupsContainer = document.getElementById('schedule-rule-groups');
|
||||
const ruleGroupsBox = document.getElementById('schedule-rule-groups-box');
|
||||
const ruleGroupsMenu = document.getElementById('schedule-rule-groups-menu');
|
||||
const ruleGroupsText = document.getElementById('schedule-rule-groups-text');
|
||||
const ruleGroupCheckboxes = document.getElementById('schedule-rule-group-checkboxes');
|
||||
const ruleSlotsContainer = document.getElementById('schedule-rule-slots');
|
||||
const ruleResetButton = document.getElementById('schedule-rule-reset');
|
||||
const slotAddButton = document.getElementById('schedule-slot-add');
|
||||
@@ -94,16 +99,44 @@ export async function initSchedule() {
|
||||
|
||||
ruleSlotsContainer.addEventListener('click', (event) => {
|
||||
const removeButton = event.target.closest('.schedule-slot-remove');
|
||||
if (!removeButton) return;
|
||||
const slots = readRuleSlots(false);
|
||||
slots.splice(Number(removeButton.dataset.index), 1);
|
||||
renderRuleSlots(slots.length ? slots : [{}]);
|
||||
const subgroupBox = event.target.closest('.slot-subgroup-box');
|
||||
const subgroupMenu = event.target.closest('.slot-subgroup-menu');
|
||||
|
||||
if (removeButton) {
|
||||
const slots = readRuleSlots(false);
|
||||
slots.splice(Number(removeButton.dataset.index), 1);
|
||||
renderRuleSlots(slots.length ? slots : [{}]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subgroupBox) {
|
||||
toggleSlotSubgroupMenu(event, subgroupBox);
|
||||
return;
|
||||
}
|
||||
|
||||
if (subgroupMenu) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
ruleGroupsContainer.addEventListener('change', () => renderRuleSlots(readRuleSlots(false)));
|
||||
ruleGroupsBox.addEventListener('click', toggleRuleGroupsMenu);
|
||||
ruleGroupsBox.addEventListener('keydown', handleRuleGroupsKeydown);
|
||||
ruleGroupsMenu.addEventListener('click', event => event.stopPropagation());
|
||||
ruleGroupCheckboxes.addEventListener('change', () => {
|
||||
updateRuleGroupsText();
|
||||
renderRuleSlots(readRuleSlots(false));
|
||||
});
|
||||
document.addEventListener('click', () => {
|
||||
closeRuleGroupsMenu();
|
||||
closeSlotSubgroupMenus();
|
||||
});
|
||||
ruleSlotsContainer.addEventListener('change', (event) => {
|
||||
if (event.target.closest('.slot-lesson-type')) {
|
||||
renderRuleSlots(readRuleSlots(false));
|
||||
return;
|
||||
}
|
||||
if (event.target.closest('.slot-subgroup-checkboxes')) {
|
||||
updateSlotSubgroupText(event.target.closest('.schedule-slot-row'));
|
||||
}
|
||||
});
|
||||
ruleForm.addEventListener('submit', saveRule);
|
||||
@@ -167,7 +200,7 @@ export async function initSchedule() {
|
||||
slot.dayName,
|
||||
slot.timeSlotLabel,
|
||||
parity,
|
||||
slot.subgroupName,
|
||||
slotSubgroupNames(slot).join(', '),
|
||||
slot.teacherName,
|
||||
slot.classroomName,
|
||||
slot.lessonTypeName,
|
||||
@@ -232,8 +265,7 @@ export async function initSchedule() {
|
||||
}
|
||||
|
||||
function buildRulePayload() {
|
||||
const groupIds = Array.from(ruleGroupsContainer.querySelectorAll('input[type="checkbox"]:checked'))
|
||||
.map(input => Number(input.value));
|
||||
const groupIds = selectedRuleGroupIds();
|
||||
const slots = readRuleSlots(true);
|
||||
if (!ruleSubjectSelect.value) throw new Error('Выберите дисциплину');
|
||||
if (!ruleSemesterSelect.value) throw new Error('Выберите семестр');
|
||||
@@ -277,29 +309,31 @@ export async function initSchedule() {
|
||||
|
||||
function renderRuleGroups(selectedIds) {
|
||||
if (!groups.length) {
|
||||
ruleGroupsContainer.innerHTML = '<div class="loading-row">Нет групп</div>';
|
||||
ruleGroupCheckboxes.innerHTML = '<div class="loading-row">Нет групп</div>';
|
||||
updateRuleGroupsText();
|
||||
return;
|
||||
}
|
||||
const selected = new Set((selectedIds || []).map(String));
|
||||
ruleGroupsContainer.innerHTML = groups.map(group => `
|
||||
ruleGroupCheckboxes.innerHTML = groups.map(group => `
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="${group.id}" ${selected.has(String(group.id)) ? 'checked' : ''}>
|
||||
<span class="checkmark"></span>
|
||||
<span>${escapeHtml(group.name)}</span>
|
||||
</label>
|
||||
`).join('');
|
||||
updateRuleGroupsText();
|
||||
}
|
||||
|
||||
function renderRuleSlots(slots) {
|
||||
ruleSlotsContainer.innerHTML = (slots || [{}]).map((slot, index) => `
|
||||
<div class="schedule-slot-row" data-index="${index}">
|
||||
<div class="schedule-slot-row ${isLaboratorySlot(slot) ? 'has-subgroup' : ''}" data-index="${index}">
|
||||
<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>
|
||||
<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-subgroup" ${isLaboratorySlot(slot) ? '' : 'disabled'}>${subgroupOptions(slot)}</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>
|
||||
</div>
|
||||
@@ -309,13 +343,14 @@ export async function initSchedule() {
|
||||
function readRuleSlots(validate) {
|
||||
const rows = Array.from(ruleSlotsContainer.querySelectorAll('.schedule-slot-row'));
|
||||
return rows.map((row, index) => {
|
||||
const subgroupValue = row.querySelector('.slot-subgroup')?.value || '';
|
||||
const subgroupIds = readSlotSubgroupIds(row);
|
||||
const slot = {
|
||||
id: null,
|
||||
dayOfWeek: Number(row.querySelector('.slot-day').value),
|
||||
parity: row.querySelector('.slot-parity').value,
|
||||
timeSlotId: Number(row.querySelector('.slot-time').value),
|
||||
subgroupId: subgroupValue ? Number(subgroupValue) : null,
|
||||
subgroupId: subgroupIds[0] || null,
|
||||
subgroupIds,
|
||||
teacherId: Number(row.querySelector('.slot-teacher').value),
|
||||
classroomId: Number(row.querySelector('.slot-classroom').value),
|
||||
lessonTypeId: Number(row.querySelector('.slot-lesson-type').value),
|
||||
@@ -370,18 +405,26 @@ export async function initSchedule() {
|
||||
|
||||
function validateSlotSubgroup(slot, index) {
|
||||
const typeKey = lessonTypeKeyById(slot.lessonTypeId);
|
||||
if (slot.subgroupId && typeKey !== 'laboratory') {
|
||||
const subgroupIds = slotSubgroupIds(slot);
|
||||
if (subgroupIds.length && typeKey !== 'laboratory') {
|
||||
throw new Error(`В слоте ${index + 1} подгруппа доступна только для лабораторных`);
|
||||
}
|
||||
if (!slot.subgroupId) return;
|
||||
if (!subgroupIds.length) return;
|
||||
|
||||
const selectedSubgroup = subgroups.find(item => String(item.id) === String(slot.subgroupId));
|
||||
if (!selectedSubgroup) {
|
||||
throw new Error(`В слоте ${index + 1} подгруппа не найдена`);
|
||||
}
|
||||
const selectedGroups = new Set(selectedRuleGroupIds().map(String));
|
||||
if (!selectedGroups.has(String(selectedSubgroup.groupId))) {
|
||||
throw new Error(`В слоте ${index + 1} подгруппа должна относиться к выбранным группам правила`);
|
||||
const subgroupGroupIds = new Set();
|
||||
for (const subgroupId of subgroupIds) {
|
||||
const selectedSubgroup = subgroups.find(item => String(item.id) === String(subgroupId));
|
||||
if (!selectedSubgroup) {
|
||||
throw new Error(`В слоте ${index + 1} подгруппа не найдена`);
|
||||
}
|
||||
if (!selectedGroups.has(String(selectedSubgroup.groupId))) {
|
||||
throw new Error(`В слоте ${index + 1} подгруппа должна относиться к выбранным группам правила`);
|
||||
}
|
||||
if (subgroupGroupIds.has(String(selectedSubgroup.groupId))) {
|
||||
throw new Error(`В слоте ${index + 1} выберите не больше одной подгруппы каждой группы`);
|
||||
}
|
||||
subgroupGroupIds.add(String(selectedSubgroup.groupId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,22 +460,196 @@ export async function initSchedule() {
|
||||
if (!isLaboratorySlot(slot)) {
|
||||
return '<option value="">Только для лабораторных</option>';
|
||||
}
|
||||
const selected = slot.subgroupId == null ? '' : String(slot.subgroupId);
|
||||
const selectedGroups = new Set(selectedRuleGroupIds().map(String));
|
||||
const selectedGroupIds = selectedRuleGroupIds();
|
||||
if (selectedGroupIds.length !== 1) {
|
||||
return '<option value="">Выберите группу правила</option>';
|
||||
}
|
||||
const selected = slotSubgroupIds(slot)[0] == null
|
||||
? WHOLE_GROUP_SUBGROUP_VALUE
|
||||
: String(slotSubgroupIds(slot)[0]);
|
||||
const selectedGroups = new Set(selectedGroupIds.map(String));
|
||||
const matchingSubgroups = subgroups
|
||||
.filter(subgroup => selectedGroups.has(String(subgroup.groupId)))
|
||||
.sort((a, b) => subgroupLabel(a).localeCompare(subgroupLabel(b), 'ru-RU'));
|
||||
return '<option value="">Вся группа</option>' + matchingSubgroups.map(subgroup => {
|
||||
return `<option value="${WHOLE_GROUP_SUBGROUP_VALUE}" ${selected === WHOLE_GROUP_SUBGROUP_VALUE ? 'selected' : ''}>Вся группа</option>` + matchingSubgroups.map(subgroup => {
|
||||
const value = String(subgroup.id);
|
||||
return `<option value="${escapeHtml(value)}" ${value === selected ? 'selected' : ''}>${escapeHtml(subgroupLabel(subgroup))}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderSubgroupField(slot, index) {
|
||||
return selectedRuleGroupIds().length > 1
|
||||
? renderSubgroupMultiSelect(slot, index)
|
||||
: `<div class="form-group"><label>Подгруппа</label><select class="slot-subgroup">${subgroupOptions(slot)}</select></div>`;
|
||||
}
|
||||
|
||||
function renderSubgroupMultiSelect(slot, index) {
|
||||
const selected = new Set(slotSubgroupIds(slot).map(String));
|
||||
const selectedGroups = new Set(selectedRuleGroupIds().map(String));
|
||||
const matchingSubgroups = subgroups
|
||||
.filter(subgroup => selectedGroups.has(String(subgroup.groupId)))
|
||||
.sort((a, b) => subgroupLabel(a).localeCompare(subgroupLabel(b), 'ru-RU'));
|
||||
const items = matchingSubgroups.length
|
||||
? matchingSubgroups.map(subgroup => {
|
||||
const value = String(subgroup.id);
|
||||
return `
|
||||
<label class="checkbox-item">
|
||||
<input type="checkbox" value="${escapeHtml(value)}" ${selected.has(value) ? 'checked' : ''}>
|
||||
<span class="checkmark"></span>
|
||||
<span>${escapeHtml(subgroupLabel(subgroup))}</span>
|
||||
</label>
|
||||
`;
|
||||
}).join('')
|
||||
: '<div class="loading-row">Нет подгрупп выбранных групп</div>';
|
||||
|
||||
return `
|
||||
<div class="form-group slot-subgroup-multi">
|
||||
<label>Подгруппы</label>
|
||||
<div class="custom-multi-select">
|
||||
<div class="select-box slot-subgroup-box" role="button" tabindex="0" aria-expanded="false">
|
||||
<span class="select-text">${escapeHtml(slotSubgroupText(slot))}</span>
|
||||
<svg class="dropdown-icon" width="12" height="8" viewBox="0 0 12 8" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1 1.5L6 6.5L11 1.5" stroke="#9ca3af" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="dropdown-menu slot-subgroup-menu">
|
||||
<div class="checkbox-group-vertical slot-subgroup-checkboxes" data-index="${index}">
|
||||
${items}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function readSlotSubgroupIds(row) {
|
||||
const subgroupValue = row.querySelector('.slot-subgroup')?.value || '';
|
||||
const checkboxValues = Array.from(row.querySelectorAll('.slot-subgroup-checkboxes input[type="checkbox"]:checked'))
|
||||
.map(input => Number(input.value))
|
||||
.filter(Boolean);
|
||||
return checkboxValues.length
|
||||
? checkboxValues
|
||||
: (subgroupValue && subgroupValue !== WHOLE_GROUP_SUBGROUP_VALUE ? [Number(subgroupValue)] : []);
|
||||
}
|
||||
|
||||
function slotSubgroupIds(slot) {
|
||||
if (Array.isArray(slot.subgroupIds) && slot.subgroupIds.length) {
|
||||
return slot.subgroupIds.map(Number).filter(Boolean);
|
||||
}
|
||||
return slot.subgroupId ? [Number(slot.subgroupId)] : [];
|
||||
}
|
||||
|
||||
function slotSubgroupNames(slot) {
|
||||
if (Array.isArray(slot.subgroupNames) && slot.subgroupNames.length) {
|
||||
return slot.subgroupNames.filter(Boolean);
|
||||
}
|
||||
return slot.subgroupName ? [slot.subgroupName] : [];
|
||||
}
|
||||
|
||||
function slotSubgroupText(slot) {
|
||||
const selectedIds = slotSubgroupIds(slot);
|
||||
if (!selectedIds.length) return 'Все выбранные группы';
|
||||
if (selectedIds.length === 1) {
|
||||
const subgroup = subgroups.find(item => String(item.id) === String(selectedIds[0]));
|
||||
return subgroup ? subgroupLabel(subgroup) : 'Выбрана 1 подгруппа';
|
||||
}
|
||||
return `Выбрано подгрупп: ${selectedIds.length}`;
|
||||
}
|
||||
|
||||
function selectedRuleGroupIds() {
|
||||
return Array.from(ruleGroupsContainer.querySelectorAll('input[type="checkbox"]:checked'))
|
||||
return Array.from(ruleGroupCheckboxes.querySelectorAll('input[type="checkbox"]:checked'))
|
||||
.map(input => Number(input.value));
|
||||
}
|
||||
|
||||
function toggleRuleGroupsMenu(event) {
|
||||
event.stopPropagation();
|
||||
const isOpen = ruleGroupsMenu.classList.contains('open');
|
||||
document.querySelectorAll('.dropdown-menu').forEach(menu => menu.classList.remove('open'));
|
||||
document.querySelectorAll('.select-box').forEach(box => box.classList.remove('active'));
|
||||
if (isOpen) {
|
||||
closeRuleGroupsMenu();
|
||||
return;
|
||||
}
|
||||
ruleGroupsMenu.classList.add('open');
|
||||
ruleGroupsBox.classList.add('active');
|
||||
ruleGroupsBox.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
|
||||
function handleRuleGroupsKeydown(event) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
toggleRuleGroupsMenu(event);
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
closeRuleGroupsMenu();
|
||||
}
|
||||
}
|
||||
|
||||
function closeRuleGroupsMenu() {
|
||||
ruleGroupsMenu.classList.remove('open');
|
||||
ruleGroupsBox.classList.remove('active');
|
||||
ruleGroupsBox.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
function toggleSlotSubgroupMenu(event, box) {
|
||||
event.stopPropagation();
|
||||
const menu = box.closest('.custom-multi-select')?.querySelector('.slot-subgroup-menu');
|
||||
if (!menu) return;
|
||||
const isOpen = menu.classList.contains('open');
|
||||
document.querySelectorAll('.dropdown-menu').forEach(item => item.classList.remove('open'));
|
||||
document.querySelectorAll('.select-box').forEach(item => item.classList.remove('active'));
|
||||
if (isOpen) {
|
||||
box.setAttribute('aria-expanded', 'false');
|
||||
return;
|
||||
}
|
||||
menu.classList.add('open');
|
||||
box.classList.add('active');
|
||||
box.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
|
||||
function closeSlotSubgroupMenus() {
|
||||
ruleSlotsContainer.querySelectorAll('.slot-subgroup-menu').forEach(menu => menu.classList.remove('open'));
|
||||
ruleSlotsContainer.querySelectorAll('.slot-subgroup-box').forEach(box => {
|
||||
box.classList.remove('active');
|
||||
box.setAttribute('aria-expanded', 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function updateSlotSubgroupText(row) {
|
||||
if (!row) return;
|
||||
const text = row.querySelector('.slot-subgroup-box .select-text');
|
||||
if (!text) return;
|
||||
const selectedIds = readSlotSubgroupIds(row);
|
||||
if (!selectedIds.length) {
|
||||
text.textContent = 'Все выбранные группы';
|
||||
return;
|
||||
}
|
||||
if (selectedIds.length === 1) {
|
||||
const subgroup = subgroups.find(item => String(item.id) === String(selectedIds[0]));
|
||||
text.textContent = subgroup ? subgroupLabel(subgroup) : 'Выбрана 1 подгруппа';
|
||||
return;
|
||||
}
|
||||
text.textContent = `Выбрано подгрупп: ${selectedIds.length}`;
|
||||
}
|
||||
|
||||
function updateRuleGroupsText() {
|
||||
const selectedIds = selectedRuleGroupIds();
|
||||
if (!groups.length) {
|
||||
ruleGroupsText.textContent = 'Нет групп';
|
||||
return;
|
||||
}
|
||||
if (!selectedIds.length) {
|
||||
ruleGroupsText.textContent = 'Выберите группы...';
|
||||
return;
|
||||
}
|
||||
if (selectedIds.length === 1) {
|
||||
const selectedGroup = groups.find(group => String(group.id) === String(selectedIds[0]));
|
||||
ruleGroupsText.textContent = selectedGroup?.name || 'Выбрана 1 группа';
|
||||
return;
|
||||
}
|
||||
ruleGroupsText.textContent = `Выбрано групп: ${selectedIds.length}`;
|
||||
}
|
||||
|
||||
function toTimeSlotOption(slot) {
|
||||
return { value: slot.id, label: `${slot.orderNumber} пара, ${trimTime(slot.startTime)} - ${trimTime(slot.endTime)}` };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user