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 []; } } 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 filterEfSelect = document.getElementById('filter-ef'); // Модалка группы 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 manageSubgroupNameInput = document.getElementById('manage-subgroup-name'); const manageSubgroupCapacityInput = document.getElementById('manage-subgroup-capacity'); 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 = []; bindEvents(); await loadInitialData(); function bindEvents() { filterEfSelect.addEventListener('change', applyGroupFilter); 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); 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'); }); } 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 filterId = filterEfSelect.value; const filtered = filterId ? allGroups.filter(group => group.educationFormId == filterId) : allGroups; renderGroups(filtered); } 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); const currentFilter = filterEfSelect.value; filterEfSelect.innerHTML = '' + forms.map(form => ``).join(''); if (currentFilter) filterEfSelect.value = currentFilter; syncSelects(filterEfSelect); } 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-delete'); 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) { 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; } manageSubgroupsTbody.innerHTML = list.map(sub => ` ${escapeHtml(sub.name)} ${escapeHtml(String(sub.studentCapacity ?? '-'))} чел. `).join(''); } function resetSubgroupForm() { manageSubgroupIdInput.value = ''; manageSubgroupNameInput.value = ''; manageSubgroupCapacityInput.value = ''; subgroupFormTitle.textContent = 'Создание подгруппы'; btnSaveSubgroup.textContent = 'Создать'; btnCancelSubgroupEdit.style.display = 'none'; hideAlert('manage-subgroup-alert'); } async function saveSubgroup(event) { event.preventDefault(); hideAlert('manage-subgroup-alert'); const groupId = manageSubgroupGroupIdInput.value; const subgroupId = manageSubgroupIdInput.value; const name = manageSubgroupNameInput.value.trim(); const capacity = manageSubgroupCapacityInput.value; if (!name || !capacity) { showAlert('manage-subgroup-alert', 'Заполните все поля', 'error'); return; } const payload = { name, studentCapacity: Number(capacity) }; try { const endpoint = `/api/groups/${groupId}/subgroups${subgroupId ? `/${subgroupId}` : ''}`; const saved = subgroupId ? await api.put(endpoint, payload) : await api.post(endpoint, payload); showAlert('manage-subgroup-alert', `Подгруппа "${escapeHtml(saved.name)}" сохранена`, 'success'); resetSubgroupForm(); await loadSubgroups(); renderSubgroupsList(groupId); } catch (error) { showAlert('manage-subgroup-alert', error.message || 'Ошибка сохранения подгруппы', 'error'); } } 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; manageSubgroupIdInput.value = subgroup.id; manageSubgroupNameInput.value = subgroup.name || ''; manageSubgroupCapacityInput.value = subgroup.studentCapacity ?? ''; subgroupFormTitle.textContent = 'Редактирование подгруппы'; btnSaveSubgroup.textContent = 'Сохранить'; btnCancelSubgroupEdit.style.display = 'inline-block'; hideAlert('manage-subgroup-alert'); return; } if (deleteButton) { 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)} `).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 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 })); }); } }