Админка, настройки, логин, кабинеты преподавателя и студента полностью переведены на 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
154 lines
5.4 KiB
JavaScript
154 lines
5.4 KiB
JavaScript
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 };
|
||
}
|