import { api } from '../api.js'; import { escapeHtml, showAlert, hideAlert } from '../utils.js'; import { confirmAction } from '../dialog.js'; import { renderPagination } from '../pagination.js'; import { debounce, readUrlState, writeUrlState } from '../url-state.js'; import { markTableReady, renderTableEmpty, renderTableError, renderTableLoading } from '../view-state.js'; import { trackUxEvent } from '../../../telemetry.js'; import { AsyncCombobox } from '../async-combobox.js'; export async function initSubjects() { const subjectsTbody = document.getElementById('subjects-tbody'); const createSubjectForm = document.getElementById('create-subject-form'); const newSubjectDeptSelect = document.getElementById('new-subject-department'); const departmentFilter = document.getElementById('subjects-department-filter'); const searchInput = document.getElementById('subjects-search'); const sortSelect = document.getElementById('subjects-sort'); const pagination = document.getElementById('subjects-pagination'); // Модалка управления преподавателями const modalManageTeachers = document.getElementById('modal-manage-teachers'); const modalManageTeachersClose = document.getElementById('modal-manage-teachers-close'); const subjectTeachersContext = document.getElementById('subject-teachers-context'); const manageTeacherSubjectForm = document.getElementById('manage-teacher-subject-form'); const manageTsSubjectIdInput = document.getElementById('manage-ts-subject-id'); const manageTsTeacherSelect = document.getElementById('manage-ts-teacher-select'); const manageTsTbody = document.getElementById('manage-ts-tbody'); let allSubjects = []; let allDepartments = []; let teacherSubjects = []; let teacherCombobox = null; const restored = readUrlState({ subjectQuery: '', subjectDepartment: '', subjectPage: '0', subjectSize: '25', subjectSort: 'name,asc' }); const pageState = { query: restored.subjectQuery, departmentId: restored.subjectDepartment, page: Math.max(Number(restored.subjectPage) || 0, 0), size: [25, 50, 100].includes(Number(restored.subjectSize)) ? Number(restored.subjectSize) : 25, sort: restored.subjectSort }; searchInput.value = pageState.query; sortSelect.value = pageState.sort; bindEvents(); await loadInitialData(); function bindEvents() { createSubjectForm.addEventListener('submit', createSubject); subjectsTbody.addEventListener('click', handleSubjectsTableClick); manageTeacherSubjectForm.addEventListener('submit', saveTeacherAssignment); manageTsTbody.addEventListener('click', handleTeacherAssignmentTableClick); modalManageTeachersClose.addEventListener('click', () => modalManageTeachers.classList.remove('open')); modalManageTeachers.addEventListener('click', (e) => { if (e.target === modalManageTeachers) modalManageTeachers.classList.remove('open'); }); searchInput.addEventListener('input', debounce(() => { pageState.query = searchInput.value.trim(); pageState.page = 0; loadSubjects(); })); departmentFilter.addEventListener('change', () => { pageState.departmentId = departmentFilter.value; pageState.page = 0; loadSubjects(); }); sortSelect.addEventListener('change', () => { pageState.sort = sortSelect.value; pageState.page = 0; loadSubjects(); }); } async function loadInitialData() { try { allDepartments = await api.get('/api/departments'); teacherCombobox = new AsyncCombobox(manageTsTeacherSelect, { placeholder: 'Найдите преподавателя по ФИО', optionLabel: teacher => teacher.fullName || teacher.username, optionDescription: teacher => [teacher.jobTitle, teacher.departmentName].filter(Boolean).join(' · '), load: async (query, selectedId) => { const params = new URLSearchParams({ query, limit: '30' }); if (selectedId) params.set('selectedId', selectedId); const teachers = await api.get(`/api/users/teachers/options?${params}`); const subjectId = manageTsSubjectIdInput.value; const assigned = new Set(teacherSubjects .filter(item => String(item.subjectId) === String(subjectId)) .map(item => String(item.userId))); return teachers.filter(teacher => !assigned.has(String(teacher.id)) || String(teacher.id) === String(selectedId)); } }); populateDepartmentsSelect(); populateDepartmentFilter(); await loadSubjects(); } catch (e) { subjectsTbody.innerHTML = `Ошибка загрузки справочников: ${escapeHtml(e.message)}`; } } function populateDepartmentsSelect() { newSubjectDeptSelect.innerHTML = '' + allDepartments.map(d => { const name = d.departmentName || d.name || 'Без названия'; const code = d.departmentCode || d.code || d.id; return ``; }).join(''); } function populateDepartmentFilter() { departmentFilter.innerHTML = '' + allDepartments.map(department => `` ).join(''); departmentFilter.value = pageState.departmentId; } async function loadSubjects() { renderTableLoading(subjectsTbody, 6, 'Загрузка дисциплин...'); hideAlert('subjects-alert'); try { const [sort, direction] = pageState.sort.split(','); const params = new URLSearchParams({ query: pageState.query, page: String(pageState.page), size: String(pageState.size), sort, direction }); if (pageState.departmentId) params.set('departmentId', pageState.departmentId); const page = await api.get(`/api/subjects/page?${params}`); if (!(page.items || []).length && page.totalItems > 0 && pageState.page > 0) { pageState.page = Math.max(0, page.totalPages - 1); return loadSubjects(); } allSubjects = page.items || []; teacherSubjects = await loadTeacherAssignments(allSubjects.map(subject => subject.id)); renderSubjects(); renderPagination(pagination, page, { label: 'дисциплин', onPage: nextPage => { pageState.page = nextPage; loadSubjects(); }, onSize: size => { pageState.size = size; pageState.page = 0; loadSubjects(); } }); persistState(); trackUxEvent('реестр_дисциплин_загружен', { total: Number(page.totalItems || 0), page: pageState.page }); } catch (e) { renderTableError(subjectsTbody, 6, e.message || 'Не удалось загрузить дисциплины', loadSubjects); } } async function loadTeacherAssignments(subjectIds) { if (!subjectIds.length) return []; const params = new URLSearchParams(); subjectIds.forEach(subjectId => params.append('subjectId', subjectId)); return api.get(`/api/teacher-subjects?${params}`); } function renderSubjects() { if (!allSubjects || !allSubjects.length) { renderTableEmpty(subjectsTbody, 6, pageState.query || pageState.departmentId ? 'Дисциплины не найдены' : 'Дисциплин пока нет'); return; } // Группируем преподавателей по дисциплинам const teachersBySubject = new Map(); teacherSubjects.forEach(ts => { if (!teachersBySubject.has(ts.subjectId)) { teachersBySubject.set(ts.subjectId, []); } teachersBySubject.get(ts.subjectId).push(ts); }); subjectsTbody.innerHTML = allSubjects.map(s => { const teachers = teachersBySubject.get(s.id) || []; const teachersHtml = teachers.length ? teachers.map(t => `${escapeHtml(t.fullName || t.username)}`).join('') : 'Нет преподавателей'; return ` ${s.id} ${escapeHtml(s.name)} ${escapeHtml(s.code || '-')} ${escapeHtml(departmentLabel(s.departmentId))}
${teachersHtml}
`; }).join(''); markTableReady(subjectsTbody); } async function createSubject(e) { e.preventDefault(); hideAlert('create-subject-alert'); const name = document.getElementById('new-subject-name').value.trim(); const code = document.getElementById('new-subject-code').value.trim(); const departmentId = newSubjectDeptSelect.value; if (!name || !code || !departmentId) { showAlert('create-subject-alert', 'Заполните все поля', 'error'); return; } try { const data = await api.post('/api/subjects', { name, code, departmentId: Number(departmentId) }); showAlert('create-subject-alert', `Дисциплина "${escapeHtml(data.name || name)}" создана`, 'success'); createSubjectForm.reset(); await loadSubjects(); } catch (error) { showAlert('create-subject-alert', error.message || 'Ошибка создания', 'error'); } } async function handleSubjectsTableClick(e) { const btnDelete = e.target.closest('.btn-delete-subject'); const btnTeachers = e.target.closest('.btn-manage-teachers'); if (btnDelete) { if (!await confirmAction({ title: 'Архивировать дисциплину?', message: 'Дисциплина станет недоступна для новых назначений. Исторические данные сохранятся.', confirmLabel: 'Архивировать', danger: true })) return; try { await api.delete('/api/subjects/' + btnDelete.dataset.id); await loadSubjects(); } catch (error) { showAlert('subjects-alert', error.message || 'Ошибка архивирования', 'error'); } } if (btnTeachers) { const subjectId = btnTeachers.dataset.id; const subject = allSubjects.find(s => s.id == subjectId); if (subject) openTeachersModal(subject); } } // --- Управление привязками преподавателей к дисциплине --- function openTeachersModal(subject) { subjectTeachersContext.textContent = `${subject.code ? `${subject.code} · ` : ''}${subject.name}`; manageTsSubjectIdInput.value = subject.id; hideAlert('manage-ts-alert'); modalManageTeachers.classList.add('open'); renderSubjectTeachers(subject.id); populateTeachersSelect(subject.id); } function renderSubjectTeachers(subjectId) { const list = teacherSubjects.filter(ts => String(ts.subjectId) === String(subjectId)); if (!list.length) { manageTsTbody.innerHTML = 'Преподаватели не привязаны'; return; } manageTsTbody.innerHTML = list.map(ts => ` ${escapeHtml(ts.fullName || ts.username)} (${escapeHtml(ts.username)}) `).join(''); } function populateTeachersSelect(subjectId) { manageTsTeacherSelect.innerHTML = ''; manageTsTeacherSelect.value = ''; teacherCombobox?.syncFromSelect(); teacherCombobox?.search(''); } async function saveTeacherAssignment(e) { e.preventDefault(); hideAlert('manage-ts-alert'); const subjectId = manageTsSubjectIdInput.value; const userId = manageTsTeacherSelect.value; if (!userId) { showAlert('manage-ts-alert', 'Выберите преподавателя', 'error'); return; } try { await api.post('/api/teacher-subjects', { userId: Number(userId), subjectId: Number(subjectId) }); showAlert('manage-ts-alert', 'Преподаватель успешно привязан', 'success'); // Перезагрузим привязки и обновим интерфейс teacherSubjects = await loadTeacherAssignments(allSubjects.map(subject => subject.id)); renderSubjectTeachers(subjectId); populateTeachersSelect(subjectId); // Также обновим главную таблицу renderSubjects(); } catch (error) { showAlert('manage-ts-alert', error.message || 'Ошибка привязки', 'error'); } } async function handleTeacherAssignmentTableClick(e) { const btnDelete = e.target.closest('.btn-delete-ts'); if (!btnDelete) return; if (!await confirmAction({ title: 'Удалить привязку преподавателя?', message: 'Преподаватель больше не будет доступен для этой дисциплины в новых правилах.', confirmLabel: 'Удалить привязку', danger: true })) return; const subjectId = manageTsSubjectIdInput.value; try { await api.delete('/api/teacher-subjects', { userId: Number(btnDelete.dataset.userId), subjectId: Number(btnDelete.dataset.subjectId) }); // Перезагрузим привязки teacherSubjects = await loadTeacherAssignments(allSubjects.map(subject => subject.id)); renderSubjectTeachers(subjectId); populateTeachersSelect(subjectId); // Обновим главную таблицу renderSubjects(); } catch (error) { showAlert('manage-ts-alert', error.message || 'Ошибка удаления привязки', 'error'); } } // --- Вспомогательные функции --- function departmentLabel(departmentId) { const department = allDepartments.find(item => item.id == departmentId); if (!department) return departmentId || '-'; return department.departmentName || department.name || departmentId; } function persistState() { writeUrlState({ subjectQuery: pageState.query, subjectDepartment: pageState.departmentId, subjectPage: pageState.page || '', subjectSize: pageState.size === 25 ? '' : pageState.size, subjectSort: pageState.sort === 'name,asc' ? '' : pageState.sort }); } }