Много разных изменений
This commit is contained in:
@@ -1,10 +1,20 @@
|
||||
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');
|
||||
@@ -16,9 +26,19 @@ export async function initSubjects() {
|
||||
const manageTsTbody = document.getElementById('manage-ts-tbody');
|
||||
|
||||
let allSubjects = [];
|
||||
let allTeachers = [];
|
||||
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();
|
||||
@@ -34,15 +54,43 @@ export async function initSubjects() {
|
||||
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 {
|
||||
[allTeachers, allDepartments] = await Promise.all([
|
||||
api.get('/api/users/teachers'),
|
||||
api.get('/api/departments')
|
||||
]);
|
||||
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 = `<tr><td colspan="6" class="loading-row">Ошибка загрузки справочников: ${escapeHtml(e.message)}</td></tr>`;
|
||||
@@ -58,22 +106,58 @@ export async function initSubjects() {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function populateDepartmentFilter() {
|
||||
departmentFilter.innerHTML = '<option value="">Все кафедры</option>' + allDepartments.map(department =>
|
||||
`<option value="${department.id}">${escapeHtml(department.departmentName || department.name || department.id)}</option>`
|
||||
).join('');
|
||||
departmentFilter.value = pageState.departmentId;
|
||||
}
|
||||
|
||||
async function loadSubjects() {
|
||||
subjectsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Загрузка...</td></tr>';
|
||||
renderTableLoading(subjectsTbody, 6, 'Загрузка дисциплин...');
|
||||
hideAlert('subjects-alert');
|
||||
try {
|
||||
[allSubjects, teacherSubjects] = await Promise.all([
|
||||
api.get('/api/subjects'),
|
||||
api.get('/api/teacher-subjects')
|
||||
]);
|
||||
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) {
|
||||
subjectsTbody.innerHTML = `<tr><td colspan="6" class="loading-row">Ошибка загрузки: ${escapeHtml(e.message)}</td></tr>`;
|
||||
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) {
|
||||
subjectsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Нет дисциплин</td></tr>';
|
||||
renderTableEmpty(subjectsTbody, 6, pageState.query || pageState.departmentId
|
||||
? 'Дисциплины не найдены'
|
||||
: 'Дисциплин пока нет');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -106,6 +190,7 @@ export async function initSubjects() {
|
||||
</tr>
|
||||
`;
|
||||
}).join('');
|
||||
markTableReady(subjectsTbody);
|
||||
}
|
||||
|
||||
async function createSubject(e) {
|
||||
@@ -139,12 +224,17 @@ export async function initSubjects() {
|
||||
const btnTeachers = e.target.closest('.btn-manage-teachers');
|
||||
|
||||
if (btnDelete) {
|
||||
if (!confirm('Удалить дисциплину? Это удалит её привязки к преподавателям.')) return;
|
||||
if (!await confirmAction({
|
||||
title: 'Архивировать дисциплину?',
|
||||
message: 'Дисциплина станет недоступна для новых назначений. Исторические данные сохранятся.',
|
||||
confirmLabel: 'Архивировать',
|
||||
danger: true
|
||||
})) return;
|
||||
try {
|
||||
await api.delete('/api/subjects/' + btnDelete.dataset.id);
|
||||
await loadSubjects();
|
||||
} catch (error) {
|
||||
alert(error.message || 'Ошибка удаления');
|
||||
showAlert('subjects-alert', error.message || 'Ошибка архивирования', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,22 +272,10 @@ export async function initSubjects() {
|
||||
}
|
||||
|
||||
function populateTeachersSelect(subjectId) {
|
||||
// Показываем только тех преподавателей, которые еще не привязаны к этой дисциплине
|
||||
const assignedUserIds = new Set(
|
||||
teacherSubjects
|
||||
.filter(ts => String(ts.subjectId) === String(subjectId))
|
||||
.map(ts => String(ts.userId))
|
||||
);
|
||||
|
||||
const availableTeachers = allTeachers.filter(t => !assignedUserIds.has(String(t.id)));
|
||||
|
||||
if (!availableTeachers.length) {
|
||||
manageTsTeacherSelect.innerHTML = '<option value="">Все преподаватели привязаны</option>';
|
||||
return;
|
||||
}
|
||||
|
||||
manageTsTeacherSelect.innerHTML = '<option value="">Выберите преподавателя</option>' +
|
||||
availableTeachers.map(t => `<option value="${t.id}">${escapeHtml(t.fullName || t.username)} (${escapeHtml(t.username)})</option>`).join('');
|
||||
manageTsTeacherSelect.innerHTML = '<option value=""></option>';
|
||||
manageTsTeacherSelect.value = '';
|
||||
teacherCombobox?.syncFromSelect();
|
||||
teacherCombobox?.search('');
|
||||
}
|
||||
|
||||
async function saveTeacherAssignment(e) {
|
||||
@@ -219,7 +297,7 @@ export async function initSubjects() {
|
||||
showAlert('manage-ts-alert', 'Преподаватель успешно привязан', 'success');
|
||||
|
||||
// Перезагрузим привязки и обновим интерфейс
|
||||
teacherSubjects = await api.get('/api/teacher-subjects');
|
||||
teacherSubjects = await loadTeacherAssignments(allSubjects.map(subject => subject.id));
|
||||
renderSubjectTeachers(subjectId);
|
||||
populateTeachersSelect(subjectId);
|
||||
|
||||
@@ -233,7 +311,12 @@ export async function initSubjects() {
|
||||
async function handleTeacherAssignmentTableClick(e) {
|
||||
const btnDelete = e.target.closest('.btn-delete-ts');
|
||||
if (!btnDelete) return;
|
||||
if (!confirm('Удалить привязку преподавателя?')) return;
|
||||
if (!await confirmAction({
|
||||
title: 'Удалить привязку преподавателя?',
|
||||
message: 'Преподаватель больше не будет доступен для этой дисциплины в новых правилах.',
|
||||
confirmLabel: 'Удалить привязку',
|
||||
danger: true
|
||||
})) return;
|
||||
const subjectId = manageTsSubjectIdInput.value;
|
||||
|
||||
try {
|
||||
@@ -243,14 +326,14 @@ export async function initSubjects() {
|
||||
});
|
||||
|
||||
// Перезагрузим привязки
|
||||
teacherSubjects = await api.get('/api/teacher-subjects');
|
||||
teacherSubjects = await loadTeacherAssignments(allSubjects.map(subject => subject.id));
|
||||
renderSubjectTeachers(subjectId);
|
||||
populateTeachersSelect(subjectId);
|
||||
|
||||
// Обновим главную таблицу
|
||||
renderSubjects();
|
||||
} catch (error) {
|
||||
alert(error.message || 'Ошибка удаления привязки');
|
||||
showAlert('manage-ts-alert', error.message || 'Ошибка удаления привязки', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,4 +343,14 @@ export async function initSubjects() {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user