import { apiClient } from '../shared/api-client.js'; import { buildAcademicCalendarTitle } from '../../admin/js/views/academic-calendar-title.js'; export const GROUP_TABLE_COLUMNS = 10; export const GROUP_PAGE_SIZES = [25, 50, 100]; export const GROUP_SORT_OPTIONS = Object.freeze([ { value: 'name,asc', label: 'По названию А–Я' }, { value: 'name,desc', label: 'По названию Я–А' }, { value: 'yearStartStudy,desc', label: 'Сначала новые наборы' }, { value: 'groupSize,desc', label: 'Сначала крупные' }, { value: 'status,asc', label: 'По статусу' } ]); export const SUBGROUP_COUNT_OPTIONS = Object.freeze([ { value: '0', label: 'Без разделения на подгруппы' }, { value: '2', label: 'Две подгруппы' }, { value: '3', label: 'Три подгруппы' } ]); export const FILTER_EF_EMPTY_TEXT = 'Выберите форму обучения'; export function normalizeGroupsState(restored = {}) { const size = Number(restored.groupSize); return { query: restored.groupQuery || '', page: Math.max(Number(restored.groupPage) || 0, 0), size: GROUP_PAGE_SIZES.includes(size) ? size : 25, sort: GROUP_SORT_OPTIONS.some(option => option.value === restored.groupSort) ? restored.groupSort : 'name,asc', educationFormIds: Array.isArray(restored.groupEducationForm) ? restored.groupEducationForm.map(String) : [] }; } export function initialGroupsState(restored = {}) { return normalizeGroupsState(restored); } export function groupsQueryParams(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, includeArchived: 'true' }); (state.educationFormIds || []).forEach(id => params.append('educationFormId', id)); return `/api/groups/page?${params}`; } export function groupsUrlState(state) { return { groupQuery: state.query || '', groupPage: state.page || '', groupSize: state.size === 25 ? '' : state.size, groupSort: state.sort === 'name,asc' ? '' : state.sort, groupEducationForm: state.educationFormIds || [] }; } // --- Справочники --- export function fetchEducationForms(client = apiClient) { // legacy молча подставляет пустой список при сбое загрузки форм обучения return client.get('/api/education-forms').catch(() => []); } export function fetchDepartments(client = apiClient) { return client.get('/api/departments', { errorMessage: 'Не удалось загрузить список кафедр' }); } export function fetchSpecialties(client = apiClient) { return client.get('/api/specialties', { errorMessage: 'Не удалось загрузить список специальностей' }); } export function fetchAcademicYears(client = apiClient) { return client.get('/api/admin/calendar/years', { errorMessage: 'Не удалось загрузить учебные годы' }); } export async function fetchProfilesForSpecialties(specialties, client = apiClient) { const pairs = await Promise.all((specialties || []).map(async (speciality) => { const profiles = await client.get( `/api/specialties/${speciality.id}/profiles`, { errorMessage: 'Не удалось загрузить профили обучения' } ); return [String(speciality.id), profiles || []]; })); return new Map(pairs); } // --- Реестр групп --- export function fetchGroupsPage(state, client = apiClient) { return client.get(groupsQueryParams(state), { errorMessage: 'Не удалось загрузить группы' }); } export function groupPayloadFromFields(fields) { const checks = [ ['name', 'Введите название группы'], ['groupSize', 'Введите размер группы'], ['educationFormId', 'Выберите форму обучения'], ['departmentId', 'Выберите кафедру'], ['yearStartStudy', 'Введите год начала обучения'], ['specialtyId', 'Выберите специальность'], ['specialtyProfileId', 'Выберите профиль обучения'] ]; for (const [field, message] of checks) { if (!fields[field]) return { error: message }; } return { payload: { name: fields.name, groupSize: Number(fields.groupSize), educationFormId: Number(fields.educationFormId), departmentId: Number(fields.departmentId), yearStartStudy: Number(fields.yearStartStudy), specialtyId: Number(fields.specialtyId), specialtyProfileId: Number(fields.specialtyProfileId) } }; } export function createGroup(payload, client = apiClient) { return client.post('/api/groups', payload, { errorMessage: 'Ошибка создания' }); } export function updateGroup(id, payload, client = apiClient) { return client.put(`/api/groups/${id}`, payload, { errorMessage: 'Ошибка обновления' }); } export function archiveGroup(id, client = apiClient) { return client.delete(`/api/groups/${id}`, { errorMessage: 'Ошибка архивирования' }); } export function restoreGroup(id, client = apiClient) { return client.post(`/api/groups/${id}/restore`, {}, { errorMessage: 'Ошибка восстановления' }); } // --- Подгруппы --- export function fetchSubgroups(groupId, client = apiClient) { return client.get(`/api/groups/${groupId}/subgroups`, { errorMessage: 'Не удалось загрузить подгруппы' }); } export function createSubgroup(groupId, payload, client = apiClient) { return client.post(`/api/groups/${groupId}/subgroups`, payload, { errorMessage: 'Ошибка сохранения подгруппы' }); } export function updateSubgroup(groupId, id, payload, client = apiClient) { return client.put(`/api/groups/${groupId}/subgroups/${id}`, payload, { errorMessage: 'Ошибка сохранения подгруппы' }); } export function deleteSubgroup(groupId, id, client = apiClient) { return client.delete(`/api/groups/${groupId}/subgroups/${id}`, { errorMessage: 'Ошибка удаления подгруппы' }); } export function naturalSubgroupOrder(name) { const number = String(name || '').match(/\d+/)?.[0]; return number ? String(number).padStart(3, '0') : String(name || ''); } export function sortSubgroups(subgroups) { return [...(subgroups || [])].sort((left, right) => naturalSubgroupOrder(left.name).localeCompare(naturalSubgroupOrder(right.name), 'ru')); } export function suggestedSubgroupCapacities(groupSize, count) { if (!groupSize || !count) return []; const base = Math.floor(groupSize / count); const remainder = groupSize % count; return Array.from({ length: count }, (_, index) => base + (index < remainder ? 1 : 0)); } export function validateSubgroupCapacities(capacities, groupSize, count) { if (capacities.length !== count || capacities.some(value => !Number.isInteger(value) || value <= 0)) { return 'Укажите положительную численность для каждой подгруппы'; } const total = capacities.reduce((sum, value) => sum + value, 0); if (total !== groupSize) { return `Сумма численностей подгрупп должна быть равна численности группы (${groupSize} чел.)`; } return null; } // Синхронизация подгрупп воспроизводит legacy-порядок: сначала удаляем лишние, // затем обновляем существующие (сначала уменьшающие вместимость), затем создаём новые. export async function syncSubgroups(groupId, existingSubgroups, capacities, client = apiClient) { const targetCount = capacities.length; const extraSubgroups = existingSubgroups.slice(targetCount); for (const subgroup of extraSubgroups) { await deleteSubgroup(groupId, subgroup.id, client); } const updates = []; const creates = []; for (let index = 0; index < targetCount; index += 1) { const payload = { name: `Подгруппа ${index + 1}`, studentCapacity: capacities[index] }; const existing = existingSubgroups[index]; if (existing) { updates.push({ existing, payload }); } else { creates.push(payload); } } updates.sort((left, right) => { const leftIncreases = left.payload.studentCapacity > Number(left.existing.studentCapacity || 0); const rightIncreases = right.payload.studentCapacity > Number(right.existing.studentCapacity || 0); return Number(leftIncreases) - Number(rightIncreases); }); for (const update of updates) { await updateSubgroup(groupId, update.existing.id, update.payload, client); } for (const payload of creates) { await createSubgroup(groupId, payload, client); } } // --- Календарные графики --- export function calendarsOptionsEndpoint({ query, academicYearId, specialtyId, profileId, studyFormId, selectedId }) { const params = new URLSearchParams({ query, academicYearId, specialtyId, profileId, studyFormId, limit: '30' }); if (selectedId) params.set('selectedId', selectedId); return `/api/admin/academic-calendars/options?${params}`; } export function fetchCalendarsOptions(params, client = apiClient) { return client.get(calendarsOptionsEndpoint(params), { errorMessage: 'Не удалось загрузить календарные графики' }); } export function fetchCalendarAssignments(groupId, client = apiClient) { return client.get(`/api/groups/${groupId}/calendar-assignments`, { errorMessage: 'Не удалось загрузить назначения графиков' }); } export function saveCalendarAssignment(groupId, { academicYearId, calendarId }, client = apiClient) { return client.put( `/api/groups/${groupId}/calendar-assignments`, { academicYearId: Number(academicYearId), calendarId: Number(calendarId) }, { errorMessage: 'Ошибка назначения графика' } ); } export function deleteCalendarAssignment(groupId, id, client = apiClient) { return client.delete(`/api/groups/${groupId}/calendar-assignments/${id}`, { errorMessage: 'Ошибка удаления назначения' }); } // --- Фильтр форм обучения --- export function efFilterLabel(educationForms, selectedIds) { const ids = selectedIds || []; if (!ids.length) return 'Все'; if (ids.length === 1) { const form = (educationForms || []).find(item => String(item.id) === ids[0]); return form?.name || FILTER_EF_EMPTY_TEXT; } return `Выбрано: ${ids.length}`; } // --- Метки --- 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 specialityLabel(specialties, specialityId) { const speciality = (specialties || []).find(item => String(item.id) === String(specialityId)); if (!speciality) return specialityId || '-'; const code = speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code || speciality.id; const name = speciality.specialityName || speciality.name || ''; return name ? `${code} — ${name}` : code; } export function specialityOptionLabel(speciality) { const code = speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code; const name = speciality.specialityName || speciality.name; return `${code} · ${name}`; } export function subjectLabel(subject) { const code = subject.subjectCode ? `${subject.subjectCode} · ` : ''; return `${code}${subject.subjectName || subject.subjectId || 'Без названия'}`; } export function statusLabel(group) { if (group.status === 'ARCHIVED') return 'Архив'; return group.studyStateName || (group.active ? 'Активна' : 'Неактивна'); } export function statusBadgeClass(group) { if (group.status === 'ARCHIVED' || group.studyState === 'GRADUATED' || group.studyState === 'INACTIVE') { return 'badge-unavailable'; } if (group.studyState === 'NOT_STARTED') { return 'badge-ef'; } return 'badge-available'; } export function calendarOptionLabel(calendar) { return buildAcademicCalendarTitle(calendar) || calendar.title || `График ${calendar.id}`; } export function calendarOptionDescription(calendar) { return `${calendar.academicYearTitle || ''} · ${calendar.studyFormName || ''}`; } export function assignmentCalendarTitle(assignment) { return assignment.calendarTitle || '-'; } export function assignmentSubjectsGroups(subjects) { const grouped = new Map(); (subjects || []).forEach(subject => { const key = String(subject.semesterNumber || '-'); if (!grouped.has(key)) grouped.set(key, []); grouped.get(key).push(subject); }); return Array.from(grouped.entries()) .sort(([a], [b]) => Number(a) - Number(b)) .map(([semesterNumber, items]) => ({ semesterNumber, items })); } export function groupsEmptyMessage(state) { return state.query || (state.educationFormIds || []).length ? 'По заданным условиям группы не найдены' : 'Групп пока нет'; }