import { useEffect, useRef, useState } from 'react'; import { readUrlState, writeUrlState } from '../../../admin/js/url-state.js'; import { trackUxEvent } from '../../../telemetry.js'; import { PromptDialog } from '../../shared/ui/ConfirmDialog.jsx'; import { Pagination } from '../../shared/ui/Pagination.jsx'; import { requestCountLabel } from '../admin-service.js'; import { REQUEST_STATUS_OPTIONS, REQUEST_TABLE_COLUMNS, approvePayloadFromFields, approveRequest, fetchDepartments, fetchTeacherRequestsPage, initialTeacherRequestsState, pendingCountDetail, rejectRequest, requestDepartmentLabel, reviewedStatusLabel, teacherRequestsEmptyMessage, teacherRequestsUrlState } from '../teacher-requests-service.js'; const SEARCH_DEBOUNCE_MS = 320; export function TeacherRequestsTab() { const [state, setState] = useState(() => initialTeacherRequestsState( typeof window === 'undefined' ? {} : readUrlState({ requestQuery: '', requestStatus: 'PENDING', requestPage: '0', requestSize: '25' }) )); const [searchInput, setSearchInput] = useState(() => state.query); const [departments, setDepartments] = useState([]); const [departmentsError, setDepartmentsError] = useState(false); const [pageData, setPageData] = useState(null); const [tableError, setTableError] = useState(''); const [tableLoading, setTableLoading] = useState(true); const [alert, setAlert] = useState({ text: '', isError: false }); const [rejectState, setRejectState] = useState(null); const searchTimer = useRef(null); const requestNumber = useRef(0); const stateRef = useRef(state); stateRef.current = state; useEffect(() => { let active = true; (async () => { try { const loadedDepartments = await fetchDepartments(); if (!active) return; setDepartments(loadedDepartments || []); await loadRequests(stateRef.current); } catch { if (!active) return; setTableLoading(false); setDepartmentsError(true); } })(); return () => { active = false; requestNumber.current += 1; }; }, []); useEffect(() => () => { window.clearTimeout(searchTimer.current); }, []); async function loadRequests(nextState) { const currentRequest = requestNumber.current + 1; requestNumber.current = currentRequest; setTableLoading(true); setTableError(''); setAlert(current => (current.text ? { text: '', isError: false } : current)); try { let page = await fetchTeacherRequestsPage(nextState); if (!(page.items || []).length && page.totalItems > 0 && nextState.page > 0) { nextState = { ...nextState, page: Math.max(0, page.totalPages - 1) }; page = await fetchTeacherRequestsPage(nextState); } if (requestNumber.current !== currentRequest) return; setState(nextState); setPageData(page); setTableLoading(false); writeUrlState(teacherRequestsUrlState(nextState)); document.dispatchEvent(new CustomEvent('teacherRequestsChanged', { detail: pendingCountDetail(nextState, page.totalItems) })); trackUxEvent('реестр_заявок_загружен', { total: Number(page.totalItems || 0), page: nextState.page }); } catch { if (requestNumber.current !== currentRequest) return; setState(nextState); setPageData(null); setTableLoading(false); setTableError('Не удалось загрузить заявки преподавателей'); } } function applyState(patch) { void loadRequests({ ...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 approve(requestId, fields) { const { payload, error } = approvePayloadFromFields(fields); if (error) { setAlert({ text: error, isError: true }); return; } try { await approveRequest(requestId, payload); setAlert({ text: 'Преподаватель создан, заявка одобрена', isError: false }); await loadRequests(stateRef.current); } catch (err) { setAlert({ text: err.message || 'Ошибка одобрения заявки', isError: true }); } } async function confirmReject(reviewComment) { const requestId = rejectState?.requestId; setRejectState(null); if (!requestId) return; try { await rejectRequest(requestId, reviewComment); setAlert({ text: 'Заявка отклонена', isError: false }); await loadRequests(stateRef.current); } catch (err) { setAlert({ text: err.message || 'Ошибка отклонения заявки', isError: true }); } } const requests = pageData?.items || []; const countLabel = requestCountLabel(pageData ? Number(pageData.totalItems || 0) : 0); return (

Заявки на преподавателей

{countLabel}
{alert.text}
handleSearchInput(event.target.value)} />
{departmentsError ? ( ) : tableError ? ( ) : tableLoading ? ( ) : requests.length === 0 ? ( ) : requests.map(request => ( setRejectState({ requestId })} /> ))}
Кафедра Логин ФИО Должность Пароль Комментарий Действия
Не удалось загрузить заявки преподавателей
{tableError}
{teacherRequestsEmptyMessage(state)}
{pageData && ( applyState({ page })} onSize={size => applyState({ size, page: 0 })} /> )} {rejectState && ( void confirmReject(value)} onClose={() => setRejectState(null)} /> )}
); } // Строка заявки хранит черновик полей локально: значения инпутов строки // отправляются при одобрении — как в legacy-реестре. function TeacherRequestRow({ request, departments, onApprove, onReject }) { const [departmentId, setDepartmentId] = useState(String(request.departmentId ?? '')); const [username, setUsername] = useState(request.username || ''); const [fullName, setFullName] = useState(request.fullName || ''); const [jobTitle, setJobTitle] = useState(request.jobTitle || ''); const [password, setPassword] = useState(''); function approve() { onApprove(request.id, { departmentId, username, fullName, jobTitle, password }); } return ( setUsername(event.target.value)} /> setFullName(event.target.value)} /> setJobTitle(event.target.value)} /> setPassword(event.target.value)} /> {request.comment || '-'}
{request.status === 'PENDING' ? ( <> ) : ( {reviewedStatusLabel(request.status)} )}
); }