import { api } from '../api.js'; import { escapeHtml, showAlert, hideAlert } from '../utils.js'; import { fetchEducationForms } from './edu-forms.js'; 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 calendarForm = document.getElementById('group-calendar-form'); const calendarGroupSelect = document.getElementById('calendar-group'); const calendarYearSelect = document.getElementById('calendar-year'); const calendarSelect = document.getElementById('calendar-id'); const calendarAssignmentsTbody = document.getElementById('group-calendar-tbody'); const subgroupForm = document.getElementById('subgroup-form'); const subgroupIdInput = document.getElementById('subgroup-id'); const subgroupGroupSelect = document.getElementById('subgroup-group'); const subgroupNameInput = document.getElementById('subgroup-name'); const subgroupCapacityInput = document.getElementById('subgroup-capacity'); const subgroupSubmitButton = document.getElementById('subgroup-submit'); const subgroupResetButton = document.getElementById('subgroup-reset'); const subgroupsTbody = document.getElementById('subgroups-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)); calendarGroupSelect.addEventListener('change', () => { populateCalendarSelect(); loadAssignments(); }); calendarYearSelect.addEventListener('change', populateCalendarSelect); createGroupForm.addEventListener('submit', createGroup); editGroupForm.addEventListener('submit', updateGroup); calendarForm.addEventListener('submit', saveAssignment); subgroupForm.addEventListener('submit', saveSubgroup); subgroupGroupSelect.addEventListener('change', renderSubgroups); subgroupResetButton.addEventListener('click', resetSubgroupForm); groupsTbody.addEventListener('click', handleGroupTableClick); subgroupsTbody.addEventListener('click', handleSubgroupTableClick); calendarAssignmentsTbody.addEventListener('click', handleAssignmentTableClick); modalEditGroupClose.addEventListener('click', () => modalEditGroup.classList.remove('open')); modalEditGroup.addEventListener('click', (event) => { if (event.target === modalEditGroup) { modalEditGroup.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(); populateCalendarSelect(); await loadGroups(); await loadSubgroups(); } catch (error) { groupsTbody.innerHTML = `Ошибка загрузки данных: ${escapeHtml(error.message)}`; subgroupsTbody.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'); applyGroupFilter(); populateGroupSelect(); populateSubgroupGroupSelect(); await loadAssignments(); } catch (error) { groupsTbody.innerHTML = `Ошибка загрузки: ${escapeHtml(error.message)}`; } } async function loadSubgroups() { try { subgroups = await api.get('/api/subgroups'); renderSubgroups(); } catch (error) { subgroupsTbody.innerHTML = `Ошибка загрузки: ${escapeHtml(error.message)}`; } } 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 || '-')} `).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() { calendarYearSelect.innerHTML = '' + academicYears.map(year => ``).join(''); if (!calendarYearSelect.value && academicYears.length) { calendarYearSelect.value = String(academicYears[0].id); } syncSelects(calendarYearSelect); } function populateGroupSelect() { const current = calendarGroupSelect.value; calendarGroupSelect.innerHTML = '' + allGroups.map(group => ``).join(''); if (current && allGroups.some(group => String(group.id) === String(current))) { calendarGroupSelect.value = current; } else if (allGroups.length) { calendarGroupSelect.value = String(allGroups[0].id); } syncSelects(calendarGroupSelect); } function populateSubgroupGroupSelect() { const current = subgroupGroupSelect.value; subgroupGroupSelect.innerHTML = '' + allGroups.map(group => ``).join(''); if (current && allGroups.some(group => String(group.id) === String(current))) { subgroupGroupSelect.value = current; } else if (allGroups.length) { subgroupGroupSelect.value = String(allGroups[0].id); } syncSelects(subgroupGroupSelect); } function populateCalendarSelect() { const group = selectedCalendarGroup(); const yearId = calendarYearSelect.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) ) : []; calendarSelect.innerHTML = matching.length ? '' + matching.map(calendar => `` ).join('') : ''; syncSelects(calendarSelect); } 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(); await loadSubgroups(); } 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(); await loadSubgroups(); } 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'); if (btnDelete) { if (!confirm('Удалить группу?')) return; try { await api.delete('/api/groups/' + btnDelete.dataset.id); await loadGroups(); await loadSubgroups(); } catch (error) { alert(error.message || 'Ошибка удаления'); } } if (btnEdit) { openEditGroupModal(btnEdit.dataset.id); } } 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'); } async function saveSubgroup(event) { event.preventDefault(); hideAlert('subgroup-alert'); const groupId = subgroupGroupSelect.value; const name = subgroupNameInput.value.trim(); const capacity = subgroupCapacityInput.value; const subgroupId = subgroupIdInput.value; if (!groupId) { showAlert('subgroup-alert', 'Выберите группу', 'error'); return; } if (!name) { showAlert('subgroup-alert', 'Введите название подгруппы', 'error'); return; } if (capacity && Number(capacity) < 0) { showAlert('subgroup-alert', 'Численность не может быть отрицательной', 'error'); return; } const payload = { name, studentCapacity: capacity ? Number(capacity) : null }; try { const endpoint = `/api/groups/${groupId}/subgroups${subgroupId ? `/${subgroupId}` : ''}`; const saved = subgroupId ? await api.put(endpoint, payload) : await api.post(endpoint, payload); showAlert('subgroup-alert', `Подгруппа "${escapeHtml(saved.name)}" сохранена`, 'success'); resetSubgroupForm(); await loadSubgroups(); } catch (error) { showAlert('subgroup-alert', error.message || 'Ошибка сохранения подгруппы', 'error'); } } function renderSubgroups() { const groupId = subgroupGroupSelect.value; const visibleSubgroups = groupId ? subgroups.filter(subgroup => String(subgroup.groupId) === String(groupId)) : subgroups; if (!visibleSubgroups.length) { subgroupsTbody.innerHTML = 'Подгруппы не созданы'; return; } subgroupsTbody.innerHTML = visibleSubgroups.map(subgroup => ` ${escapeHtml(subgroup.groupName || groupName(subgroup.groupId))} ${escapeHtml(subgroup.name)} ${escapeHtml(String(subgroup.studentCapacity ?? '-'))} `).join(''); } async function handleSubgroupTableClick(event) { const editButton = event.target.closest('.btn-edit-subgroup'); const deleteButton = event.target.closest('.btn-delete-subgroup'); if (editButton) { const subgroup = subgroups.find(item => String(item.id) === String(editButton.dataset.id)); if (!subgroup) return; subgroupIdInput.value = subgroup.id; subgroupGroupSelect.value = subgroup.groupId; subgroupGroupSelect.disabled = true; subgroupNameInput.value = subgroup.name || ''; subgroupCapacityInput.value = subgroup.studentCapacity ?? ''; subgroupSubmitButton.textContent = 'Сохранить подгруппу'; syncSelects(subgroupGroupSelect); hideAlert('subgroup-alert'); return; } if (deleteButton) { const subgroup = subgroups.find(item => String(item.id) === String(deleteButton.dataset.id)); if (!subgroup || !confirm('Удалить подгруппу?')) return; try { await api.delete(`/api/groups/${subgroup.groupId}/subgroups/${subgroup.id}`); showAlert('subgroup-alert', 'Подгруппа удалена', 'success'); resetSubgroupForm(); await loadSubgroups(); } catch (error) { showAlert('subgroup-alert', error.message || 'Ошибка удаления подгруппы', 'error'); } } } function resetSubgroupForm() { subgroupIdInput.value = ''; subgroupNameInput.value = ''; subgroupCapacityInput.value = ''; subgroupGroupSelect.disabled = false; subgroupSubmitButton.textContent = 'Создать подгруппу'; syncSelects(subgroupGroupSelect); hideAlert('subgroup-alert'); } async function saveAssignment(event) { event.preventDefault(); hideAlert('group-calendar-alert'); const groupId = calendarGroupSelect.value; const academicYearId = calendarYearSelect.value; const calendarId = calendarSelect.value; if (!groupId || !academicYearId || !calendarId) { showAlert('group-calendar-alert', 'Выберите группу, учебный год и график', 'error'); return; } try { await api.put(`/api/groups/${groupId}/calendar-assignments`, { academicYearId: Number(academicYearId), calendarId: Number(calendarId) }); showAlert('group-calendar-alert', 'Календарный график назначен', 'success'); await loadAssignments(); } catch (error) { showAlert('group-calendar-alert', error.message || 'Ошибка назначения графика', 'error'); } } async function loadAssignments() { const groupId = calendarGroupSelect.value; if (!groupId) { currentAssignments = []; calendarAssignmentsTbody.innerHTML = 'Выберите группу'; return; } try { currentAssignments = await api.get(`/api/groups/${groupId}/calendar-assignments`); renderAssignments(); } catch (error) { calendarAssignmentsTbody.innerHTML = `Ошибка загрузки: ${escapeHtml(error.message)}`; } } function renderAssignments() { if (!currentAssignments.length) { calendarAssignmentsTbody.innerHTML = 'Графики не назначены'; return; } calendarAssignmentsTbody.innerHTML = currentAssignments.map(assignment => ` ${escapeHtml(assignment.academicYearTitle)} ${escapeHtml(assignment.calendarTitle)} `).join(''); } async function handleAssignmentTableClick(event) { const deleteButton = event.target.closest('.btn-delete-assignment'); if (!deleteButton) return; if (!confirm('Удалить назначение графика?')) return; try { await api.delete(`/api/groups/${calendarGroupSelect.value}/calendar-assignments/${deleteButton.dataset.id}`); showAlert('group-calendar-alert', 'Назначение удалено', 'success'); await loadAssignments(); } catch (error) { showAlert('group-calendar-alert', error.message || 'Ошибка удаления назначения', 'error'); } } function selectedCalendarGroup() { return allGroups.find(group => String(group.id) === String(calendarGroupSelect.value)); } function departmentLabel(departmentId) { const department = departments.find(item => item.id == departmentId); if (!department) return departmentId || '-'; return department.departmentName || department.name || departmentId; } function groupName(groupId) { const group = allGroups.find(item => String(item.id) === String(groupId)); return group ? group.name : groupId || '-'; } 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 syncSelects(...selects) { selects.filter(Boolean).forEach(select => { select.dispatchEvent(new Event('change', { bubbles: true })); }); } }