import { api } from '../api.js'; import { escapeHtml, showAlert, hideAlert } from '../utils.js'; async function fetchEducationForms() { try { return await api.get('/api/education-forms'); } catch (e) { console.error("Failed to fetch education forms", e); return []; } } const FILTER_EF_EMPTY_TEXT = 'Выберите форму обучения'; export async function initGroups() { const groupsTbody = document.getElementById('groups-tbody'); const createGroupForm = document.getElementById('create-group-form'); const newGroupEfSelect = document.getElementById('new-group-ef'); const newGroupDepartmentSelect = document.getElementById('new-group-department'); const newGroupSpecialitySelect = document.getElementById('new-group-speciality-code'); const newGroupProfileSelect = document.getElementById('new-group-profile'); const filterEfBox = document.getElementById('filter-ef-box'); const filterEfMenu = document.getElementById('filter-ef-menu'); const filterEfText = document.getElementById('filter-ef-text'); const filterEfCheckboxes = document.getElementById('filter-ef-checkboxes'); // Модалка группы const modalEditGroup = document.getElementById('modal-edit-group'); const modalEditGroupClose = document.getElementById('modal-edit-group-close'); const editGroupForm = document.getElementById('edit-group-form'); const editGroupEfSelect = document.getElementById('edit-group-ef'); const editGroupDepartmentSelect = document.getElementById('edit-group-department'); const editGroupSpecialitySelect = document.getElementById('edit-group-speciality-code'); const editGroupProfileSelect = document.getElementById('edit-group-profile'); // Модалка подгрупп const modalManageSubgroups = document.getElementById('modal-manage-subgroups'); const modalManageSubgroupsClose = document.getElementById('modal-manage-subgroups-close'); const subgroupsGroupContext = document.getElementById('subgroups-group-context'); const subgroupFormTitle = document.getElementById('subgroup-form-title'); const manageSubgroupForm = document.getElementById('manage-subgroup-form'); const manageSubgroupIdInput = document.getElementById('manage-subgroup-id'); const manageSubgroupGroupIdInput = document.getElementById('manage-subgroup-group-id'); const manageSubgroupCountSelect = document.getElementById('manage-subgroup-count'); const manageSubgroupCapacities = document.getElementById('manage-subgroup-capacities'); const btnSaveSubgroup = document.getElementById('btn-save-subgroup'); const btnCancelSubgroupEdit = document.getElementById('btn-cancel-subgroup-edit'); const manageSubgroupsTbody = document.getElementById('manage-subgroups-tbody'); // Модалка календарей const modalManageCalendar = document.getElementById('modal-manage-calendar'); const modalManageCalendarClose = document.getElementById('modal-manage-calendar-close'); const calendarGroupContext = document.getElementById('calendar-group-context'); const manageCalendarForm = document.getElementById('manage-calendar-form'); const manageCalendarGroupIdInput = document.getElementById('manage-calendar-group-id'); const manageCalendarYearSelect = document.getElementById('manage-calendar-year'); const manageCalendarSelect = document.getElementById('manage-calendar-id'); const manageCalendarTbody = document.getElementById('manage-calendar-tbody'); let allGroups = []; let subgroups = []; let educationForms = []; let departments = []; let specialties = []; let profilesBySpecialty = new Map(); let academicYears = []; let calendars = []; let currentAssignments = []; let activeSubgroupGroup = null; bindEvents(); await loadInitialData(); function bindEvents() { initFilterEfDropdown(); filterEfCheckboxes.addEventListener('change', handleFilterEfChange); newGroupSpecialitySelect.addEventListener('change', () => populateProfileSelect(newGroupProfileSelect, newGroupSpecialitySelect.value)); editGroupSpecialitySelect.addEventListener('change', () => populateProfileSelect(editGroupProfileSelect, editGroupSpecialitySelect.value)); createGroupForm.addEventListener('submit', createGroup); editGroupForm.addEventListener('submit', updateGroup); // Модалка подгрупп manageSubgroupForm.addEventListener('submit', saveSubgroup); manageSubgroupCountSelect.addEventListener('change', () => renderSubgroupCapacityFields(Number(manageSubgroupCountSelect.value))); btnCancelSubgroupEdit.addEventListener('click', resetSubgroupForm); manageSubgroupsTbody.addEventListener('click', handleSubgroupTableClick); // Модалка календарей manageCalendarForm.addEventListener('submit', saveAssignment); manageCalendarYearSelect.addEventListener('change', populateCalendarSelect); groupsTbody.addEventListener('click', handleGroupTableClick); // Закрытие модалок modalEditGroupClose.addEventListener('click', () => modalEditGroup.classList.remove('open')); modalEditGroup.addEventListener('click', (event) => { if (event.target === modalEditGroup) modalEditGroup.classList.remove('open'); }); modalManageSubgroupsClose.addEventListener('click', () => modalManageSubgroups.classList.remove('open')); modalManageSubgroups.addEventListener('click', (event) => { if (event.target === modalManageSubgroups) modalManageSubgroups.classList.remove('open'); }); modalManageCalendarClose.addEventListener('click', () => modalManageCalendar.classList.remove('open')); modalManageCalendar.addEventListener('click', (event) => { if (event.target === modalManageCalendar) modalManageCalendar.classList.remove('open'); }); } function initFilterEfDropdown() { if (!filterEfBox || !filterEfMenu) return; const toggleDropdown = (event) => { event.stopPropagation(); const isOpen = filterEfMenu.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) { filterEfMenu.classList.add('open'); filterEfBox.classList.add('active'); filterEfBox.setAttribute('aria-expanded', 'true'); } else { filterEfBox.setAttribute('aria-expanded', 'false'); } }; filterEfBox.addEventListener('click', toggleDropdown); filterEfBox.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggleDropdown(event); } }); filterEfMenu.addEventListener('click', (event) => { event.stopPropagation(); }); } async function loadInitialData() { try { [educationForms, departments, specialties, academicYears, calendars] = await Promise.all([ fetchEducationForms(), api.get('/api/departments'), api.get('/api/specialties'), api.get('/api/admin/calendar/years'), api.get('/api/admin/academic-calendars') ]); await loadProfiles(); populateEfSelects(educationForms); populateDepartmentSelects(departments); populateSpecialitySelects(specialties); populateYearSelect(); await loadGroups(); await loadSubgroups(); } catch (error) { groupsTbody.innerHTML = `Ошибка загрузки данных: ${escapeHtml(error.message)}`; } } async function loadProfiles() { const pairs = await Promise.all(specialties.map(async (speciality) => { const profiles = await api.get(`/api/specialties/${speciality.id}/profiles`); return [String(speciality.id), profiles]; })); profilesBySpecialty = new Map(pairs); } async function loadGroups() { try { allGroups = await api.get('/api/groups?includeArchived=true'); applyGroupFilter(); } catch (error) { groupsTbody.innerHTML = `Ошибка загрузки: ${escapeHtml(error.message)}`; } } async function loadSubgroups() { try { subgroups = await api.get('/api/subgroups'); } catch (error) { console.error("Failed to load subgroups", error); } } function applyGroupFilter() { const selectedIds = selectedFilterEducationFormIds(); const filtered = selectedIds.length ? allGroups.filter(group => selectedIds.includes(String(group.educationFormId))) : allGroups; renderGroups(filtered); } function handleFilterEfChange(event) { const changedCheckbox = event.target.closest('input[type="checkbox"]'); const allCheckbox = filterEfCheckboxes.querySelector('input[value="all"]'); if (changedCheckbox?.value === 'all' && changedCheckbox.checked) { filterEfCheckboxes.querySelectorAll('input[type="checkbox"]:not([value="all"])').forEach(checkbox => { checkbox.checked = false; }); } else if (changedCheckbox?.value !== 'all' && changedCheckbox?.checked) { allCheckbox.checked = false; } if (!selectedFilterEducationFormIds().length) { allCheckbox.checked = true; } updateFilterEfText(); applyGroupFilter(); } function selectedFilterEducationFormIds() { const allCheckbox = filterEfCheckboxes.querySelector('input[value="all"]'); if (allCheckbox?.checked) return []; return Array.from(filterEfCheckboxes.querySelectorAll('input[type="checkbox"]:checked:not([value="all"])')) .map(checkbox => String(checkbox.value)); } function updateFilterEfText() { const allCheckbox = filterEfCheckboxes.querySelector('input[value="all"]'); const selectedIds = selectedFilterEducationFormIds(); if (allCheckbox?.checked) { filterEfText.textContent = 'Все'; return; } if (!selectedIds.length) { filterEfText.textContent = FILTER_EF_EMPTY_TEXT; return; } if (selectedIds.length === 1) { const form = educationForms.find(item => String(item.id) === selectedIds[0]); filterEfText.textContent = form?.name || FILTER_EF_EMPTY_TEXT; return; } filterEfText.textContent = `Выбрано: ${selectedIds.length}`; } function renderGroups(groups) { if (!groups || !groups.length) { groupsTbody.innerHTML = 'Нет групп'; return; } groupsTbody.innerHTML = groups.map(group => ` ${group.id} ${escapeHtml(group.name)} ${escapeHtml(String(group.groupSize))} ${escapeHtml(group.educationFormName)} ${escapeHtml(departmentLabel(group.departmentId))} ${group.course || '-'} ${escapeHtml(specialityLabel(group.specialtyId || group.specialityCode))} ${escapeHtml(group.specialtyProfileName || '-')} ${escapeHtml(statusLabel(group))}
${group.status === 'ARCHIVED' ? `` : ``}
`).join(''); } function populateEfSelects(forms) { populateEfSelect(newGroupEfSelect, forms); populateEfSelect(editGroupEfSelect, forms); renderFilterEfCheckboxes(forms); } function renderFilterEfCheckboxes(forms) { const selectedIds = new Set(selectedFilterEducationFormIds()); const allChecked = !selectedIds.size; filterEfCheckboxes.innerHTML = ` ${forms.map(form => { const id = String(form.id); return ` `; }).join('')} `; updateFilterEfText(); } function populateEfSelect(select, forms) { const currentVal = select.value; select.innerHTML = forms.map(form => ``).join(''); if (currentVal && forms.find(form => form.id == currentVal)) { select.value = currentVal; } syncSelects(select); } function populateDepartmentSelects(items) { populateDepartmentSelect(newGroupDepartmentSelect, items); populateDepartmentSelect(editGroupDepartmentSelect, items); } function populateDepartmentSelect(select, items) { const currentVal = select.value; select.innerHTML = '' + items.map(department => { const name = department.departmentName || department.name || 'Без названия'; const code = department.departmentCode || department.code || department.id; return ``; }).join(''); if (currentVal && items.find(department => department.id == currentVal)) { select.value = currentVal; } syncSelects(select); } function populateSpecialitySelects(items) { populateSpecialitySelect(newGroupSpecialitySelect, items); populateSpecialitySelect(editGroupSpecialitySelect, items); populateProfileSelect(newGroupProfileSelect, newGroupSpecialitySelect.value); populateProfileSelect(editGroupProfileSelect, editGroupSpecialitySelect.value); } function populateSpecialitySelect(select, items) { const currentVal = select.value; select.innerHTML = '' + items.map(speciality => ``).join(''); if (currentVal && items.find(speciality => speciality.id == currentVal)) { select.value = currentVal; } syncSelects(select); } function populateProfileSelect(select, specialtyId, selectedProfileId = null) { const profiles = profilesBySpecialty.get(String(specialtyId)) || []; select.innerHTML = profiles.length ? '' + profiles.map(profile => `` ).join('') : ''; if (selectedProfileId && profiles.some(profile => String(profile.id) === String(selectedProfileId))) { select.value = selectedProfileId; } syncSelects(select); } function populateYearSelect() { manageCalendarYearSelect.innerHTML = '' + academicYears.map(year => ``).join(''); if (academicYears.length) { manageCalendarYearSelect.value = String(academicYears[0].id); } syncSelects(manageCalendarYearSelect); } function populateCalendarSelect() { const groupId = manageCalendarGroupIdInput.value; const group = allGroups.find(g => g.id == groupId); const yearId = manageCalendarYearSelect.value; const matching = group && yearId ? calendars.filter(calendar => String(calendar.academicYearId) === String(yearId) && String(calendar.specialtyId) === String(group.specialtyId || group.specialityCode) && String(calendar.specialtyProfileId) === String(group.specialtyProfileId) && String(calendar.studyFormId) === String(group.educationFormId) ) : []; manageCalendarSelect.innerHTML = matching.length ? '' + matching.map(calendar => `` ).join('') : ''; syncSelects(manageCalendarSelect); } async function createGroup(event) { event.preventDefault(); hideAlert('create-group-alert'); const payload = readGroupPayload('new'); if (!payload) return; try { const data = await api.post('/api/groups', payload); showAlert('create-group-alert', `Группа "${escapeHtml(data.name || payload.name)}" создана`, 'success'); createGroupForm.reset(); syncSelects(newGroupEfSelect, newGroupDepartmentSelect, newGroupSpecialitySelect, newGroupProfileSelect); await loadGroups(); } catch (error) { showAlert('create-group-alert', error.message || 'Ошибка создания', 'error'); } } async function updateGroup(event) { event.preventDefault(); hideAlert('edit-group-alert'); const id = document.getElementById('edit-group-id').value; const payload = readGroupPayload('edit'); if (!payload) return; try { const data = await api.put('/api/groups/' + id, payload); modalEditGroup.classList.remove('open'); showAlert('create-group-alert', `Группа "${escapeHtml(data.name || payload.name)}" обновлена`, 'success'); await loadGroups(); } catch (error) { showAlert('edit-group-alert', error.message || 'Ошибка обновления', 'error'); } } function readGroupPayload(prefix) { const isEdit = prefix === 'edit'; const alertId = isEdit ? 'edit-group-alert' : 'create-group-alert'; const name = document.getElementById(`${prefix}-group-name`).value.trim(); const groupSize = document.getElementById(`${prefix}-group-size`).value; const educationFormId = (isEdit ? editGroupEfSelect : newGroupEfSelect).value; const departmentId = (isEdit ? editGroupDepartmentSelect : newGroupDepartmentSelect).value; const yearStartStudy = document.getElementById(`${prefix}-group-yearStartStudy`).value; const specialtyId = (isEdit ? editGroupSpecialitySelect : newGroupSpecialitySelect).value; const specialtyProfileId = (isEdit ? editGroupProfileSelect : newGroupProfileSelect).value; if (!name) { showAlert(alertId, 'Введите название группы', 'error'); return null; } if (!groupSize) { showAlert(alertId, 'Введите размер группы', 'error'); return null; } if (!educationFormId) { showAlert(alertId, 'Выберите форму обучения', 'error'); return null; } if (!departmentId) { showAlert(alertId, 'Выберите кафедру', 'error'); return null; } if (!yearStartStudy) { showAlert(alertId, 'Введите год начала обучения', 'error'); return null; } if (!specialtyId) { showAlert(alertId, 'Выберите специальность', 'error'); return null; } if (!specialtyProfileId) { showAlert(alertId, 'Выберите профиль обучения', 'error'); return null; } return { name, groupSize: Number(groupSize), educationFormId: Number(educationFormId), departmentId: Number(departmentId), yearStartStudy: Number(yearStartStudy), specialtyId: Number(specialtyId), specialtyProfileId: Number(specialtyProfileId) }; } async function handleGroupTableClick(event) { const btnDelete = event.target.closest('.btn-archive-group'); const btnEdit = event.target.closest('.btn-edit-group'); const btnRestore = event.target.closest('.btn-restore-group'); const btnSubgroups = event.target.closest('.btn-manage-subgroups'); const btnCalendar = event.target.closest('.btn-manage-calendar'); if (btnDelete) { if (!confirm('Архивировать группу? История расписания сохранится.')) return; try { await api.delete('/api/groups/' + btnDelete.dataset.id); await loadGroups(); } catch (error) { alert(error.message || 'Ошибка архивирования'); } } if (btnRestore) { try { await api.post('/api/groups/' + btnRestore.dataset.id + '/restore', {}); await loadGroups(); } catch (error) { alert(error.message || 'Ошибка восстановления'); } } if (btnEdit) { openEditGroupModal(btnEdit.dataset.id); } if (btnSubgroups) { const groupId = btnSubgroups.dataset.id; const group = allGroups.find(g => g.id == groupId); if (group) openSubgroupsModal(group); } if (btnCalendar) { const groupId = btnCalendar.dataset.id; const group = allGroups.find(g => g.id == groupId); if (group) openCalendarModal(group); } } function openEditGroupModal(id) { const group = allGroups.find(item => item.id == id); if (!group) { alert('Группа не найдена'); return; } document.getElementById('edit-group-id').value = group.id; document.getElementById('edit-group-name').value = group.name || ''; document.getElementById('edit-group-size').value = group.groupSize || ''; editGroupEfSelect.value = group.educationFormId || ''; editGroupDepartmentSelect.value = group.departmentId || ''; document.getElementById('edit-group-yearStartStudy').value = group.yearStartStudy || ''; editGroupSpecialitySelect.value = group.specialtyId || group.specialityCode || ''; populateProfileSelect(editGroupProfileSelect, editGroupSpecialitySelect.value, group.specialtyProfileId); syncSelects(editGroupEfSelect, editGroupDepartmentSelect, editGroupSpecialitySelect, editGroupProfileSelect); hideAlert('edit-group-alert'); modalEditGroup.classList.add('open'); } // --- Управление подгруппами --- function openSubgroupsModal(group) { activeSubgroupGroup = group; subgroupsGroupContext.textContent = `Группа: ${group.name} (${group.groupSize} чел.)`; manageSubgroupGroupIdInput.value = group.id; resetSubgroupForm(); modalManageSubgroups.classList.add('open'); renderSubgroupsList(group.id); } function renderSubgroupsList(groupId) { const list = subgroups.filter(sub => String(sub.groupId) === String(groupId)); if (!list.length) { manageSubgroupsTbody.innerHTML = 'Подгруппы не созданы'; return; } const canDeleteSingleSubgroup = list.length === 1; const deleteTitle = canDeleteSingleSubgroup ? 'Удалить подгруппу' : 'Чтобы изменить количество подгрупп, выберите нужный режим выше и нажмите «Применить»'; manageSubgroupsTbody.innerHTML = list.map(sub => ` ${escapeHtml(sub.name)} ${escapeHtml(String(sub.studentCapacity ?? '-'))} чел. `).join(''); } function resetSubgroupForm() { manageSubgroupIdInput.value = ''; const current = currentGroupSubgroups(manageSubgroupGroupIdInput.value); const count = [2, 3].includes(current.length) ? current.length : 0; manageSubgroupCountSelect.value = String(count); syncSelects(manageSubgroupCountSelect); renderSubgroupCapacityFields(count, current.map(item => item.studentCapacity)); subgroupFormTitle.textContent = 'Настройка подгрупп'; btnSaveSubgroup.textContent = 'Применить'; btnCancelSubgroupEdit.style.display = 'none'; hideAlert('manage-subgroup-alert'); } function currentGroupSubgroups(groupId) { return subgroups .filter(sub => String(sub.groupId) === String(groupId)) .sort((left, right) => naturalSubgroupOrder(left.name).localeCompare(naturalSubgroupOrder(right.name), 'ru')); } function naturalSubgroupOrder(name) { const number = String(name || '').match(/\d+/)?.[0]; return number ? String(number).padStart(3, '0') : String(name || ''); } function renderSubgroupCapacityFields(count, capacities = []) { manageSubgroupCapacities.className = `subgroup-capacity-fields subgroup-capacity-fields-${count}`; if (!count) { manageSubgroupCapacities.innerHTML = ''; return; } const groupSize = Number(activeSubgroupGroup?.groupSize || 0); const suggested = suggestedSubgroupCapacities(groupSize, count); manageSubgroupCapacities.innerHTML = Array.from({ length: count }, (_, index) => { const value = capacities[index] ?? suggested[index] ?? ''; return `
`; }).join(''); } function suggestedSubgroupCapacities(groupSize, count) { if (!groupSize || !count) return []; const base = Math.floor(groupSize / count); const remainder = groupSize % count; return Array.from({ length: count }, (_, index) => base + (index < remainder ? 1 : 0)); } async function saveSubgroup(event) { event.preventDefault(); hideAlert('manage-subgroup-alert'); const groupId = manageSubgroupGroupIdInput.value; const group = activeSubgroupGroup || allGroups.find(item => String(item.id) === String(groupId)); const count = Number(manageSubgroupCountSelect.value); const existingSubgroups = currentGroupSubgroups(groupId); if (!group) { showAlert('manage-subgroup-alert', 'Группа не найдена', 'error'); return; } if (count === 0) { await applyNoSubgroups(groupId, existingSubgroups); return; } const capacities = Array.from(manageSubgroupCapacities.querySelectorAll('.manage-subgroup-capacity')) .map(input => Number(input.value)); const validationError = validateSubgroupCapacities(capacities, Number(group.groupSize), count); if (validationError) { showAlert('manage-subgroup-alert', validationError, 'error'); return; } try { await syncSubgroups(groupId, existingSubgroups, capacities); showAlert('manage-subgroup-alert', 'Подгруппы сохранены', 'success'); await loadSubgroups(); resetSubgroupForm(); renderSubgroupsList(groupId); } catch (error) { showAlert('manage-subgroup-alert', error.message || 'Ошибка сохранения подгруппы', 'error'); } } function validateSubgroupCapacities(capacities, groupSize, count) { if (capacities.length !== count || capacities.some(value => !Number.isInteger(value) || value <= 0)) { return 'Укажите положительную численность для каждой подгруппы'; } const total = capacities.reduce((sum, value) => sum + value, 0); if (total !== groupSize) { return `Сумма численностей подгрупп должна быть равна численности группы (${groupSize} чел.)`; } return null; } async function applyNoSubgroups(groupId, existingSubgroups) { try { for (const subgroup of existingSubgroups) { await api.delete(`/api/groups/${groupId}/subgroups/${subgroup.id}`); } showAlert('manage-subgroup-alert', 'Деление на подгруппы отключено', 'success'); await loadSubgroups(); resetSubgroupForm(); renderSubgroupsList(groupId); } catch (error) { showAlert('manage-subgroup-alert', error.message || 'Ошибка отключения подгрупп', 'error'); } } async function syncSubgroups(groupId, existingSubgroups, capacities) { const targetCount = capacities.length; const extraSubgroups = existingSubgroups.slice(targetCount); for (const subgroup of extraSubgroups) { await api.delete(`/api/groups/${groupId}/subgroups/${subgroup.id}`); } const updates = []; const creates = []; for (let index = 0; index < targetCount; index += 1) { const payload = { name: `Подгруппа ${index + 1}`, studentCapacity: capacities[index] }; const existing = existingSubgroups[index]; if (existing) { updates.push({ existing, payload }); } else { creates.push(payload); } } updates.sort((left, right) => { const leftIncreases = left.payload.studentCapacity > Number(left.existing.studentCapacity || 0); const rightIncreases = right.payload.studentCapacity > Number(right.existing.studentCapacity || 0); return Number(leftIncreases) - Number(rightIncreases); }); for (const update of updates) { await api.put(`/api/groups/${groupId}/subgroups/${update.existing.id}`, update.payload); } for (const payload of creates) { await api.post(`/api/groups/${groupId}/subgroups`, payload); } } async function handleSubgroupTableClick(event) { const editButton = event.target.closest('.btn-edit-subgroup'); const deleteButton = event.target.closest('.btn-delete-subgroup'); const groupId = manageSubgroupGroupIdInput.value; if (editButton) { const subgroup = subgroups.find(item => item.id == editButton.dataset.id); if (!subgroup) return; resetSubgroupForm(); const inputIndex = currentGroupSubgroups(groupId).findIndex(item => item.id === subgroup.id); const input = manageSubgroupCapacities.querySelectorAll('.manage-subgroup-capacity')[inputIndex]; input?.focus(); hideAlert('manage-subgroup-alert'); return; } if (deleteButton) { if (deleteButton.disabled) { showAlert('manage-subgroup-alert', 'Чтобы изменить количество подгрупп, выберите нужный режим выше и нажмите «Применить»', 'error'); return; } if (!confirm('Удалить подгруппу?')) return; try { await api.delete(`/api/groups/${groupId}/subgroups/${deleteButton.dataset.id}`); showAlert('manage-subgroup-alert', 'Подгруппа удалена', 'success'); resetSubgroupForm(); await loadSubgroups(); renderSubgroupsList(groupId); } catch (error) { showAlert('manage-subgroup-alert', error.message || 'Ошибка удаления подгруппы', 'error'); } } } // --- Управление календарными графиками --- async function openCalendarModal(group) { calendarGroupContext.textContent = `Группа: ${group.name} (${specialityLabel(group.specialtyId || group.specialityCode)})`; manageCalendarGroupIdInput.value = group.id; hideAlert('manage-calendar-alert'); populateCalendarSelect(); modalManageCalendar.classList.add('open'); await loadAssignmentsList(group.id); } async function loadAssignmentsList(groupId) { manageCalendarTbody.innerHTML = 'Загрузка...'; try { currentAssignments = await api.get(`/api/groups/${groupId}/calendar-assignments`); renderAssignmentsList(); } catch (error) { manageCalendarTbody.innerHTML = `Ошибка: ${escapeHtml(error.message)}`; } } function renderAssignmentsList() { if (!currentAssignments.length) { manageCalendarTbody.innerHTML = 'Календарные графики не назначены'; return; } manageCalendarTbody.innerHTML = currentAssignments.map(assignment => ` ${escapeHtml(assignment.academicYearTitle)} ${escapeHtml(assignment.calendarTitle)} ${renderAssignmentSubjects(assignment.subjects || [])} `).join(''); } function renderAssignmentSubjects(subjects) { if (!subjects.length) { return 'Не привязаны'; } const grouped = subjects.reduce((acc, subject) => { const key = String(subject.semesterNumber || '-'); if (!acc.has(key)) acc.set(key, []); acc.get(key).push(subject); return acc; }, new Map()); return `
${Array.from(grouped.entries()) .sort(([a], [b]) => Number(a) - Number(b)) .map(([semesterNumber, items]) => `
${escapeHtml(semesterNumber)} семестр ${items.map(subject => escapeHtml(subjectLabel(subject))).join(', ')}
`).join('')}
`; } async function saveAssignment(event) { event.preventDefault(); hideAlert('manage-calendar-alert'); const groupId = manageCalendarGroupIdInput.value; const academicYearId = manageCalendarYearSelect.value; const calendarId = manageCalendarSelect.value; if (!academicYearId || !calendarId) { showAlert('manage-calendar-alert', 'Выберите учебный год и график', 'error'); return; } try { await api.put(`/api/groups/${groupId}/calendar-assignments`, { academicYearId: Number(academicYearId), calendarId: Number(calendarId) }); showAlert('manage-calendar-alert', 'Календарный график назначен', 'success'); await loadAssignmentsList(groupId); } catch (error) { showAlert('manage-calendar-alert', error.message || 'Ошибка назначения графика', 'error'); } } manageCalendarTbody.addEventListener('click', async (event) => { const deleteButton = event.target.closest('.btn-delete-assignment'); if (!deleteButton) return; if (!confirm('Удалить назначение графика?')) return; const groupId = manageCalendarGroupIdInput.value; try { await api.delete(`/api/groups/${groupId}/calendar-assignments/${deleteButton.dataset.id}`); showAlert('manage-calendar-alert', 'Назначение удалено', 'success'); await loadAssignmentsList(groupId); } catch (error) { showAlert('manage-calendar-alert', error.message || 'Ошибка удаления назначения', 'error'); } }); // --- Вспомогательные функции --- function departmentLabel(departmentId) { const department = departments.find(item => item.id == departmentId); if (!department) return departmentId || '-'; return department.departmentName || department.name || departmentId; } function specialityLabel(specialityId) { const speciality = specialties.find(item => item.id == specialityId); if (!speciality) return specialityId || '-'; const code = speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code || speciality.id; const name = speciality.specialityName || speciality.name || ''; return name ? `${code} — ${name}` : code; } function subjectLabel(subject) { const code = subject.subjectCode ? `${subject.subjectCode} · ` : ''; return `${code}${subject.subjectName || subject.subjectId || 'Без названия'}`; } function statusLabel(group) { if (group.status === 'ARCHIVED') return 'Архив'; return group.studyStateName || (group.active ? 'Активна' : 'Неактивна'); } function statusBadgeClass(group) { if (group.status === 'ARCHIVED' || group.studyState === 'GRADUATED' || group.studyState === 'INACTIVE') { return 'badge-unavailable'; } if (group.studyState === 'NOT_STARTED') { return 'badge-ef'; } return 'badge-available'; } function syncSelects(...selects) { selects.filter(Boolean).forEach(select => { select.dispatchEvent(new Event('change', { bubbles: true })); }); } }