Много разных изменений
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
import { buildAcademicCalendarTitle } from './academic-calendar-title.js';
|
||||
import { AsyncCombobox } from '../async-combobox.js';
|
||||
import { alertAction, confirmAction } from '../dialog.js';
|
||||
import { renderPagination } from '../pagination.js';
|
||||
import { debounce, readUrlState, readUrlValues, writeUrlState } from '../url-state.js';
|
||||
import { markTableReady, renderTableEmpty, renderTableError, renderTableLoading } from '../view-state.js';
|
||||
import { trackUxEvent } from '../../../telemetry.js';
|
||||
|
||||
async function fetchEducationForms() {
|
||||
try {
|
||||
@@ -24,6 +30,10 @@ export async function initGroups() {
|
||||
const filterEfMenu = document.getElementById('filter-ef-menu');
|
||||
const filterEfText = document.getElementById('filter-ef-text');
|
||||
const filterEfCheckboxes = document.getElementById('filter-ef-checkboxes');
|
||||
const searchInput = document.getElementById('groups-search');
|
||||
const sortSelect = document.getElementById('groups-sort');
|
||||
const filterResetButton = document.getElementById('groups-filter-reset');
|
||||
const pagination = document.getElementById('groups-pagination');
|
||||
|
||||
// Модалка группы
|
||||
const modalEditGroup = document.getElementById('modal-edit-group');
|
||||
@@ -65,9 +75,19 @@ export async function initGroups() {
|
||||
let specialties = [];
|
||||
let profilesBySpecialty = new Map();
|
||||
let academicYears = [];
|
||||
let calendars = [];
|
||||
let currentAssignments = [];
|
||||
let activeSubgroupGroup = null;
|
||||
let calendarCombobox = null;
|
||||
const restored = readUrlState({ groupQuery: '', groupPage: '0', groupSize: '25', groupSort: 'name,asc' });
|
||||
const pageState = {
|
||||
query: restored.groupQuery,
|
||||
page: Math.max(Number(restored.groupPage) || 0, 0),
|
||||
size: [25, 50, 100].includes(Number(restored.groupSize)) ? Number(restored.groupSize) : 25,
|
||||
sort: restored.groupSort,
|
||||
educationFormIds: readUrlValues('groupEducationForm')
|
||||
};
|
||||
searchInput.value = pageState.query;
|
||||
sortSelect.value = pageState.sort;
|
||||
|
||||
bindEvents();
|
||||
await loadInitialData();
|
||||
@@ -89,7 +109,30 @@ export async function initGroups() {
|
||||
|
||||
// Модалка календарей
|
||||
manageCalendarForm.addEventListener('submit', saveAssignment);
|
||||
manageCalendarYearSelect.addEventListener('change', populateCalendarSelect);
|
||||
manageCalendarYearSelect.addEventListener('change', () => {
|
||||
calendarCombobox?.clear();
|
||||
populateCalendarSelect();
|
||||
});
|
||||
searchInput.addEventListener('input', debounce(() => {
|
||||
pageState.query = searchInput.value.trim();
|
||||
pageState.page = 0;
|
||||
loadGroups();
|
||||
}));
|
||||
sortSelect.addEventListener('change', () => {
|
||||
pageState.sort = sortSelect.value;
|
||||
pageState.page = 0;
|
||||
loadGroups();
|
||||
});
|
||||
filterResetButton.addEventListener('click', () => {
|
||||
pageState.query = '';
|
||||
pageState.page = 0;
|
||||
pageState.sort = 'name,asc';
|
||||
pageState.educationFormIds = [];
|
||||
searchInput.value = '';
|
||||
sortSelect.value = pageState.sort;
|
||||
renderFilterEfCheckboxes(educationForms);
|
||||
loadGroups();
|
||||
});
|
||||
|
||||
groupsTbody.addEventListener('click', handleGroupTableClick);
|
||||
|
||||
@@ -143,20 +186,19 @@ export async function initGroups() {
|
||||
|
||||
async function loadInitialData() {
|
||||
try {
|
||||
[educationForms, departments, specialties, academicYears, calendars] = await Promise.all([
|
||||
[educationForms, departments, specialties, academicYears] = await Promise.all([
|
||||
fetchEducationForms(),
|
||||
api.get('/api/departments'),
|
||||
api.get('/api/specialties'),
|
||||
api.get('/api/admin/calendar/years'),
|
||||
api.get('/api/admin/academic-calendars')
|
||||
api.get('/api/admin/calendar/years')
|
||||
]);
|
||||
await loadProfiles();
|
||||
populateEfSelects(educationForms);
|
||||
populateDepartmentSelects(departments);
|
||||
populateSpecialitySelects(specialties);
|
||||
populateYearSelect();
|
||||
initCalendarCombobox();
|
||||
await loadGroups();
|
||||
await loadSubgroups();
|
||||
} catch (error) {
|
||||
groupsTbody.innerHTML = `<tr><td colspan="10" class="loading-row">Ошибка загрузки данных: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
@@ -171,28 +213,50 @@ export async function initGroups() {
|
||||
}
|
||||
|
||||
async function loadGroups() {
|
||||
renderTableLoading(groupsTbody, 10, 'Загрузка групп...');
|
||||
try {
|
||||
allGroups = await api.get('/api/groups?includeArchived=true');
|
||||
applyGroupFilter();
|
||||
const [sort, direction] = pageState.sort.split(',');
|
||||
const params = new URLSearchParams({
|
||||
query: pageState.query,
|
||||
page: String(pageState.page),
|
||||
size: String(pageState.size),
|
||||
sort,
|
||||
direction,
|
||||
includeArchived: 'true'
|
||||
});
|
||||
pageState.educationFormIds.forEach(id => params.append('educationFormId', id));
|
||||
const result = await api.get(`/api/groups/page?${params}`);
|
||||
allGroups = result.items || [];
|
||||
if (!allGroups.length && result.totalItems > 0 && pageState.page > 0) {
|
||||
pageState.page = Math.max(0, result.totalPages - 1);
|
||||
return loadGroups();
|
||||
}
|
||||
renderGroups(allGroups);
|
||||
renderPagination(pagination, result, {
|
||||
label: 'групп',
|
||||
onPage: page => { pageState.page = page; loadGroups(); },
|
||||
onSize: size => { pageState.size = size; pageState.page = 0; loadGroups(); }
|
||||
});
|
||||
persistPageState();
|
||||
trackUxEvent('реестр_групп_загружен', { total: Number(result.totalItems || 0), page: pageState.page });
|
||||
} catch (error) {
|
||||
groupsTbody.innerHTML = `<tr><td colspan="10" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
renderTableError(groupsTbody, 10, error.message || 'Не удалось загрузить группы', loadGroups);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubgroups() {
|
||||
async function loadSubgroups(groupId) {
|
||||
try {
|
||||
subgroups = await api.get('/api/subgroups');
|
||||
subgroups = groupId ? await api.get(`/api/groups/${groupId}/subgroups`) : [];
|
||||
} catch (error) {
|
||||
console.error('Не удалось загрузить подгруппы');
|
||||
subgroups = [];
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function applyGroupFilter() {
|
||||
const selectedIds = selectedFilterEducationFormIds();
|
||||
const filtered = selectedIds.length
|
||||
? allGroups.filter(group => selectedIds.includes(String(group.educationFormId)))
|
||||
: allGroups;
|
||||
renderGroups(filtered);
|
||||
pageState.educationFormIds = selectedFilterEducationFormIds();
|
||||
pageState.page = 0;
|
||||
loadGroups();
|
||||
}
|
||||
|
||||
function handleFilterEfChange(event) {
|
||||
@@ -247,7 +311,9 @@ export async function initGroups() {
|
||||
|
||||
function renderGroups(groups) {
|
||||
if (!groups || !groups.length) {
|
||||
groupsTbody.innerHTML = '<tr><td colspan="10" class="loading-row">Нет групп</td></tr>';
|
||||
renderTableEmpty(groupsTbody, 10, pageState.query || pageState.educationFormIds.length
|
||||
? 'По заданным условиям группы не найдены'
|
||||
: 'Групп пока нет');
|
||||
return;
|
||||
}
|
||||
groupsTbody.innerHTML = groups.map(group => `
|
||||
@@ -273,6 +339,7 @@ export async function initGroups() {
|
||||
</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
markTableReady(groupsTbody);
|
||||
}
|
||||
|
||||
function populateEfSelects(forms) {
|
||||
@@ -282,7 +349,7 @@ export async function initGroups() {
|
||||
}
|
||||
|
||||
function renderFilterEfCheckboxes(forms) {
|
||||
const selectedIds = new Set(selectedFilterEducationFormIds());
|
||||
const selectedIds = new Set(pageState.educationFormIds);
|
||||
const allChecked = !selectedIds.size;
|
||||
filterEfCheckboxes.innerHTML = `
|
||||
<label class="checkbox-item">
|
||||
@@ -372,23 +439,40 @@ export async function initGroups() {
|
||||
}
|
||||
|
||||
function populateCalendarSelect() {
|
||||
const groupId = manageCalendarGroupIdInput.value;
|
||||
const group = allGroups.find(g => g.id == groupId);
|
||||
const yearId = manageCalendarYearSelect.value;
|
||||
const matching = group && yearId
|
||||
? calendars.filter(calendar =>
|
||||
String(calendar.academicYearId) === String(yearId)
|
||||
&& String(calendar.specialtyId) === String(group.specialtyId || group.specialityCode)
|
||||
&& String(calendar.specialtyProfileId) === String(group.specialtyProfileId)
|
||||
&& String(calendar.studyFormId) === String(group.educationFormId)
|
||||
)
|
||||
: [];
|
||||
manageCalendarSelect.innerHTML = matching.length
|
||||
? '<option value="">Выберите график</option>' + matching.map(calendar =>
|
||||
`<option value="${calendar.id}">${escapeHtml(buildAcademicCalendarTitle(calendar) || '-')}</option>`
|
||||
).join('')
|
||||
: '<option value="">Нет подходящих графиков</option>';
|
||||
syncSelects(manageCalendarSelect);
|
||||
calendarCombobox?.search('');
|
||||
}
|
||||
|
||||
function initCalendarCombobox() {
|
||||
calendarCombobox = new AsyncCombobox(manageCalendarSelect, {
|
||||
placeholder: 'Найдите совместимый график',
|
||||
emptyLabel: 'Совместимых графиков не найдено',
|
||||
optionLabel: calendar => buildAcademicCalendarTitle(calendar) || calendar.title || `График ${calendar.id}`,
|
||||
optionDescription: calendar => `${calendar.academicYearTitle || ''} · ${calendar.studyFormName || ''}`,
|
||||
load: async (query, selectedId) => {
|
||||
const group = allGroups.find(item => String(item.id) === String(manageCalendarGroupIdInput.value));
|
||||
if (!group || !manageCalendarYearSelect.value) return [];
|
||||
const params = new URLSearchParams({
|
||||
query,
|
||||
academicYearId: manageCalendarYearSelect.value,
|
||||
specialtyId: String(group.specialtyId || group.specialityCode),
|
||||
profileId: String(group.specialtyProfileId),
|
||||
studyFormId: String(group.educationFormId),
|
||||
limit: '30'
|
||||
});
|
||||
if (selectedId) params.set('selectedId', selectedId);
|
||||
return api.get(`/api/admin/academic-calendars/options?${params}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function persistPageState() {
|
||||
writeUrlState({
|
||||
groupQuery: pageState.query,
|
||||
groupPage: pageState.page || '',
|
||||
groupSize: pageState.size === 25 ? '' : pageState.size,
|
||||
groupSort: pageState.sort === 'name,asc' ? '' : pageState.sort,
|
||||
groupEducationForm: pageState.educationFormIds
|
||||
});
|
||||
}
|
||||
|
||||
async function createGroup(event) {
|
||||
@@ -463,12 +547,17 @@ export async function initGroups() {
|
||||
const btnCalendar = event.target.closest('.btn-manage-calendar');
|
||||
|
||||
if (btnDelete) {
|
||||
if (!confirm('Архивировать группу? История расписания сохранится.')) return;
|
||||
if (!await confirmAction({
|
||||
title: 'Архивировать группу?',
|
||||
message: 'Группа исчезнет из рабочих списков, но история расписания сохранится.',
|
||||
confirmLabel: 'Архивировать',
|
||||
danger: true
|
||||
})) return;
|
||||
try {
|
||||
await api.delete('/api/groups/' + btnDelete.dataset.id);
|
||||
await loadGroups();
|
||||
} catch (error) {
|
||||
alert(error.message || 'Ошибка архивирования');
|
||||
await alertAction({ title: 'Не удалось архивировать группу', message: error.message || 'Ошибка архивирования' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,7 +566,7 @@ export async function initGroups() {
|
||||
await api.post('/api/groups/' + btnRestore.dataset.id + '/restore', {});
|
||||
await loadGroups();
|
||||
} catch (error) {
|
||||
alert(error.message || 'Ошибка восстановления');
|
||||
await alertAction({ title: 'Не удалось восстановить группу', message: error.message || 'Ошибка восстановления' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -488,7 +577,7 @@ export async function initGroups() {
|
||||
if (btnSubgroups) {
|
||||
const groupId = btnSubgroups.dataset.id;
|
||||
const group = allGroups.find(g => g.id == groupId);
|
||||
if (group) openSubgroupsModal(group);
|
||||
if (group) await openSubgroupsModal(group);
|
||||
}
|
||||
|
||||
if (btnCalendar) {
|
||||
@@ -498,10 +587,10 @@ export async function initGroups() {
|
||||
}
|
||||
}
|
||||
|
||||
function openEditGroupModal(id) {
|
||||
async function openEditGroupModal(id) {
|
||||
const group = allGroups.find(item => item.id == id);
|
||||
if (!group) {
|
||||
alert('Группа не найдена');
|
||||
await alertAction({ title: 'Группа не найдена', message: 'Обновите список и повторите действие.' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -519,13 +608,20 @@ export async function initGroups() {
|
||||
}
|
||||
|
||||
// --- Управление подгруппами ---
|
||||
function openSubgroupsModal(group) {
|
||||
async function openSubgroupsModal(group) {
|
||||
activeSubgroupGroup = group;
|
||||
subgroupsGroupContext.textContent = `Группа: ${group.name} (${group.groupSize} чел.)`;
|
||||
manageSubgroupGroupIdInput.value = group.id;
|
||||
resetSubgroupForm();
|
||||
modalManageSubgroups.classList.add('open');
|
||||
renderSubgroupsList(group.id);
|
||||
manageSubgroupsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Загрузка подгрупп...</td></tr>';
|
||||
try {
|
||||
await loadSubgroups(group.id);
|
||||
resetSubgroupForm();
|
||||
renderSubgroupsList(group.id);
|
||||
} catch (error) {
|
||||
showAlert('manage-subgroup-alert', error.message || 'Не удалось загрузить подгруппы', 'error');
|
||||
manageSubgroupsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Не удалось загрузить подгруппы</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderSubgroupsList(groupId) {
|
||||
@@ -633,7 +729,7 @@ export async function initGroups() {
|
||||
try {
|
||||
await syncSubgroups(groupId, existingSubgroups, capacities);
|
||||
showAlert('manage-subgroup-alert', 'Подгруппы сохранены', 'success');
|
||||
await loadSubgroups();
|
||||
await loadSubgroups(groupId);
|
||||
resetSubgroupForm();
|
||||
renderSubgroupsList(groupId);
|
||||
} catch (error) {
|
||||
@@ -660,7 +756,7 @@ export async function initGroups() {
|
||||
await api.delete(`/api/groups/${groupId}/subgroups/${subgroup.id}`);
|
||||
}
|
||||
showAlert('manage-subgroup-alert', 'Деление на подгруппы отключено', 'success');
|
||||
await loadSubgroups();
|
||||
await loadSubgroups(groupId);
|
||||
resetSubgroupForm();
|
||||
renderSubgroupsList(groupId);
|
||||
} catch (error) {
|
||||
@@ -727,12 +823,17 @@ export async function initGroups() {
|
||||
showAlert('manage-subgroup-alert', 'Чтобы изменить количество подгрупп, выберите нужный режим выше и нажмите «Применить»', 'error');
|
||||
return;
|
||||
}
|
||||
if (!confirm('Удалить подгруппу?')) return;
|
||||
if (!await confirmAction({
|
||||
title: 'Удалить подгруппу?',
|
||||
message: 'Удаление возможно только если подгруппа не используется в расписании.',
|
||||
confirmLabel: 'Удалить',
|
||||
danger: true
|
||||
})) return;
|
||||
try {
|
||||
await api.delete(`/api/groups/${groupId}/subgroups/${deleteButton.dataset.id}`);
|
||||
showAlert('manage-subgroup-alert', 'Подгруппа удалена', 'success');
|
||||
resetSubgroupForm();
|
||||
await loadSubgroups();
|
||||
await loadSubgroups(groupId);
|
||||
renderSubgroupsList(groupId);
|
||||
} catch (error) {
|
||||
showAlert('manage-subgroup-alert', error.message || 'Ошибка удаления подгруппы', 'error');
|
||||
@@ -778,10 +879,7 @@ export async function initGroups() {
|
||||
}
|
||||
|
||||
function assignmentCalendarTitle(assignment) {
|
||||
const calendar = calendars.find(item =>
|
||||
String(item.id) === String(assignment.calendarId)
|
||||
);
|
||||
return buildAcademicCalendarTitle(calendar) || assignment.calendarTitle || '-';
|
||||
return assignment.calendarTitle || '-';
|
||||
}
|
||||
|
||||
function renderAssignmentSubjects(subjects) {
|
||||
@@ -831,7 +929,12 @@ export async function initGroups() {
|
||||
manageCalendarTbody.addEventListener('click', async (event) => {
|
||||
const deleteButton = event.target.closest('.btn-delete-assignment');
|
||||
if (!deleteButton) return;
|
||||
if (!confirm('Удалить назначение графика?')) return;
|
||||
if (!await confirmAction({
|
||||
title: 'Удалить назначение графика?',
|
||||
message: 'Группа потеряет связь с этим графиком на выбранный учебный год.',
|
||||
confirmLabel: 'Удалить назначение',
|
||||
danger: true
|
||||
})) return;
|
||||
const groupId = manageCalendarGroupIdInput.value;
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user