feat: завершить миграцию фронтенда на React (этапы 1-5)
Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на React 19 (модель strangler, docs/REACT_MIGRATION.md). - 15 вкладок админки: lazy-загрузка, общий error boundary, сервисный слой - Удалены legacy-views и переходный слой LegacyTabView (45 файлов) - Сборка esbuild (frontend/scripts/build-react.mjs) с бюджетами gzip на чанки - CSP-хэши, кэширование: chunk'и immutable, CSS/HTML no-cache (react-cache.conf) - Фиксы: lazy-импорт именованных экспортов (краш после авторизации), растяжение #admin-root/#settings-root на всю ширину, возврат subjects/lessonTypes из справочников (краш просмотра расписаний), отказ от заведомо широкого поиска расписания без выбранной цели, центрирование карточки логина - Тесты: 237/237 (npm run check), регрессионные на lazy-вкладки и ширину - Документация: REACT_MIGRATION.md, REACT_MIGRATION_RESULT.md, FRONTEND.md
This commit is contained in:
118
frontend/react/admin/teacher-requests-service.js
Normal file
118
frontend/react/admin/teacher-requests-service.js
Normal file
@@ -0,0 +1,118 @@
|
||||
import { apiClient } from '../shared/api-client.js';
|
||||
|
||||
export const REQUEST_TABLE_COLUMNS = 7;
|
||||
export const REQUEST_PAGE_SIZES = [25, 50, 100];
|
||||
|
||||
export const REQUEST_STATUS_OPTIONS = Object.freeze([
|
||||
{ value: 'PENDING', label: 'Ожидают рассмотрения' },
|
||||
{ value: 'APPROVED', label: 'Одобрены' },
|
||||
{ value: 'REJECTED', label: 'Отклонены' },
|
||||
{ value: '', label: 'Все статусы' }
|
||||
]);
|
||||
|
||||
export function normalizeTeacherRequestsState(restored = {}) {
|
||||
const size = Number(restored.requestSize);
|
||||
return {
|
||||
query: restored.requestQuery || '',
|
||||
status: restored.requestStatus || 'PENDING',
|
||||
page: Math.max(Number(restored.requestPage) || 0, 0),
|
||||
size: REQUEST_PAGE_SIZES.includes(size) ? size : 25
|
||||
};
|
||||
}
|
||||
|
||||
export function initialTeacherRequestsState(restored = {}) {
|
||||
return normalizeTeacherRequestsState(restored);
|
||||
}
|
||||
|
||||
export function teacherRequestsQueryParams(state) {
|
||||
const params = new URLSearchParams({
|
||||
query: state.query || '',
|
||||
page: String(state.page || 0),
|
||||
size: String(state.size || 25),
|
||||
sort: 'createdAt',
|
||||
direction: 'desc'
|
||||
});
|
||||
if (state.status) params.set('status', state.status);
|
||||
return `/api/teacher-requests/page?${params}`;
|
||||
}
|
||||
|
||||
export function teacherRequestsUrlState(state) {
|
||||
return {
|
||||
requestQuery: state.query || '',
|
||||
requestStatus: state.status === 'PENDING' ? '' : state.status,
|
||||
requestPage: state.page || '',
|
||||
requestSize: state.size === 25 ? '' : state.size
|
||||
};
|
||||
}
|
||||
|
||||
// --- Справочники ---
|
||||
|
||||
export function fetchDepartments(client = apiClient) {
|
||||
return client.get('/api/departments', { errorMessage: 'Не удалось загрузить список кафедр' });
|
||||
}
|
||||
|
||||
// Кафедра в строке заявки подписывается только названием — как в legacy-реестре.
|
||||
export function requestDepartmentLabel(department) {
|
||||
return department.departmentName || department.name || department.id;
|
||||
}
|
||||
|
||||
// --- Реестр заявок ---
|
||||
|
||||
export function fetchTeacherRequestsPage(state, client = apiClient) {
|
||||
return client.get(teacherRequestsQueryParams(state), {
|
||||
errorMessage: 'Не удалось загрузить заявки преподавателей'
|
||||
});
|
||||
}
|
||||
|
||||
export function teacherRequestsEmptyMessage(state) {
|
||||
return state.query ? 'Заявки не найдены' : 'Заявок с выбранным статусом нет';
|
||||
}
|
||||
|
||||
export function reviewedStatusLabel(status) {
|
||||
return status === 'APPROVED' ? 'Одобрена' : 'Отклонена';
|
||||
}
|
||||
|
||||
// Счётчик в sidebar обновляется событием teacherRequestsChanged: он актуален
|
||||
// только когда реестр показывает нефильтрованный список ожидающих заявок.
|
||||
export function pendingCountDetail(state, totalItems) {
|
||||
return {
|
||||
count: state.status === 'PENDING' && !state.query
|
||||
? Number(totalItems || 0)
|
||||
: undefined
|
||||
};
|
||||
}
|
||||
|
||||
// --- Одобрение и отклонение ---
|
||||
|
||||
export function approvePayloadFromFields(fields) {
|
||||
const payload = {
|
||||
departmentId: Number(fields.departmentId),
|
||||
username: (fields.username || '').trim(),
|
||||
fullName: (fields.fullName || '').trim(),
|
||||
jobTitle: (fields.jobTitle || '').trim(),
|
||||
password: fields.password
|
||||
};
|
||||
if (!payload.departmentId || !payload.username || !payload.fullName || !payload.password) {
|
||||
return { error: 'Заполните кафедру, логин, ФИО и пароль' };
|
||||
}
|
||||
if (payload.password.length < 8) {
|
||||
return { error: 'Пароль должен быть не короче 8 символов' };
|
||||
}
|
||||
return { payload };
|
||||
}
|
||||
|
||||
export function approveRequest(id, payload, client = apiClient) {
|
||||
return client.post(
|
||||
`/api/teacher-requests/${encodeURIComponent(id)}/approve`,
|
||||
payload,
|
||||
{ errorMessage: 'Ошибка одобрения заявки' }
|
||||
);
|
||||
}
|
||||
|
||||
export function rejectRequest(id, reviewComment, client = apiClient) {
|
||||
return client.post(
|
||||
`/api/teacher-requests/${encodeURIComponent(id)}/reject`,
|
||||
{ reviewComment },
|
||||
{ errorMessage: 'Ошибка отклонения заявки' }
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user