264 lines
12 KiB
JavaScript
Executable File
264 lines
12 KiB
JavaScript
Executable File
import { api } from '../api.js';
|
||
import { escapeHtml, showAlert, hideAlert } from '../utils.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 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 allTeachers = [];
|
||
let allDepartments = [];
|
||
let teacherSubjects = [];
|
||
|
||
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');
|
||
});
|
||
}
|
||
|
||
async function loadInitialData() {
|
||
try {
|
||
[allTeachers, allDepartments] = await Promise.all([
|
||
api.get('/api/users/teachers'),
|
||
api.get('/api/departments')
|
||
]);
|
||
populateDepartmentsSelect();
|
||
await loadSubjects();
|
||
} catch (e) {
|
||
subjectsTbody.innerHTML = `<tr><td colspan="6" class="loading-row">Ошибка загрузки справочников: ${escapeHtml(e.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
function populateDepartmentsSelect() {
|
||
newSubjectDeptSelect.innerHTML = '<option value="">Выберите кафедру</option>' +
|
||
allDepartments.map(d => {
|
||
const name = d.departmentName || d.name || 'Без названия';
|
||
const code = d.departmentCode || d.code || d.id;
|
||
return `<option value="${d.id}">${escapeHtml(name)} (${escapeHtml(String(code))})</option>`;
|
||
}).join('');
|
||
}
|
||
|
||
async function loadSubjects() {
|
||
subjectsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Загрузка...</td></tr>';
|
||
try {
|
||
[allSubjects, teacherSubjects] = await Promise.all([
|
||
api.get('/api/subjects'),
|
||
api.get('/api/teacher-subjects')
|
||
]);
|
||
renderSubjects();
|
||
} catch (e) {
|
||
subjectsTbody.innerHTML = `<tr><td colspan="6" class="loading-row">Ошибка загрузки: ${escapeHtml(e.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
function renderSubjects() {
|
||
if (!allSubjects || !allSubjects.length) {
|
||
subjectsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Нет дисциплин</td></tr>';
|
||
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 => `<span class="badge badge-ef" style="margin: 0.1rem;">${escapeHtml(t.fullName || t.username)}</span>`).join('')
|
||
: '<span class="text-muted"><small>Нет преподавателей</small></span>';
|
||
|
||
return `
|
||
<tr>
|
||
<td>${s.id}</td>
|
||
<td><strong>${escapeHtml(s.name)}</strong></td>
|
||
<td>${escapeHtml(s.code || '-')}</td>
|
||
<td>${escapeHtml(departmentLabel(s.departmentId))}</td>
|
||
<td><div style="display: flex; flex-wrap: wrap; gap: 0.25rem;">${teachersHtml}</div></td>
|
||
<td style="text-align: right; white-space: nowrap;">
|
||
<button class="btn-primary btn-manage-teachers" data-id="${s.id}" style="padding: 0.25rem 0.5rem; font-size: 0.85rem;">Преподаватели</button>
|
||
<button class="btn-delete btn-delete-subject" data-id="${s.id}">Удалить</button>
|
||
</td>
|
||
</tr>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
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 (!confirm('Удалить дисциплину? Это удалит её привязки к преподавателям.')) return;
|
||
try {
|
||
await api.delete('/api/subjects/' + btnDelete.dataset.id);
|
||
await loadSubjects();
|
||
} catch (error) {
|
||
alert(error.message || 'Ошибка удаления');
|
||
}
|
||
}
|
||
|
||
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 = '<tr><td colspan="2" class="loading-row">Преподаватели не привязаны</td></tr>';
|
||
return;
|
||
}
|
||
manageTsTbody.innerHTML = list.map(ts => `
|
||
<tr>
|
||
<td><strong>${escapeHtml(ts.fullName || ts.username)}</strong> <span class="text-muted">(${escapeHtml(ts.username)})</span></td>
|
||
<td>
|
||
<button class="btn-delete btn-delete-ts" data-user-id="${ts.userId}" data-subject-id="${ts.subjectId}">Удалить</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
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('');
|
||
}
|
||
|
||
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 api.get('/api/teacher-subjects');
|
||
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 (!confirm('Удалить привязку преподавателя?')) return;
|
||
const subjectId = manageTsSubjectIdInput.value;
|
||
|
||
try {
|
||
await api.delete('/api/teacher-subjects', {
|
||
userId: Number(btnDelete.dataset.userId),
|
||
subjectId: Number(btnDelete.dataset.subjectId)
|
||
});
|
||
|
||
// Перезагрузим привязки
|
||
teacherSubjects = await api.get('/api/teacher-subjects');
|
||
renderSubjectTeachers(subjectId);
|
||
populateTeachersSelect(subjectId);
|
||
|
||
// Обновим главную таблицу
|
||
renderSubjects();
|
||
} catch (error) {
|
||
alert(error.message || 'Ошибка удаления привязки');
|
||
}
|
||
}
|
||
|
||
// --- Вспомогательные функции ---
|
||
function departmentLabel(departmentId) {
|
||
const department = allDepartments.find(item => item.id == departmentId);
|
||
if (!department) return departmentId || '-';
|
||
return department.departmentName || department.name || departmentId;
|
||
}
|
||
}
|