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:
150
frontend/react/admin/university-structure-service.js
Normal file
150
frontend/react/admin/university-structure-service.js
Normal file
@@ -0,0 +1,150 @@
|
||||
import { apiClient } from '../shared/api-client.js';
|
||||
|
||||
export const DEPARTMENT_TABLE_COLUMNS = 4;
|
||||
export const SPECIALTY_TABLE_COLUMNS = 4;
|
||||
export const PROFILE_TABLE_COLUMNS = 5;
|
||||
export const LIST_PAGE_SIZES = [25, 50, 100];
|
||||
|
||||
// --- Загрузка ---
|
||||
|
||||
export function fetchDepartments(client = apiClient) {
|
||||
return client.get('/api/departments', { errorMessage: 'Не удалось загрузить кафедры' });
|
||||
}
|
||||
|
||||
export function fetchSpecialties(client = apiClient) {
|
||||
return client.get('/api/specialties', { errorMessage: 'Не удалось загрузить специальности' });
|
||||
}
|
||||
|
||||
export function fetchAllProfiles(client = apiClient) {
|
||||
return client.get('/api/specialties/profiles', { errorMessage: 'Не удалось загрузить профили обучения' });
|
||||
}
|
||||
|
||||
export function fetchProfiles(specId, client = apiClient) {
|
||||
return client.get(`/api/specialties/${specId}/profiles`, { errorMessage: 'Не удалось загрузить профили обучения' });
|
||||
}
|
||||
|
||||
// --- Кафедры ---
|
||||
|
||||
export function createDepartment({ name, code }, client = apiClient) {
|
||||
return client.post(
|
||||
'/api/departments',
|
||||
{ departmentName: name, departmentCode: Number(code) },
|
||||
{ errorMessage: 'Ошибка создания кафедры' }
|
||||
);
|
||||
}
|
||||
|
||||
export function updateDepartment(id, { name, code }, client = apiClient) {
|
||||
return client.put(
|
||||
`/api/departments/${id}`,
|
||||
{ departmentName: name, departmentCode: Number(code) },
|
||||
{ errorMessage: 'Ошибка обновления кафедры' }
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteDepartment(id, client = apiClient) {
|
||||
return client.delete(`/api/departments/${id}`, { errorMessage: 'Ошибка удаления кафедры' });
|
||||
}
|
||||
|
||||
// --- Специальности ---
|
||||
|
||||
export function createSpecialty({ name, code }, client = apiClient) {
|
||||
return client.post(
|
||||
'/api/specialties',
|
||||
{ specialityName: name, specialityCode: code },
|
||||
{ errorMessage: 'Ошибка создания специальности' }
|
||||
);
|
||||
}
|
||||
|
||||
export function updateSpecialty(id, { name, code }, client = apiClient) {
|
||||
return client.put(
|
||||
`/api/specialties/${id}`,
|
||||
{ specialityName: name, specialityCode: code },
|
||||
{ errorMessage: 'Ошибка обновления специальности' }
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteSpecialty(id, client = apiClient) {
|
||||
return client.delete(`/api/specialties/${id}`, { errorMessage: 'Ошибка удаления специальности' });
|
||||
}
|
||||
|
||||
// --- Профили ---
|
||||
|
||||
export function createProfile(specId, { name, description }, client = apiClient) {
|
||||
return client.post(
|
||||
`/api/specialties/${specId}/profiles`,
|
||||
{ name, description },
|
||||
{ errorMessage: 'Ошибка сохранения профиля' }
|
||||
);
|
||||
}
|
||||
|
||||
export function updateProfile(specId, profileId, { name, description }, client = apiClient) {
|
||||
return client.put(
|
||||
`/api/specialties/${specId}/profiles/${profileId}`,
|
||||
{ id: Number(profileId), name, description },
|
||||
{ errorMessage: 'Ошибка сохранения профиля' }
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteProfile(specId, profileId, client = apiClient) {
|
||||
return client.delete(`/api/specialties/${specId}/profiles/${profileId}`, { errorMessage: 'Ошибка удаления профиля' });
|
||||
}
|
||||
|
||||
// --- Клиентская фильтрация и пагинация списков (как в legacy listPage) ---
|
||||
|
||||
export function filterListItems(items, query, fields) {
|
||||
const normalized = String(query || '').trim().toLocaleLowerCase('ru');
|
||||
if (!normalized) return [...(items || [])];
|
||||
return (items || []).filter(item =>
|
||||
fields(item).join(' ').toLocaleLowerCase('ru').includes(normalized));
|
||||
}
|
||||
|
||||
export function clampPage(page, totalPages) {
|
||||
return Math.min(Math.max(Number(page) || 0, 0), Math.max(0, totalPages - 1));
|
||||
}
|
||||
|
||||
export function pageSlice(items, page, size) {
|
||||
const pageSize = Number(size) || 25;
|
||||
return (items || []).slice(page * pageSize, (page + 1) * pageSize);
|
||||
}
|
||||
|
||||
export function departmentFields(department) {
|
||||
return [department.id, department.departmentName || department.name, department.departmentCode ?? department.code];
|
||||
}
|
||||
|
||||
export function specialtyFields(speciality) {
|
||||
return [
|
||||
speciality.id,
|
||||
speciality.specialityName || speciality.name,
|
||||
speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code
|
||||
];
|
||||
}
|
||||
|
||||
export function profileFields(profile) {
|
||||
return [profile.id, profile.name, profile.description, profile.specialityCode, profile.specialityName];
|
||||
}
|
||||
|
||||
// --- Метки ---
|
||||
|
||||
export function departmentName(department) {
|
||||
return department.departmentName || department.name;
|
||||
}
|
||||
|
||||
export function departmentCode(department) {
|
||||
return department.departmentCode ?? department.code;
|
||||
}
|
||||
|
||||
export function specialtyName(speciality) {
|
||||
return speciality.specialityName || speciality.name;
|
||||
}
|
||||
|
||||
export function specialtyCode(speciality) {
|
||||
return speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code;
|
||||
}
|
||||
|
||||
export function specialtyOptionLabel(speciality) {
|
||||
return `${specialtyCode(speciality)} · ${specialtyName(speciality)}`;
|
||||
}
|
||||
|
||||
export function profileSpecialtyLabel(profile) {
|
||||
return `${profile.specialityCode} · ${profile.specialityName}`;
|
||||
}
|
||||
Reference in New Issue
Block a user