Files
magistr/frontend/react/admin/subjects-service.js
dipatrik10 97bf3cc50a 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
2026-09-28 23:15:04 +03:00

154 lines
5.4 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 { apiClient } from '../shared/api-client.js';
export const SUBJECT_TABLE_COLUMNS = 6;
export const SUBJECT_PAGE_SIZES = [25, 50, 100];
export const SUBJECT_SORT_OPTIONS = Object.freeze([
{ value: 'name,asc', label: 'По названию А–Я' },
{ value: 'name,desc', label: 'По названию Я–А' },
{ value: 'code,asc', label: 'По коду' },
{ value: 'departmentId,asc', label: 'По кафедре' }
]);
const DEFAULT_STATE = Object.freeze({
query: '',
departmentId: '',
page: 0,
size: 25,
sort: 'name,asc'
});
export function normalizeSubjectsState(restored = {}) {
const size = Number(restored.subjectSize);
return {
query: restored.subjectQuery || '',
departmentId: restored.subjectDepartment || '',
page: Math.max(Number(restored.subjectPage) || 0, 0),
size: SUBJECT_PAGE_SIZES.includes(size) ? size : 25,
sort: SUBJECT_SORT_OPTIONS.some(option => option.value === restored.subjectSort)
? restored.subjectSort
: 'name,asc'
};
}
export function initialSubjectsState(restored = {}) {
return normalizeSubjectsState(restored);
}
export function defaultSubjectsState() {
return { ...DEFAULT_STATE };
}
export function subjectsQueryParams(state) {
const [sort, direction] = String(state.sort || 'name,asc').split(',');
const params = new URLSearchParams({
query: state.query || '',
page: String(state.page || 0),
size: String(state.size || 25),
sort,
direction
});
if (state.departmentId) params.set('departmentId', state.departmentId);
return `/api/subjects/page?${params}`;
}
export function subjectsUrlState(state) {
return {
subjectQuery: state.query || '',
subjectDepartment: state.departmentId || '',
subjectPage: state.page || '',
subjectSize: state.size === 25 ? '' : state.size,
subjectSort: state.sort === 'name,asc' ? '' : state.sort
};
}
export function fetchDepartments(client = apiClient) {
return client.get('/api/departments', { errorMessage: 'Не удалось загрузить список кафедр' });
}
export function fetchSubjectsPage(state, client = apiClient) {
return client.get(subjectsQueryParams(state), { errorMessage: 'Не удалось загрузить дисциплины' });
}
export function teacherAssignmentsEndpoint(subjectIds) {
const params = new URLSearchParams();
(subjectIds || []).forEach(subjectId => params.append('subjectId', subjectId));
return `/api/teacher-subjects?${params}`;
}
export function fetchTeacherAssignments(subjectIds, client = apiClient) {
if (!(subjectIds || []).length) return Promise.resolve([]);
return client.get(
teacherAssignmentsEndpoint(subjectIds),
{ errorMessage: 'Не удалось загрузить привязки преподавателей' }
);
}
export function createSubject({ name, code, departmentId }, client = apiClient) {
return client.post(
'/api/subjects',
{ name, code, departmentId: Number(departmentId) },
{ errorMessage: 'Ошибка создания' }
);
}
export function archiveSubject(id, client = apiClient) {
return client.delete(`/api/subjects/${id}`, { errorMessage: 'Ошибка архивирования' });
}
export function assignTeacher({ userId, subjectId }, client = apiClient) {
return client.post(
'/api/teacher-subjects',
{ userId: Number(userId), subjectId: Number(subjectId) },
{ errorMessage: 'Ошибка привязки' }
);
}
export function removeTeacherAssignment({ userId, subjectId }, client = apiClient) {
return client.delete('/api/teacher-subjects', {
json: { userId: Number(userId), subjectId: Number(subjectId) },
errorMessage: 'Ошибка удаления привязки'
});
}
export function teachersSearchEndpoint(query, selectedId) {
const params = new URLSearchParams({ query, limit: '30' });
if (selectedId) params.set('selectedId', selectedId);
return `/api/users/teachers/options?${params}`;
}
export function departmentLabel(departments, departmentId) {
const department = (departments || []).find(item => String(item.id) === String(departmentId));
if (!department) return departmentId || '-';
return department.departmentName || department.name || departmentId;
}
export function departmentOptionLabel(department) {
const name = department.departmentName || department.name || 'Без названия';
const code = department.departmentCode || department.code || department.id;
return `${name} (${code})`;
}
export function groupTeachersBySubject(assignments) {
const grouped = new Map();
(assignments || []).forEach(assignment => {
const key = String(assignment.subjectId);
if (!grouped.has(key)) grouped.set(key, []);
grouped.get(key).push(assignment);
});
return grouped;
}
export function subjectContextLabel(subject) {
return `${subject.code ? `${subject.code} · ` : ''}${subject.name}`;
}
export function subjectsEmptyMessage(state) {
return state.query || state.departmentId ? 'Дисциплины не найдены' : 'Дисциплин пока нет';
}
export function normalizePageState(state, page) {
const nextPage = Number(page);
return { ...state, page: Number.isFinite(nextPage) && nextPage > 0 ? nextPage : 0 };
}