import { useEffect, useRef, useState } from 'react'; import { readUrlState, writeUrlState } from '../../../admin/js/url-state.js'; import { trackUxEvent } from '../../../telemetry.js'; import { AlertDialog, ConfirmDialog } from '../../shared/ui/ConfirmDialog.jsx'; import { Pagination } from '../../shared/ui/Pagination.jsx'; import { ROLE_FILTER_OPTIONS, USER_SORT_OPTIONS, USER_TABLE_COLUMNS, archiveUser, createUser, fetchUsersPage, initialUsersState, restoreUser, roleBadgeClass, roleLabel, usersEmptyMessage, usersUrlState } from '../users-service.js'; const SEARCH_DEBOUNCE_MS = 320; export function UsersTab() { const [state, setState] = useState(() => initialUsersState( typeof window === 'undefined' ? {} : readUrlState({ userQuery: '', userRole: '', userPage: '0', userSize: '25', userSort: 'fullName,asc' }) )); const [searchInput, setSearchInput] = useState(() => state.query); const [pageData, setPageData] = useState(null); const [tableError, setTableError] = useState(''); const [tableLoading, setTableLoading] = useState(true); const [createUsername, setCreateUsername] = useState(''); const [createPassword, setCreatePassword] = useState(''); const [createRole, setCreateRole] = useState('STUDENT'); const [createFullName, setCreateFullName] = useState(''); const [createJobTitle, setCreateJobTitle] = useState(''); const [createDepartmentId, setCreateDepartmentId] = useState(''); const [createAlert, setCreateAlert] = useState({ text: '', isError: false }); const [confirmState, setConfirmState] = useState(null); const [alertState, setAlertState] = useState(null); const searchTimer = useRef(null); const requestNumber = useRef(0); const stateRef = useRef(state); stateRef.current = state; useEffect(() => { void loadUsers(stateRef.current); return () => { requestNumber.current += 1; }; }, []); useEffect(() => () => { window.clearTimeout(searchTimer.current); }, []); async function loadUsers(nextState) { const currentRequest = requestNumber.current + 1; requestNumber.current = currentRequest; setTableLoading(true); setTableError(''); try { let page = await fetchUsersPage(nextState); if (!(page.items || []).length && page.totalItems > 0 && nextState.page > 0) { nextState = { ...nextState, page: Math.max(0, page.totalPages - 1) }; page = await fetchUsersPage(nextState); } if (requestNumber.current !== currentRequest) return; setState(nextState); setPageData(page); setTableLoading(false); writeUrlState(usersUrlState(nextState)); trackUxEvent('реестр_пользователей_загружен', { total: Number(page.totalItems || 0), page: nextState.page }); } catch (error) { if (requestNumber.current !== currentRequest) return; setState(nextState); setTableLoading(false); setTableError(error.message || 'Не удалось загрузить пользователей'); } } function applyState(patch) { void loadUsers({ ...stateRef.current, ...patch }); } function handleSearchInput(value) { setSearchInput(value); window.clearTimeout(searchTimer.current); searchTimer.current = window.setTimeout(() => { applyState({ query: value.trim(), page: 0 }); }, SEARCH_DEBOUNCE_MS); } async function submitCreate(event) { event.preventDefault(); const username = createUsername.trim(); const password = createPassword; const fullName = createFullName.trim(); const jobTitle = createJobTitle.trim(); if (!username || !password || !fullName || !jobTitle || !createDepartmentId) { setCreateAlert({ text: 'Заполните все поля', isError: true }); return; } if (password.length < 8) { setCreateAlert({ text: 'Пароль должен быть не короче 8 символов', isError: true }); return; } try { const created = await createUser({ username, password, role: createRole, fullName, jobTitle, departmentId: createDepartmentId }); setCreateAlert({ text: `Пользователь «${created.username}» создан`, isError: false }); setCreateUsername(''); setCreatePassword(''); setCreateFullName(''); setCreateJobTitle(''); setCreateDepartmentId(''); await loadUsers(stateRef.current); } catch (error) { setCreateAlert({ text: error.message || 'Ошибка соединения', isError: true }); } } function confirmArchiveUser(user) { setConfirmState({ title: 'Архивировать пользователя?', message: 'Доступ будет отключён, а история расписания и связей сохранится.', confirmLabel: 'Архивировать', danger: true, action: async () => { try { await archiveUser(user.id); await loadUsers(stateRef.current); } catch (error) { setAlertState({ title: 'Ошибка архивирования', message: error.message || 'Ошибка архивирования' }); } } }); } async function restore(user) { try { await restoreUser(user.id); await loadUsers(stateRef.current); } catch (error) { setAlertState({ title: 'Ошибка восстановления', message: error.message || 'Ошибка восстановления' }); } } async function runConfirm() { const confirm = confirmState; setConfirmState(null); await confirm.action(); } const users = pageData?.items || []; return ( <>

Новый пользователь

setCreateUsername(event.target.value)} />
setCreatePassword(event.target.value)} />
setCreateFullName(event.target.value)} />
setCreateJobTitle(event.target.value)} />
setCreateDepartmentId(event.target.value)} />
{createAlert.text}

Все пользователи

handleSearchInput(event.target.value)} />
{tableError ? ( ) : tableLoading ? ( ) : users.length === 0 ? ( ) : users.map(user => ( ))}
ID Имя пользователя ФИО Должность Кафедра Роль Действия
{tableError}
{usersEmptyMessage(state)}
{user.id} {user.username} {user.fullName || '-'} {user.jobTitle || '-'} {user.departmentName || '-'} {roleLabel(user.role)} {user.status === 'ARCHIVED' ? ( ) : ( )}
{pageData && ( applyState({ page })} onSize={size => applyState({ size, page: 0 })} /> )}
{confirmState && ( void runConfirm()} onClose={() => setConfirmState(null)} /> )} {alertState && ( setAlertState(null)} /> )} ); }