Files
magistr/frontend/admin/js/views/teacher-requests.js
2026-08-20 00:55:27 +03:00

206 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { api } from '../api.js';
import { escapeHtml, hideAlert, renderTableMessage, showAlert } from '../utils.js';
import { promptAction } 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';
export async function initTeacherRequests() {
const requestsTbody = document.getElementById('teacher-requests-tbody');
const refreshButton = document.getElementById('teacher-requests-refresh');
const requestsCount = document.getElementById('teacher-requests-count');
const searchInput = document.getElementById('teacher-requests-search');
const statusSelect = document.getElementById('teacher-requests-status');
const pagination = document.getElementById('teacher-requests-pagination');
let departments = [];
const restored = readUrlState({ requestQuery: '', requestStatus: 'PENDING', requestPage: '0', requestSize: '25' });
const state = {
query: restored.requestQuery,
status: restored.requestStatus,
page: Math.max(Number(restored.requestPage) || 0, 0),
size: [25, 50, 100].includes(Number(restored.requestSize)) ? Number(restored.requestSize) : 25
};
searchInput.value = state.query;
statusSelect.value = state.status;
async function loadDepartments() {
departments = await api.get('/api/departments');
}
async function loadRequests() {
renderTableLoading(requestsTbody, 7, 'Загрузка заявок...');
hideAlert('teacher-requests-alert');
try {
const params = new URLSearchParams({
query: state.query,
page: String(state.page),
size: String(state.size),
sort: 'createdAt',
direction: 'desc'
});
if (state.status) params.set('status', state.status);
const page = await api.get(`/api/teacher-requests/page?${params}`);
if (!(page.items || []).length && page.totalItems > 0 && state.page > 0) {
state.page = Math.max(0, page.totalPages - 1);
return loadRequests();
}
const normalizedRequests = page.items || [];
renderRequests(normalizedRequests);
updateCount(Number(page.totalItems || 0));
renderPagination(pagination, page, {
label: 'заявок',
onPage: nextPage => { state.page = nextPage; loadRequests(); },
onSize: size => { state.size = size; state.page = 0; loadRequests(); }
});
persistState();
document.dispatchEvent(new CustomEvent('teacherRequestsChanged', {
detail: { count: state.status === 'PENDING' && !state.query ? Number(page.totalItems || 0) : undefined }
}));
trackUxEvent('реестр_заявок_загружен', { total: Number(page.totalItems || 0), page: state.page });
} catch (error) {
renderTableError(requestsTbody, 7, 'Не удалось загрузить заявки преподавателей', loadRequests);
updateCount(0);
}
}
function renderRequests(requests) {
if (!requests.length) {
renderTableEmpty(requestsTbody, 7, state.query ? 'Заявки не найдены' : 'Заявок с выбранным статусом нет');
return;
}
requestsTbody.innerHTML = requests.map(request => `
<tr data-request-id="${escapeHtml(request.id)}">
<td>
<select class="teacher-request-department" data-native-select="true">
${departments.map(department => `
<option value="${escapeHtml(department.id)}" ${String(department.id) === String(request.departmentId) ? 'selected' : ''}>
${escapeHtml(department.departmentName || department.name || department.id)}
</option>
`).join('')}
</select>
</td>
<td><input type="text" class="teacher-request-username" value="${escapeHtml(request.username || '')}" maxlength="50" autocomplete="username"></td>
<td><input type="text" class="teacher-request-fullname" value="${escapeHtml(request.fullName || '')}" maxlength="255"></td>
<td><input type="text" class="teacher-request-jobtitle" value="${escapeHtml(request.jobTitle || '')}" maxlength="255"></td>
<td><input type="password" class="teacher-request-password" placeholder="Минимум 8 символов" autocomplete="new-password"></td>
<td>${escapeHtml(request.comment || '-')}</td>
<td>
<div class="teacher-request-actions">
${request.status === 'PENDING' ? `
<button type="button" class="btn btn-sm btn-primary teacher-request-approve">Одобрить</button>
<button type="button" class="btn btn-sm btn-danger-subtle teacher-request-reject">Отклонить</button>
` : `<span class="badge">${escapeHtml(request.status === 'APPROVED' ? 'Одобрена' : 'Отклонена')}</span>`}
</div>
</td>
</tr>
`).join('');
markTableReady(requestsTbody);
}
function updateCount(count) {
if (!requestsCount) return;
requestsCount.textContent = requestCountLabel(count);
}
function requestCountLabel(count) {
const mod10 = count % 10;
const mod100 = count % 100;
if (mod10 === 1 && mod100 !== 11) return `${count} заявка`;
if (mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14)) return `${count} заявки`;
return `${count} заявок`;
}
async function approveRequest(row, requestId) {
const payload = {
departmentId: Number(row.querySelector('.teacher-request-department')?.value),
username: row.querySelector('.teacher-request-username')?.value.trim(),
fullName: row.querySelector('.teacher-request-fullname')?.value.trim(),
jobTitle: row.querySelector('.teacher-request-jobtitle')?.value.trim(),
password: row.querySelector('.teacher-request-password')?.value
};
if (!payload.departmentId || !payload.username || !payload.fullName || !payload.password) {
showAlert('teacher-requests-alert', 'Заполните кафедру, логин, ФИО и пароль', 'error');
return;
}
if (payload.password.length < 8) {
showAlert('teacher-requests-alert', 'Пароль должен быть не короче 8 символов', 'error');
return;
}
try {
await api.post(`/api/teacher-requests/${encodeURIComponent(requestId)}/approve`, payload);
showAlert('teacher-requests-alert', 'Преподаватель создан, заявка одобрена', 'success');
await loadRequests();
} catch (error) {
showAlert('teacher-requests-alert', error.message || 'Ошибка одобрения заявки', 'error');
}
}
async function rejectRequest(requestId) {
const reviewComment = await promptAction({
title: 'Отклонить заявку?',
message: 'Комментарий увидит кафедра, отправившая заявку.',
confirmLabel: 'Отклонить',
danger: true,
input: {
label: 'Причина отклонения',
placeholder: 'Укажите, что нужно исправить',
required: true,
requiredMessage: 'Укажите причину отклонения'
}
});
if (reviewComment === null) return;
try {
await api.post(`/api/teacher-requests/${encodeURIComponent(requestId)}/reject`, { reviewComment });
showAlert('teacher-requests-alert', 'Заявка отклонена', 'success');
await loadRequests();
} catch (error) {
showAlert('teacher-requests-alert', error.message || 'Ошибка отклонения заявки', 'error');
}
}
refreshButton?.addEventListener('click', loadRequests);
searchInput.addEventListener('input', debounce(() => {
state.query = searchInput.value.trim();
state.page = 0;
loadRequests();
}));
statusSelect.addEventListener('change', () => {
state.status = statusSelect.value;
state.page = 0;
loadRequests();
});
requestsTbody.addEventListener('click', async (event) => {
const approveButton = event.target.closest('.teacher-request-approve');
const rejectButton = event.target.closest('.teacher-request-reject');
if (!approveButton && !rejectButton) return;
const row = event.target.closest('tr[data-request-id]');
const requestId = row?.dataset.requestId;
if (!requestId) return;
if (approveButton) {
await approveRequest(row, requestId);
return;
}
await rejectRequest(requestId);
});
try {
await loadDepartments();
await loadRequests();
} catch (error) {
renderTableMessage(requestsTbody, 7, 'Не удалось загрузить заявки преподавателей');
}
function persistState() {
writeUrlState({
requestQuery: state.query,
requestStatus: state.status === 'PENDING' ? '' : state.status,
requestPage: state.page || '',
requestSize: state.size === 25 ? '' : state.size
});
}
}