Files
magistr/frontend/react/admin/department-workspace-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

263 lines
9.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';
import { defaultSchedulePeriod } from '../../admin/js/schedule-period.js';
import { toIsoDate } from '../shared/dates.js';
export const SUBJECT_TABLE_COLUMNS = 4;
export const DEPARTMENT_REQUEST_TABLE_COLUMNS = 6;
// --- Справочники и период ---
export function fetchDepartments(client = apiClient) {
return client.get('/api/departments', { errorMessage: 'Не удалось загрузить список кафедр' });
}
// Селектор кафедры кабинета подписывает варианты только названием кафедры.
export function departmentOptionLabel(department) {
return department.departmentName || department.name || department.id;
}
export function fetchSemesters(client = apiClient) {
return client.get('/api/schedule/semesters', { errorMessage: 'Не удалось загрузить семестры' });
}
export function defaultWorkloadPeriod(semesters, date = new Date()) {
return defaultSchedulePeriod(semesters, toIsoDate(date));
}
export function fallbackWorkloadPeriod(date = new Date()) {
const end = new Date(date);
end.setDate(date.getDate() + 7);
return { startDate: toIsoDate(date), endDate: toIsoDate(end) };
}
// --- Дисциплины кафедры ---
export function departmentSubjectsEndpoint(departmentId) {
return `/api/department/subjects?departmentId=${encodeURIComponent(departmentId || '')}`;
}
export function fetchDepartmentSubjects(departmentId, client = apiClient) {
return client.get(departmentSubjectsEndpoint(departmentId), {
errorMessage: 'Ошибка загрузки дисциплин'
});
}
export function importSubjectsPayload(rows, departmentId) {
if (!departmentId) return { error: 'Выберите кафедру' };
const payload = (rows || [])
.filter(item => item.name)
.map(item => ({
code: item.code,
name: item.name,
departmentId: Number(departmentId)
}));
if (!payload.length) return { error: 'Заполните название хотя бы одной дисциплины' };
return { payload };
}
export function importSubjects(rows, client = apiClient) {
return client.post('/api/department/subjects/import', rows, {
errorMessage: 'Ошибка загрузки дисциплин'
});
}
// --- Комментарии к дисциплинам ---
export function fetchSubjectComments(subjectId, client = apiClient) {
return client.get(`/api/department/subjects/${encodeURIComponent(subjectId)}/comments`, {
errorMessage: 'Ошибка загрузки комментариев'
});
}
export function saveSubjectComment(subjectId, comment, client = apiClient) {
return client.post(
`/api/department/subjects/${encodeURIComponent(subjectId)}/comments`,
{ comment },
{ errorMessage: 'Ошибка сохранения комментария' }
);
}
// --- Преподаватели и нагрузка ---
export function departmentTeachersEndpoint(departmentId) {
return `/api/department/teachers?departmentId=${encodeURIComponent(departmentId || '')}`;
}
export function fetchDepartmentTeachers(departmentId, client = apiClient) {
return client.get(departmentTeachersEndpoint(departmentId), {
errorMessage: 'Ошибка загрузки преподавателей'
});
}
export function fetchAllTeachers(client = apiClient) {
return client.get('/api/users/teachers', {
errorMessage: 'Ошибка загрузки преподавателей'
});
}
export function availableTeachers(allTeachers, assignedTeachers) {
const assignedIds = new Set((assignedTeachers || []).map(teacher => String(teacher.id)));
return (allTeachers || []).filter(teacher => !assignedIds.has(String(teacher.id)));
}
export function teacherOptionLabel(teacher) {
return teacher.fullName || teacher.username || `ID ${teacher.id}`;
}
export function assignTeacherError(departmentId, teacherId) {
if (!departmentId) return 'Выберите кафедру';
if (!teacherId) return 'Выберите преподавателя';
return null;
}
export function assignTeacher(teacherId, departmentId, comment, client = apiClient) {
return client.post(
`/api/department/teachers/${encodeURIComponent(teacherId)}/assignments`,
{
departmentId: Number(departmentId),
comment: comment || null
},
{ errorMessage: 'Ошибка привязки преподавателя' }
);
}
export function teacherWorkloadEndpoint({ startDate, endDate, departmentId }) {
const params = new URLSearchParams({ startDate, endDate });
if (departmentId) params.set('departmentId', departmentId);
return `/api/workload/teachers?${params}`;
}
export function fetchTeacherWorkload(params, client = apiClient) {
return client.get(teacherWorkloadEndpoint(params), {
errorMessage: 'Ошибка расчёта нагрузки'
});
}
// --- Заявки кафедры ---
export function departmentTeacherRequestsEndpoint(departmentId) {
return `/api/department/teacher-requests?departmentId=${encodeURIComponent(departmentId || '')}`;
}
export function fetchDepartmentTeacherRequests(departmentId, client = apiClient) {
return client.get(departmentTeacherRequestsEndpoint(departmentId), {
errorMessage: 'Ошибка загрузки заявок'
});
}
export function teacherRequestPayload(fields, departmentId) {
if (!departmentId) return { error: 'Выберите кафедру' };
const username = (fields.username || '').trim();
const fullName = (fields.fullName || '').trim();
if (!username || !fullName) return { error: 'Заполните логин и ФИО преподавателя' };
return {
payload: {
departmentId: Number(departmentId),
username,
fullName,
jobTitle: (fields.jobTitle || '').trim(),
comment: (fields.comment || '').trim()
}
};
}
export function submitTeacherRequest(payload, client = apiClient) {
return client.post('/api/department/teacher-requests', payload, {
errorMessage: 'Ошибка отправки заявки'
});
}
// --- Метки ---
export function subjectCountLabel(count) {
const tail = count % 100;
if (tail >= 11 && tail <= 14) return `${count} дисциплин`;
if (count % 10 === 1) return `${count} дисциплина`;
if ([2, 3, 4].includes(count % 10)) return `${count} дисциплины`;
return `${count} дисциплин`;
}
export function requestStatusLabel(status) {
return {
PENDING: 'На рассмотрении',
APPROVED: 'Одобрена',
REJECTED: 'Отклонена'
}[status] || status || '-';
}
export function requestStatusClass(status) {
return {
PENDING: 'badge-ef',
APPROVED: 'badge-student',
REJECTED: 'badge-admin'
}[status] || 'badge-ef';
}
export function formatDateTime(value) {
if (!value) return '-';
return String(value).replace('T', ' ').slice(0, 16);
}
// --- Сопоставление преподавателей и нагрузки ---
export function normalizeTeacherName(value) {
return value ? `name:${String(value).trim().toLowerCase()}` : null;
}
export function workloadKeys(item) {
return [
item.id == null ? null : `id:${item.id}`,
normalizeTeacherName(item.fullName || item.name || item.username)
].filter(Boolean);
}
export function workloadLabel(item, errorMessage) {
if (errorMessage) return errorMessage;
if (!item) return '0 ак. ч., 0 занятий за период';
return `${item.academicHours ?? 0} ак. ч., ${item.lessonCount ?? 0} занятий за период`;
}
// Воспроизводит legacy-логику сводки: преподаватели кафедры, затем записи
// нагрузки, не привязанные ни к одному преподавателю (есть занятия в расписании).
export function teacherMetrics(teachers, workload) {
const workloadByTeacher = new Map();
const usedWorkloadKeys = new Set();
(workload || [])
.filter(item => item && (item.id || item.name))
.forEach(item => {
workloadKeys(item).forEach(key => {
if (!workloadByTeacher.has(key)) {
workloadByTeacher.set(key, item);
}
});
});
const rows = (teachers || []).map(teacher => {
const workloadItem = workloadKeys(teacher)
.map(key => workloadByTeacher.get(key))
.find(Boolean);
if (workloadItem) {
workloadKeys(workloadItem).forEach(key => usedWorkloadKeys.add(key));
}
return {
name: teacher.fullName || teacher.username || '-',
meta: teacher.jobTitle || 'Должность не указана',
workload: workloadItem
};
});
(workload || [])
.filter(item => item && (item.id || item.name))
.filter(item => workloadKeys(item).every(key => !usedWorkloadKeys.has(key)))
.forEach(item => {
rows.push({
name: item.name || '-',
meta: 'Есть занятия в расписании кафедры',
workload: item
});
});
return rows;
}