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:
dipatrik10
2026-09-28 23:15:04 +03:00
parent c6083b35e0
commit 97bf3cc50a
167 changed files with 31283 additions and 19033 deletions

View File

@@ -0,0 +1,87 @@
import { apiClient } from '../shared/api-client.js';
export async function fetchEquipments(client = apiClient) {
try {
return await client.get('/api/equipments');
} catch {
console.error('Не удалось загрузить список оборудования');
return [];
}
}
export function fetchClassrooms(client = apiClient) {
return client.get(
'/api/classrooms?includeArchived=true',
{ errorMessage: 'Не удалось загрузить список аудиторий' }
);
}
export function createClassroom({ name, capacity, equipmentIds }, client = apiClient) {
return client.post(
'/api/classrooms',
{ name, capacity, equipmentIds, isAvailable: true },
{ errorMessage: 'Ошибка создания' }
);
}
export function updateClassroom(id, payload, client = apiClient) {
return client.put(`/api/classrooms/${id}`, payload, { errorMessage: 'Ошибка обновления' });
}
export function toggleClassroomAvailability(id, isAvailable, client = apiClient) {
return updateClassroom(id, { isAvailable }, client);
}
export function archiveClassroom(id, client = apiClient) {
return client.delete(`/api/classrooms/${id}`, { errorMessage: 'Не удалось архивировать аудиторию' });
}
export function restoreClassroom(id, client = apiClient) {
return client.post(
`/api/classrooms/${id}/restore`,
{},
{ errorMessage: 'Не удалось восстановить аудиторию' }
);
}
export function createEquipment(name, client = apiClient) {
return client.post('/api/equipments', { name }, { errorMessage: 'Ошибка добавления' });
}
export function deleteEquipment(id, client = apiClient) {
return client.delete(`/api/equipments/${id}`, { errorMessage: 'Не удалось удалить оборудование' });
}
export function parseCapacity(value) {
return parseInt(value, 10);
}
export function activeClassrooms(classrooms) {
return (classrooms || []).filter(classroom => classroom.isAvailable && classroom.status !== 'ARCHIVED');
}
export function classroomStatusLabel(classroom) {
if (classroom.status === 'ARCHIVED') return 'Архив';
return classroom.isAvailable ? 'Доступна' : 'Не доступна';
}
export function equipmentNamesText(classroom) {
const names = (classroom.equipments || []).map(equipment => equipment.name);
return names.length ? names.join(', ') : null;
}
export function equipmentSelectLabel(selectedNames) {
const names = selectedNames || [];
if (names.length === 0) return 'Выберите оборудование...';
if (names.length === 1) return names[0];
return `Выбрано: ${names.length}`;
}
export function equipmentIdSelection(equipments, selectedIds) {
const selected = new Set((selectedIds || []).map(String));
return (equipments || []).map(equipment => ({
id: equipment.id,
name: equipment.name,
checked: selected.has(String(equipment.id))
}));
}