435 lines
20 KiB
JavaScript
435 lines
20 KiB
JavaScript
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');
|
||
|
||
let allGroups = [];
|
||
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);
|
||
groupsTbody.addEventListener('click', handleGroupTableClick);
|
||
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();
|
||
} catch (error) {
|
||
groupsTbody.innerHTML = `<tr><td colspan="9" class="loading-row">Ошибка загрузки данных: ${escapeHtml(error.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
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();
|
||
await loadAssignments();
|
||
} catch (error) {
|
||
groupsTbody.innerHTML = `<tr><td colspan="9" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
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 = '<tr><td colspan="9" class="loading-row">Нет групп</td></tr>';
|
||
return;
|
||
}
|
||
groupsTbody.innerHTML = groups.map(group => `
|
||
<tr>
|
||
<td>${group.id}</td>
|
||
<td>${escapeHtml(group.name)}</td>
|
||
<td>${escapeHtml(String(group.groupSize))}</td>
|
||
<td><span class="badge badge-ef">${escapeHtml(group.educationFormName)}</span></td>
|
||
<td>${escapeHtml(departmentLabel(group.departmentId))}</td>
|
||
<td>${group.course || '-'}</td>
|
||
<td>${escapeHtml(specialityLabel(group.specialtyId || group.specialityCode))}</td>
|
||
<td>${escapeHtml(group.specialtyProfileName || '-')}</td>
|
||
<td style="text-align: right;">
|
||
<button class="btn-edit-classroom btn-edit-group" data-id="${group.id}">Изменить</button>
|
||
<button class="btn-delete" data-id="${group.id}" style="margin-left: 0.5rem;">Удалить</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function populateEfSelects(forms) {
|
||
populateEfSelect(newGroupEfSelect, forms);
|
||
populateEfSelect(editGroupEfSelect, forms);
|
||
const currentFilter = filterEfSelect.value;
|
||
filterEfSelect.innerHTML = '<option value="">Все формы</option>' +
|
||
forms.map(form => `<option value="${form.id}">${escapeHtml(form.name)}</option>`).join('');
|
||
if (currentFilter) filterEfSelect.value = currentFilter;
|
||
syncSelects(filterEfSelect);
|
||
}
|
||
|
||
function populateEfSelect(select, forms) {
|
||
const currentVal = select.value;
|
||
select.innerHTML = forms.map(form => `<option value="${form.id}">${escapeHtml(form.name)}</option>`).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 = '<option value="">Выберите кафедру</option>' +
|
||
items.map(department => {
|
||
const name = department.departmentName || department.name || 'Без названия';
|
||
const code = department.departmentCode || department.code || department.id;
|
||
return `<option value="${department.id}">${escapeHtml(name)} (${escapeHtml(String(code))})</option>`;
|
||
}).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 = '<option value="">Выберите специальность</option>' +
|
||
items.map(speciality => `<option value="${speciality.id}">${escapeHtml(specialityLabel(speciality.id))}</option>`).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
|
||
? '<option value="">Выберите профиль</option>' + profiles.map(profile =>
|
||
`<option value="${profile.id}">${escapeHtml(profile.name)}</option>`
|
||
).join('')
|
||
: '<option value="">Нет профилей</option>';
|
||
if (selectedProfileId && profiles.some(profile => String(profile.id) === String(selectedProfileId))) {
|
||
select.value = selectedProfileId;
|
||
}
|
||
syncSelects(select);
|
||
}
|
||
|
||
function populateYearSelect() {
|
||
calendarYearSelect.innerHTML = '<option value="">Выберите учебный год</option>' +
|
||
academicYears.map(year => `<option value="${year.id}">${escapeHtml(year.title)}</option>`).join('');
|
||
if (!calendarYearSelect.value && academicYears.length) {
|
||
calendarYearSelect.value = String(academicYears[0].id);
|
||
}
|
||
syncSelects(calendarYearSelect);
|
||
}
|
||
|
||
function populateGroupSelect() {
|
||
const current = calendarGroupSelect.value;
|
||
calendarGroupSelect.innerHTML = '<option value="">Выберите группу</option>' +
|
||
allGroups.map(group => `<option value="${group.id}">${escapeHtml(group.name)}</option>`).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 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)
|
||
)
|
||
: [];
|
||
calendarSelect.innerHTML = matching.length
|
||
? '<option value="">Выберите график</option>' + matching.map(calendar =>
|
||
`<option value="${calendar.id}">${escapeHtml(calendar.title)}</option>`
|
||
).join('')
|
||
: '<option value="">Нет подходящих графиков</option>';
|
||
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();
|
||
} 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');
|
||
|
||
if (btnDelete) {
|
||
if (!confirm('Удалить группу?')) return;
|
||
try {
|
||
await api.delete('/api/groups/' + btnDelete.dataset.id);
|
||
await loadGroups();
|
||
} 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 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 = '<tr><td colspan="3" class="loading-row">Выберите группу</td></tr>';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
currentAssignments = await api.get(`/api/groups/${groupId}/calendar-assignments`);
|
||
renderAssignments();
|
||
} catch (error) {
|
||
calendarAssignmentsTbody.innerHTML = `<tr><td colspan="3" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
function renderAssignments() {
|
||
if (!currentAssignments.length) {
|
||
calendarAssignmentsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Графики не назначены</td></tr>';
|
||
return;
|
||
}
|
||
calendarAssignmentsTbody.innerHTML = currentAssignments.map(assignment => `
|
||
<tr>
|
||
<td>${escapeHtml(assignment.academicYearTitle)}</td>
|
||
<td>${escapeHtml(assignment.calendarTitle)}</td>
|
||
<td>
|
||
<button class="btn-delete btn-delete-assignment" data-id="${assignment.id}">Удалить</button>
|
||
</td>
|
||
</tr>
|
||
`).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 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 }));
|
||
});
|
||
}
|
||
}
|