Files
magistr/frontend/react/settings/settings-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

214 lines
7.7 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 { capabilitiesForRole } from '../../admin/js/role-capabilities.js';
export const SETTINGS_TABS = Object.freeze([
{ id: 'general', title: 'Общие настройки' },
{ id: 'time-slots', title: 'Временные слоты' },
{ id: 'edu-forms', title: 'Формы обучения' },
{ id: 'database', title: 'База данных' }
]);
const APPLY_MODE_LABELS = Object.freeze({
DEFAULT: 'применяется по умолчанию',
WEEKDAY: 'применяется автоматически',
MANUAL: 'применяется вручную'
});
const DAY_SHORT_LABELS = Object.freeze({
1: 'Пн',
2: 'Вт',
3: 'Ср',
4: 'Чт',
5: 'Пт',
6: 'Сб',
7: 'Вс'
});
export function settingsTabsForRole(role) {
const capabilities = capabilitiesForRole(role);
if (!capabilities || capabilities.settingsTabs.length === 0) return [];
const allowed = new Set(capabilities.settingsTabs);
return SETTINGS_TABS.filter(tab => allowed.has(tab.id));
}
export function defaultSettingsTabForRole(role) {
const capabilities = capabilitiesForRole(role);
return capabilities ? capabilities.defaultSettingsTab : null;
}
export function applyModeLabel(mode) {
return APPLY_MODE_LABELS[mode] || mode || '—';
}
export function dayShort(dayOfWeek) {
return DAY_SHORT_LABELS[Number(dayOfWeek)] || '-';
}
export function scopeOptionLabel(scope) {
const suffix = scope.applyMode === 'WEEKDAY'
? `, ${dayShort(scope.dayOfWeek)}`
: '';
return `${scope.name} - ${applyModeLabel(scope.applyMode)}${suffix}`;
}
export function scopeBadgeLabel(scope) {
if (scope.applyMode === 'DEFAULT') return 'Базовая';
if (scope.applyMode === 'WEEKDAY') return dayShort(scope.dayOfWeek);
return 'Ручная';
}
export function scopeBadgeClass(scope) {
return scope.applyMode === 'DEFAULT' ? 'scope-badge scope-badge-base' : 'scope-badge';
}
export function scopeSummaryText(scope, slots) {
const mode = applyModeLabel(scope.applyMode);
const day = scope.applyMode === 'WEEKDAY' ? `, ${dayShort(scope.dayOfWeek)}` : '';
const slotCount = slotsForScope(slots, scope.id).length;
return `${mode}${day}. Слотов: ${slotCount}.`;
}
export function isSystemScope(scope) {
return Boolean(scope && scope.systemScope);
}
export function defaultScopeId(scopes) {
const found = (scopes || []).find(scope => scope.applyMode === 'DEFAULT');
return found ? found.id : (scopes || [])[0]?.id || null;
}
export function resolveActiveScopeId(scopes, preferredId) {
const list = scopes || [];
if (preferredId && list.some(scope => scope.id === preferredId)) return preferredId;
return defaultScopeId(list);
}
export function slotsForScope(slots, scopeId) {
return (slots || [])
.filter(slot => slot.scopeId === scopeId)
.sort((left, right) => (left.orderNumber || 0) - (right.orderNumber || 0));
}
export function validateSlotPayload({ scopeId, orderNumber, startTime = '', endTime = '' } = {}) {
if (!scopeId) return 'Выберите сетку времени';
if (!orderNumber || orderNumber <= 0) return 'Укажите номер пары';
if (!startTime || !endTime) return 'Укажите время начала и окончания';
if (startTime >= endTime) return 'Время начала должно быть раньше времени окончания';
return '';
}
export function slotDurationMinutes(startTime = '', endTime = '') {
if (!startTime || !endTime || startTime >= endTime) return null;
const [startHour, startMinute] = startTime.split(':').map(Number);
const [endHour, endMinute] = endTime.split(':').map(Number);
return (endHour * 60 + endMinute) - (startHour * 60 + startMinute);
}
export function buildSlotPayload({ id = '', orderNumber, scopeId, startTime, endTime }) {
return {
id: id ? Number(id) : null,
orderNumber: Number(orderNumber),
scopeId,
startTime,
endTime
};
}
export function validateTenantFields({ name = '', domain = '', url = '' } = {}) {
if (!name.trim() || !domain.trim() || !url.trim()) {
return 'Заполните все обязательные поля';
}
return '';
}
export function normalizeDomain(domain = '') {
return domain.trim().toLowerCase();
}
export async function fetchScopes(client = apiClient) {
return client.get('/api/admin/time-slots/scopes', { errorMessage: 'Не удалось загрузить сетки времени' });
}
export async function createScope(name, client = apiClient) {
return client.post(
'/api/admin/time-slots/scopes',
{ name },
{ errorMessage: 'Не удалось создать сетку времени' }
);
}
export async function deleteScope(id, client = apiClient) {
return client.delete(
`/api/admin/time-slots/scopes/${encodeURIComponent(id)}`,
{ errorMessage: 'Не удалось удалить сетку времени' }
);
}
export async function fetchTimeSlots(client = apiClient) {
return client.get('/api/admin/time-slots', { errorMessage: 'Не удалось загрузить временные слоты' });
}
export async function saveTimeSlot(payload, client = apiClient) {
const options = { errorMessage: 'Не удалось сохранить временной слот' };
return payload.id
? client.put(`/api/admin/time-slots/${encodeURIComponent(payload.id)}`, payload, options)
: client.post('/api/admin/time-slots', payload, options);
}
export async function deleteTimeSlot(id, client = apiClient) {
return client.delete(
`/api/admin/time-slots/${encodeURIComponent(id)}`,
{ errorMessage: 'Не удалось удалить временной слот' }
);
}
export async function fetchEducationForms(client = apiClient) {
return client.get('/api/education-forms', { errorMessage: 'Не удалось загрузить формы обучения' });
}
export async function createEducationForm(name, client = apiClient) {
return client.post(
'/api/education-forms',
{ name },
{ errorMessage: 'Не удалось создать форму обучения' }
);
}
export async function deleteEducationForm(id, client = apiClient) {
return client.delete(
`/api/education-forms/${encodeURIComponent(id)}`,
{ errorMessage: 'Не удалось удалить форму обучения' }
);
}
export async function fetchDatabaseStatus(client = apiClient) {
return client.get('/api/database/status', { errorMessage: 'Не удалось загрузить статус подключения' });
}
export async function fetchTenants(client = apiClient) {
return client.get('/api/database/tenants', { errorMessage: 'Не удалось загрузить список тенантов' });
}
export async function testTenantConnection({ url, username, password }, client = apiClient) {
return client.post(
'/api/database/test',
{ password, url, username },
{ errorMessage: 'Не удалось проверить подключение' }
);
}
export async function createTenant({ name, domain, url, username, password }, client = apiClient) {
return client.post(
'/api/database/tenants',
{ domain, name, password, url, username },
{ errorMessage: 'Не удалось добавить тенант' }
);
}
export async function deleteTenant(domain, client = apiClient) {
return client.delete(
`/api/database/tenants/${encodeURIComponent(domain)}`,
{ errorMessage: 'Не удалось удалить тенант' }
);
}