192 lines
7.7 KiB
JavaScript
192 lines
7.7 KiB
JavaScript
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 => `
|
||
<tr>
|
||
<td>${u.id}</td>
|
||
<td>${escapeHtml(u.username)}</td>
|
||
<td>${escapeHtml(u.fullName || '-')}</td>
|
||
<td>${escapeHtml(u.jobTitle || '-')}</td>
|
||
<td>${escapeHtml(u.departmentName || '-')}</td>
|
||
<td><span class="badge ${ROLE_BADGE[u.role] || ''}">${ROLE_LABELS[u.role] || escapeHtml(u.role)}</span></td>
|
||
<td>
|
||
${u.status === 'ARCHIVED'
|
||
? `<button class="btn btn-sm btn-secondary btn-restore-user" data-id="${u.id}">Восстановить</button>`
|
||
: `<button class="btn btn-sm btn-danger-subtle btn-archive-user" data-id="${u.id}">Архивировать</button>`}
|
||
</td>
|
||
</tr>
|
||
`).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();
|
||
}
|