Много разных изменений

This commit is contained in:
Zuev
2026-08-20 00:55:27 +03:00
parent 3599d850f6
commit a1c4ee4298
78 changed files with 4698 additions and 428 deletions

View File

@@ -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
});
}
}