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'; const ROLE_LABELS = { ADMIN: 'Администратор', EDUCATION_OFFICE: 'Учебный отдел', DEPARTMENT: 'Кафедра', SCHEDULE_VIEWER: 'Просмотр расписаний', TEACHER: 'Преподаватель', STUDENT: 'Студент' }; const ROLE_BADGE = { ADMIN: 'badge-admin', EDUCATION_OFFICE: 'badge-admin', DEPARTMENT: 'badge-teacher', SCHEDULE_VIEWER: 'badge-student', TEACHER: 'badge-teacher', STUDENT: 'badge-student' }; export async function initUsers() { const usersTbody = document.getElementById('users-tbody'); const createForm = document.getElementById('create-form'); const searchInput = document.getElementById('users-search'); const roleFilter = document.getElementById('users-role-filter'); const sortSelect = document.getElementById('users-sort'); const pagination = document.getElementById('users-pagination'); const restored = readUrlState({ userQuery: '', userRole: '', userPage: '0', userSize: '25', userSort: 'fullName,asc' }); const state = { query: restored.userQuery, role: restored.userRole, page: Math.max(Number(restored.userPage) || 0, 0), size: [25, 50, 100].includes(Number(restored.userSize)) ? Number(restored.userSize) : 25, sort: restored.userSort }; searchInput.value = state.query; roleFilter.value = state.role; sortSelect.value = state.sort; async function loadUsers() { renderTableLoading(usersTbody, 7, 'Загрузка пользователей...'); hideAlert('users-alert'); try { const [sort, direction] = state.sort.split(','); const params = new URLSearchParams({ query: state.query, page: String(state.page), size: String(state.size), sort, direction, includeArchived: 'true' }); if (state.role) params.set('role', state.role); const result = await api.get(`/api/users/page?${params}`); renderUsers(result.items || []); renderPagination(pagination, result, { label: 'пользователей', onPage: page => { state.page = page; loadUsers(); }, onSize: size => { state.size = size; state.page = 0; loadUsers(); } }); persistState(); trackUxEvent('реестр_пользователей_загружен', { total: Number(result.totalItems || 0), page: state.page }); } catch (e) { renderTableError(usersTbody, 7, e.message || 'Не удалось загрузить пользователей', loadUsers); } } function renderUsers(users) { if (!users || !users.length) { renderTableEmpty(usersTbody, 7, state.query || state.role ? 'Пользователи не найдены' : 'Пользователей пока нет'); return; } usersTbody.innerHTML = users.map(u => ` ${u.id} ${escapeHtml(u.username)} ${escapeHtml(u.fullName || '-')} ${escapeHtml(u.jobTitle || '-')} ${escapeHtml(u.departmentName || '-')} ${ROLE_LABELS[u.role] || escapeHtml(u.role)} ${u.status === 'ARCHIVED' ? `` : ``} `).join(''); markTableReady(usersTbody); } searchInput.addEventListener('input', debounce(() => { state.query = searchInput.value.trim(); state.page = 0; loadUsers(); })); roleFilter.addEventListener('change', () => { state.role = roleFilter.value; state.page = 0; loadUsers(); }); sortSelect.addEventListener('change', () => { state.sort = sortSelect.value; state.page = 0; loadUsers(); }); createForm.addEventListener('submit', async (e) => { e.preventDefault(); hideAlert('create-alert'); const username = document.getElementById('new-username').value.trim(); const password = document.getElementById('new-password').value; const role = document.getElementById('new-role').value; const fullName = document.getElementById('new-fullname').value.trim(); const jobTitle = document.getElementById('new-jobtitle').value.trim(); const departmentId = document.getElementById('new-department').value; if (!username || !password || !fullName || !jobTitle || !departmentId) { showAlert('create-alert', 'Заполните все поля', 'error'); return; } if (password.length < 8) { showAlert('create-alert', 'Пароль должен быть не короче 8 символов', 'error'); return; } try { const data = await api.post('/api/users', { username, password, role, fullName, jobTitle, departmentId: Number(departmentId) }); showAlert('create-alert', `Пользователь "${escapeHtml(data.username)}" создан`, 'success'); createForm.reset(); loadUsers(); } catch (err) { showAlert('create-alert', err.message || 'Ошибка соединения', 'error'); } }); usersTbody.addEventListener('click', async (e) => { const deleteBtn = e.target.closest('.btn-archive-user'); if (!deleteBtn) return; if (!await confirmAction({ title: 'Архивировать пользователя?', message: 'Доступ будет отключён, а история расписания и связей сохранится.', confirmLabel: 'Архивировать', danger: true })) return; try { await api.delete('/api/users/' + deleteBtn.dataset.id); loadUsers(); } catch (err) { showAlert('users-alert', err.message || 'Ошибка архивирования', 'error'); } }); usersTbody.addEventListener('click', async (e) => { const restoreBtn = e.target.closest('.btn-restore-user'); if (!restoreBtn) return; try { await api.post('/api/users/' + restoreBtn.dataset.id + '/restore', {}); loadUsers(); } catch (err) { showAlert('users-alert', err.message || 'Ошибка восстановления', 'error'); } }); function persistState() { writeUrlState({ userQuery: state.query, userRole: state.role, userPage: state.page || '', userSize: state.size === 25 ? '' : state.size, userSort: state.sort === 'fullName,asc' ? '' : state.sort }); } await loadUsers(); }