Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на 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
119 lines
4.3 KiB
JavaScript
119 lines
4.3 KiB
JavaScript
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: 'Ошибка отклонения заявки' }
|
||
);
|
||
}
|