import { api } from '../api.js';
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
const DAY_OPTIONS = [
{ value: 1, label: 'Понедельник' },
{ value: 2, label: 'Вторник' },
{ value: 3, label: 'Среда' },
{ value: 4, label: 'Четверг' },
{ value: 5, label: 'Пятница' },
{ value: 6, label: 'Суббота' },
{ value: 7, label: 'Воскресенье' }
];
const PARITY_LABELS = {
BOTH: 'каждую неделю',
ODD: 'нечётная',
EVEN: 'чётная'
};
const SEMESTER_LABELS = {
autumn: 'осенний',
spring: 'весенний'
};
const ACTIVITY_LABELS = {
THEORY: 'Теория',
EXAM: 'Экзамены',
VACATION: 'Каникулы',
PRACTICE: 'Практика'
};
export async function initSchedule() {
const ruleForm = document.getElementById('schedule-rule-form');
const ruleFormTitle = document.getElementById('schedule-rule-form-title');
const ruleIdInput = document.getElementById('schedule-rule-id');
const ruleSubjectSelect = document.getElementById('schedule-rule-subject');
const ruleSemesterSelect = document.getElementById('schedule-rule-semester');
const ruleActiveFromInput = document.getElementById('schedule-rule-active-from');
const ruleHoursInput = document.getElementById('schedule-rule-hours');
const ruleGroupsContainer = document.getElementById('schedule-rule-groups');
const ruleSlotsContainer = document.getElementById('schedule-rule-slots');
const ruleResetButton = document.getElementById('schedule-rule-reset');
const slotAddButton = document.getElementById('schedule-slot-add');
const rulesTbody = document.getElementById('schedule-tbody');
const refreshRulesButton = document.getElementById('schedule-refresh');
const timeSlotForm = document.getElementById('time-slot-form');
const timeSlotFormTitle = document.getElementById('time-slot-form-title');
const timeSlotIdInput = document.getElementById('time-slot-id');
const timeSlotOrderInput = document.getElementById('time-slot-order');
const timeSlotStartInput = document.getElementById('time-slot-start');
const timeSlotEndInput = document.getElementById('time-slot-end');
const timeSlotDurationInput = document.getElementById('time-slot-duration');
const timeSlotResetButton = document.getElementById('time-slot-reset');
const timeSlotsTbody = document.getElementById('time-slots-tbody');
const refreshSlotsButton = document.getElementById('time-slots-refresh');
const academicYearForm = document.getElementById('academic-year-form');
const academicYearFormTitle = document.getElementById('academic-year-form-title');
const academicYearIdInput = document.getElementById('academic-year-id');
const academicYearTitleInput = document.getElementById('academic-year-title');
const academicYearStartInput = document.getElementById('academic-year-start');
const academicYearEndInput = document.getElementById('academic-year-end');
const academicYearResetButton = document.getElementById('academic-year-reset');
const academicYearsTbody = document.getElementById('academic-years-tbody');
const academicYearsRefreshButton = document.getElementById('academic-years-refresh');
const semesterForm = document.getElementById('semester-form');
const semesterFormTitle = document.getElementById('semester-form-title');
const semesterIdInput = document.getElementById('semester-id');
const semesterYearSelect = document.getElementById('semester-year');
const semesterTypeSelect = document.getElementById('semester-type');
const semesterStartInput = document.getElementById('semester-start');
const semesterEndInput = document.getElementById('semester-end');
const semesterResetButton = document.getElementById('semester-reset');
const holidayForm = document.getElementById('holiday-form');
const holidayIdInput = document.getElementById('holiday-id');
const holidayYearSelect = document.getElementById('holiday-year');
const holidayDateInput = document.getElementById('holiday-date');
const holidayDescriptionInput = document.getElementById('holiday-description');
const holidayResetButton = document.getElementById('holiday-reset');
const holidaysTbody = document.getElementById('holidays-tbody');
const matrixSemesterSelect = document.getElementById('matrix-semester');
const matrixCourseInput = document.getElementById('matrix-course');
const matrixSpecialtySelect = document.getElementById('matrix-specialty');
const matrixLoadButton = document.getElementById('matrix-load');
const matrixSaveButton = document.getElementById('matrix-save');
const matrixGrid = document.getElementById('matrix-grid');
let rules = [];
let subjects = [];
let groups = [];
let teachers = [];
let classrooms = [];
let lessonTypes = [];
let timeSlots = [];
let academicYears = [];
let semesters = [];
let specialties = [];
let holidays = [];
bindEvents();
try {
await Promise.all([loadBaseLists(), loadTimeSlots(), loadYears()]);
renderRuleGroups([]);
renderRuleSlots([{}]);
await Promise.all([loadRules(), loadHolidays()]);
} catch (error) {
showAlert('schedule-rule-alert', error.message || 'Ошибка загрузки данных расписания', 'error');
}
function bindEvents() {
refreshRulesButton?.addEventListener('click', loadRules);
refreshSlotsButton?.addEventListener('click', loadTimeSlots);
academicYearsRefreshButton?.addEventListener('click', loadYears);
ruleResetButton?.addEventListener('click', resetRuleForm);
slotAddButton?.addEventListener('click', () => renderRuleSlots([...readRuleSlots(false), {}]));
timeSlotResetButton?.addEventListener('click', resetTimeSlotForm);
academicYearResetButton?.addEventListener('click', resetAcademicYearForm);
semesterResetButton?.addEventListener('click', resetSemesterForm);
holidayResetButton?.addEventListener('click', resetHolidayForm);
holidayYearSelect?.addEventListener('change', loadHolidays);
matrixLoadButton?.addEventListener('click', loadMatrix);
matrixSaveButton?.addEventListener('click', saveMatrix);
ruleSlotsContainer.addEventListener('click', (event) => {
const removeButton = event.target.closest('.schedule-slot-remove');
if (!removeButton) return;
const slots = readRuleSlots(false);
slots.splice(Number(removeButton.dataset.index), 1);
renderRuleSlots(slots.length ? slots : [{}]);
});
ruleForm.addEventListener('submit', saveRule);
timeSlotForm.addEventListener('submit', saveTimeSlot);
academicYearForm.addEventListener('submit', saveAcademicYear);
semesterForm.addEventListener('submit', saveSemester);
holidayForm.addEventListener('submit', saveHoliday);
rulesTbody.addEventListener('click', handleRuleTableClick);
timeSlotsTbody.addEventListener('click', handleTimeSlotTableClick);
academicYearsTbody.addEventListener('click', handleAcademicYearTableClick);
holidaysTbody.addEventListener('click', handleHolidayTableClick);
}
async function loadBaseLists() {
[subjects, groups, teachers, classrooms, lessonTypes, specialties] = await Promise.all([
api.get('/api/subjects'),
api.get('/api/groups'),
api.get('/api/users/teachers'),
api.get('/api/classrooms'),
api.get('/api/lesson-types'),
api.get('/api/specialties')
]);
populateSubjectSelects();
populateSpecialtySelects();
}
async function loadRules() {
rulesTbody.innerHTML = '
| Загрузка... |
';
try {
rules = await api.get('/api/admin/schedule-rules');
renderRules();
} catch (error) {
rulesTbody.innerHTML = `| Ошибка загрузки: ${escapeHtml(error.message)} |
`;
}
}
function renderRules() {
if (!rules || !rules.length) {
rulesTbody.innerHTML = '| Правила расписания не созданы |
';
return;
}
rulesTbody.innerHTML = rules.map(rule => `
| ${rule.id} |
${escapeHtml(rule.subjectName || '-')} |
${escapeHtml(formatSemester(rule))} |
${escapeHtml((rule.groupNames || []).join(', ') || '-')} |
${escapeHtml(rule.activeFromDate || '-')} |
${escapeHtml(String(rule.totalAcademicHours ?? '-'))} |
${renderRuleSlotsSummary(rule.slots)} |
|
`).join('');
}
function renderRuleSlotsSummary(slots) {
if (!slots || !slots.length) return 'Нет слотов';
return slots.map(slot => {
const parity = PARITY_LABELS[slot.parity] || slot.parity || '-';
const parts = [
slot.dayName,
slot.timeSlotLabel,
parity,
slot.teacherName,
slot.classroomName,
slot.lessonTypeName,
slot.lessonFormat
].filter(Boolean);
return `${escapeHtml(parts.join(' · '))}
`;
}).join('');
}
async function handleRuleTableClick(event) {
const editButton = event.target.closest('.btn-edit-rule');
const deleteButton = event.target.closest('.btn-delete-rule');
if (editButton) {
const rule = rules.find(item => item.id == editButton.dataset.id)
|| await api.get('/api/admin/schedule-rules/' + editButton.dataset.id);
fillRuleForm(rule);
return;
}
if (deleteButton) {
if (!confirm('Удалить правило расписания?')) return;
try {
await api.delete('/api/admin/schedule-rules/' + deleteButton.dataset.id);
showAlert('schedule-rule-alert', 'Правило расписания удалено', 'success');
await loadRules();
} catch (error) {
showAlert('schedule-rule-alert', error.message || 'Ошибка удаления правила', 'error');
}
}
}
async function saveRule(event) {
event.preventDefault();
hideAlert('schedule-rule-alert');
try {
const payload = buildRulePayload();
const id = ruleIdInput.value;
const saved = id
? await api.put('/api/admin/schedule-rules/' + id, payload)
: await api.post('/api/admin/schedule-rules', payload);
showAlert('schedule-rule-alert', `Правило "${saved.subjectName || 'расписания'}" сохранено`, 'success');
resetRuleForm();
await loadRules();
} catch (error) {
showAlert('schedule-rule-alert', error.message || 'Ошибка сохранения правила', 'error');
}
}
function buildRulePayload() {
const groupIds = Array.from(ruleGroupsContainer.querySelectorAll('input[type="checkbox"]:checked'))
.map(input => Number(input.value));
const slots = readRuleSlots(true);
if (!ruleSubjectSelect.value) {
throw new Error('Выберите дисциплину');
}
if (!ruleSemesterSelect.value) {
throw new Error('Выберите семестр');
}
if (!ruleActiveFromInput.value) {
throw new Error('Укажите дату начала правила');
}
if (!ruleHoursInput.value || Number(ruleHoursInput.value) <= 0) {
throw new Error('Укажите количество академических часов');
}
if (!groupIds.length) {
throw new Error('Выберите хотя бы одну группу');
}
if (!slots.length) {
throw new Error('Добавьте хотя бы один слот занятия');
}
return {
id: ruleIdInput.value ? Number(ruleIdInput.value) : null,
subjectId: Number(ruleSubjectSelect.value),
semesterId: Number(ruleSemesterSelect.value),
activeFromDate: ruleActiveFromInput.value,
totalAcademicHours: Number(ruleHoursInput.value),
groupIds,
slots
};
}
function fillRuleForm(rule) {
ruleFormTitle.textContent = 'Редактирование правила расписания';
ruleIdInput.value = rule.id;
ruleSubjectSelect.value = rule.subjectId || '';
ruleSemesterSelect.value = rule.semesterId || '';
ruleActiveFromInput.value = rule.activeFromDate || '';
ruleHoursInput.value = rule.totalAcademicHours || '';
syncSelects(ruleSubjectSelect, ruleSemesterSelect);
renderRuleGroups(rule.groupIds || []);
renderRuleSlots(rule.slots && rule.slots.length ? rule.slots : [{}]);
hideAlert('schedule-rule-alert');
ruleForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function resetRuleForm() {
ruleForm.reset();
ruleFormTitle.textContent = 'Новое правило расписания';
ruleIdInput.value = '';
renderRuleGroups([]);
renderRuleSlots([{}]);
syncSelects(ruleSubjectSelect, ruleSemesterSelect);
hideAlert('schedule-rule-alert');
}
function renderRuleGroups(selectedIds) {
if (!groups.length) {
ruleGroupsContainer.innerHTML = 'Нет групп
';
return;
}
const selected = new Set((selectedIds || []).map(String));
ruleGroupsContainer.innerHTML = groups.map(group => `
`).join('');
}
function renderRuleSlots(slots) {
ruleSlotsContainer.innerHTML = (slots || [{}]).map((slot, index) => `
`).join('');
}
function readRuleSlots(validate) {
const rows = Array.from(ruleSlotsContainer.querySelectorAll('.schedule-slot-row'));
return rows.map((row, index) => {
const slot = {
id: null,
dayOfWeek: Number(row.querySelector('.slot-day').value),
parity: row.querySelector('.slot-parity').value,
timeSlotId: Number(row.querySelector('.slot-time').value),
subgroupId: null,
teacherId: Number(row.querySelector('.slot-teacher').value),
classroomId: Number(row.querySelector('.slot-classroom').value),
lessonTypeId: Number(row.querySelector('.slot-lesson-type').value),
lessonFormat: row.querySelector('.slot-format').value
};
if (validate && (!slot.dayOfWeek || !slot.parity || !slot.timeSlotId
|| !slot.teacherId || !slot.classroomId || !slot.lessonTypeId || !slot.lessonFormat)) {
throw new Error(`Заполните слот ${index + 1}`);
}
return slot;
});
}
async function loadTimeSlots() {
timeSlotsTbody.innerHTML = '| Загрузка... |
';
try {
timeSlots = await api.get('/api/admin/time-slots');
renderTimeSlots();
} catch (error) {
timeSlotsTbody.innerHTML = `| Ошибка загрузки: ${escapeHtml(error.message)} |
`;
}
}
function renderTimeSlots() {
if (!timeSlots || !timeSlots.length) {
timeSlotsTbody.innerHTML = '| Сетка пар не настроена |
';
return;
}
timeSlotsTbody.innerHTML = timeSlots.map(slot => `
| ${slot.orderNumber} |
${escapeHtml(trimTime(slot.startTime))} |
${escapeHtml(trimTime(slot.endTime))} |
${escapeHtml(String(slot.durationMinutes ?? '-'))} |
|
`).join('');
}
async function saveTimeSlot(event) {
event.preventDefault();
hideAlert('time-slot-alert');
const payload = {
id: timeSlotIdInput.value ? Number(timeSlotIdInput.value) : null,
orderNumber: Number(timeSlotOrderInput.value),
startTime: timeSlotStartInput.value,
endTime: timeSlotEndInput.value,
durationMinutes: timeSlotDurationInput.value ? Number(timeSlotDurationInput.value) : null
};
try {
if (!payload.orderNumber || !payload.startTime || !payload.endTime) {
throw new Error('Заполните номер пары, начало и окончание');
}
const id = timeSlotIdInput.value;
await (id
? api.put('/api/admin/time-slots/' + id, payload)
: api.post('/api/admin/time-slots', payload));
showAlert('time-slot-alert', 'Временной слот сохранён', 'success');
resetTimeSlotForm();
await loadTimeSlots();
renderRuleSlots(readRuleSlots(false));
} catch (error) {
showAlert('time-slot-alert', error.message || 'Ошибка сохранения временного слота', 'error');
}
}
async function handleTimeSlotTableClick(event) {
const editButton = event.target.closest('.btn-edit-time-slot');
const deleteButton = event.target.closest('.btn-delete-time-slot');
if (editButton) {
const slot = timeSlots.find(item => item.id == editButton.dataset.id);
if (!slot) return;
timeSlotFormTitle.textContent = 'Редактирование временного слота';
timeSlotIdInput.value = slot.id;
timeSlotOrderInput.value = slot.orderNumber || '';
timeSlotStartInput.value = trimTime(slot.startTime);
timeSlotEndInput.value = trimTime(slot.endTime);
timeSlotDurationInput.value = slot.durationMinutes || '';
hideAlert('time-slot-alert');
timeSlotForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
return;
}
if (deleteButton) {
if (!confirm('Удалить временной слот?')) return;
try {
await api.delete('/api/admin/time-slots/' + deleteButton.dataset.id);
showAlert('time-slot-alert', 'Временной слот удалён', 'success');
await loadTimeSlots();
} catch (error) {
showAlert('time-slot-alert', error.message || 'Ошибка удаления временного слота', 'error');
}
}
}
function resetTimeSlotForm() {
timeSlotForm.reset();
timeSlotFormTitle.textContent = 'Новый временной слот';
timeSlotIdInput.value = '';
hideAlert('time-slot-alert');
}
async function loadYears() {
academicYearsTbody.innerHTML = '| Загрузка... |
';
try {
academicYears = await api.get('/api/admin/calendar/years');
semesters = academicYears.flatMap(year => year.semesters || []);
renderAcademicYears();
populateYearSelects();
populateSemesterSelects();
} catch (error) {
academicYearsTbody.innerHTML = `| Ошибка загрузки: ${escapeHtml(error.message)} |
`;
}
}
function renderAcademicYears() {
if (!academicYears || !academicYears.length) {
academicYearsTbody.innerHTML = '| Учебные годы не созданы |
';
return;
}
academicYearsTbody.innerHTML = academicYears.map(year => `
| ${escapeHtml(year.title)} |
${escapeHtml(year.startDate)} — ${escapeHtml(year.endDate)} |
${renderSemesterList(year.semesters || [])} |
|
`).join('');
}
function renderSemesterList(items) {
if (!items.length) return 'Нет семестров';
return items.map(semester => `
${escapeHtml(semesterLabel(semester))}
`).join('');
}
async function saveAcademicYear(event) {
event.preventDefault();
hideAlert('academic-year-alert');
const payload = {
title: academicYearTitleInput.value.trim(),
startDate: academicYearStartInput.value,
endDate: academicYearEndInput.value
};
try {
if (!payload.title || !payload.startDate || !payload.endDate) {
throw new Error('Заполните название и даты учебного года');
}
const id = academicYearIdInput.value;
await (id
? api.put('/api/admin/calendar/years/' + id, payload)
: api.post('/api/admin/calendar/years', payload));
showAlert('academic-year-alert', 'Учебный год сохранён', 'success');
resetAcademicYearForm();
await loadYears();
} catch (error) {
showAlert('academic-year-alert', error.message || 'Ошибка сохранения учебного года', 'error');
}
}
async function handleAcademicYearTableClick(event) {
const editYearButton = event.target.closest('.btn-edit-year');
const deleteYearButton = event.target.closest('.btn-delete-year');
const editSemesterButton = event.target.closest('.btn-edit-semester');
if (editYearButton) {
const year = academicYears.find(item => item.id == editYearButton.dataset.id);
if (!year) return;
academicYearFormTitle.textContent = 'Редактирование учебного года';
academicYearIdInput.value = year.id;
academicYearTitleInput.value = year.title || '';
academicYearStartInput.value = year.startDate || '';
academicYearEndInput.value = year.endDate || '';
hideAlert('academic-year-alert');
academicYearForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
return;
}
if (editSemesterButton) {
const semester = semesters.find(item => item.id == editSemesterButton.dataset.id);
if (!semester) return;
semesterFormTitle.textContent = 'Редактирование семестра';
semesterIdInput.value = semester.id;
semesterYearSelect.value = semester.academicYearId || '';
semesterTypeSelect.value = normalizeSemesterType(semester.semesterType);
semesterStartInput.value = semester.startDate || '';
semesterEndInput.value = semester.endDate || '';
syncSelects(semesterYearSelect, semesterTypeSelect);
hideAlert('semester-alert');
semesterForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
return;
}
if (deleteYearButton) {
if (!confirm('Удалить учебный год вместе с семестрами?')) return;
try {
await api.delete('/api/admin/calendar/years/' + deleteYearButton.dataset.id);
showAlert('academic-year-alert', 'Учебный год удалён', 'success');
await loadYears();
} catch (error) {
showAlert('academic-year-alert', error.message || 'Ошибка удаления учебного года', 'error');
}
}
}
function resetAcademicYearForm() {
academicYearForm.reset();
academicYearFormTitle.textContent = 'Учебный год';
academicYearIdInput.value = '';
hideAlert('academic-year-alert');
}
async function saveSemester(event) {
event.preventDefault();
hideAlert('semester-alert');
const yearId = semesterYearSelect.value;
const payload = {
semesterType: semesterTypeSelect.value,
startDate: semesterStartInput.value,
endDate: semesterEndInput.value
};
try {
if (!yearId || !payload.semesterType || !payload.startDate || !payload.endDate) {
throw new Error('Заполните учебный год, тип и даты семестра');
}
const id = semesterIdInput.value;
await (id
? api.put('/api/admin/calendar/semesters/' + id, payload)
: api.post(`/api/admin/calendar/years/${yearId}/semesters`, payload));
showAlert('semester-alert', 'Семестр сохранён', 'success');
resetSemesterForm();
await loadYears();
} catch (error) {
showAlert('semester-alert', error.message || 'Ошибка сохранения семестра', 'error');
}
}
function resetSemesterForm() {
semesterForm.reset();
semesterFormTitle.textContent = 'Семестр';
semesterIdInput.value = '';
syncSelects(semesterYearSelect, semesterTypeSelect);
hideAlert('semester-alert');
}
async function saveHoliday(event) {
event.preventDefault();
hideAlert('holiday-alert');
const payload = {
id: holidayIdInput.value ? Number(holidayIdInput.value) : null,
academicYearId: Number(holidayYearSelect.value),
date: holidayDateInput.value,
description: holidayDescriptionInput.value.trim()
};
try {
if (!payload.academicYearId || !payload.date) {
throw new Error('Выберите учебный год и дату');
}
const id = holidayIdInput.value;
await (id
? api.put('/api/admin/calendar/holidays/' + id, payload)
: api.post('/api/admin/calendar/holidays', payload));
showAlert('holiday-alert', 'Дата сохранена', 'success');
resetHolidayForm(false);
await loadHolidays();
} catch (error) {
showAlert('holiday-alert', error.message || 'Ошибка сохранения даты', 'error');
}
}
async function loadHolidays() {
const yearId = holidayYearSelect.value;
if (!yearId) {
holidaysTbody.innerHTML = '| Выберите учебный год |
';
return;
}
holidaysTbody.innerHTML = '| Загрузка... |
';
try {
holidays = await api.get('/api/admin/calendar/holidays?academicYearId=' + encodeURIComponent(yearId));
renderHolidays();
} catch (error) {
holidaysTbody.innerHTML = `| Ошибка загрузки: ${escapeHtml(error.message)} |
`;
}
}
function renderHolidays() {
if (!holidays || !holidays.length) {
holidaysTbody.innerHTML = '| Исключения не добавлены |
';
return;
}
holidaysTbody.innerHTML = holidays.map(holiday => `
| ${escapeHtml(holiday.date)} |
${escapeHtml(holiday.description || '-')} |
|
`).join('');
}
async function handleHolidayTableClick(event) {
const editButton = event.target.closest('.btn-edit-holiday');
const deleteButton = event.target.closest('.btn-delete-holiday');
if (editButton) {
const holiday = holidays.find(item => item.id == editButton.dataset.id);
if (!holiday) return;
holidayIdInput.value = holiday.id;
holidayYearSelect.value = holiday.academicYearId || holidayYearSelect.value;
holidayDateInput.value = holiday.date || '';
holidayDescriptionInput.value = holiday.description || '';
syncSelects(holidayYearSelect);
hideAlert('holiday-alert');
holidayForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
return;
}
if (deleteButton) {
if (!confirm('Удалить дату-исключение?')) return;
try {
await api.delete('/api/admin/calendar/holidays/' + deleteButton.dataset.id);
showAlert('holiday-alert', 'Дата удалена', 'success');
await loadHolidays();
} catch (error) {
showAlert('holiday-alert', error.message || 'Ошибка удаления даты', 'error');
}
}
}
function resetHolidayForm(clearYear = true) {
const yearId = holidayYearSelect.value;
holidayForm.reset();
if (!clearYear) holidayYearSelect.value = yearId;
holidayIdInput.value = '';
syncSelects(holidayYearSelect);
hideAlert('holiday-alert');
}
async function loadMatrix() {
hideAlert('matrix-alert');
const semesterId = matrixSemesterSelect.value;
const courseNumber = Number(matrixCourseInput.value);
const specialtyId = matrixSpecialtySelect.value;
if (!semesterId || !courseNumber || !specialtyId) {
showAlert('matrix-alert', 'Выберите семестр, курс и специальность', 'error');
return;
}
try {
const params = new URLSearchParams({ semesterId, courseNumber: String(courseNumber), specialtyId });
const rows = await api.get('/api/admin/calendar/matrix?' + params.toString());
renderMatrix(rows && rows.length ? rows : buildDefaultMatrixRows(semesterId, courseNumber, specialtyId));
showAlert('matrix-alert', rows && rows.length ? 'Матрица загружена' : 'Матрица не найдена, подготовлена новая сетка', 'success');
} catch (error) {
showAlert('matrix-alert', error.message || 'Ошибка загрузки матрицы', 'error');
}
}
async function saveMatrix() {
hideAlert('matrix-alert');
const rows = Array.from(matrixGrid.querySelectorAll('.matrix-week')).map(item => ({
id: item.dataset.id ? Number(item.dataset.id) : null,
semesterId: Number(item.dataset.semesterId),
courseNumber: Number(item.dataset.courseNumber),
specialtyId: Number(item.dataset.specialtyId),
weekNumber: Number(item.dataset.weekNumber),
activityType: item.querySelector('select').value
}));
if (!rows.length) {
showAlert('matrix-alert', 'Сначала загрузите матрицу', 'error');
return;
}
try {
await api.put('/api/admin/calendar/matrix', rows);
showAlert('matrix-alert', 'Матрица учебного графика сохранена', 'success');
await loadMatrix();
} catch (error) {
showAlert('matrix-alert', error.message || 'Ошибка сохранения матрицы', 'error');
}
}
function renderMatrix(rows) {
matrixGrid.innerHTML = rows.map(row => `
${row.weekNumber}
`).join('');
}
function buildDefaultMatrixRows(semesterId, courseNumber, specialtyId) {
return Array.from({ length: 22 }, (_, index) => ({
id: null,
semesterId: Number(semesterId),
courseNumber,
specialtyId: Number(specialtyId),
weekNumber: index + 1,
activityType: 'THEORY'
}));
}
function populateSubjectSelects() {
ruleSubjectSelect.innerHTML = '' +
subjects.map(subject => ``).join('');
}
function populateYearSelects() {
const yearOptions = '' +
academicYears.map(year => ``).join('');
semesterYearSelect.innerHTML = yearOptions;
holidayYearSelect.innerHTML = yearOptions;
if (!holidayYearSelect.value && academicYears.length) {
holidayYearSelect.value = String(academicYears[0].id);
}
syncSelects(semesterYearSelect, holidayYearSelect);
}
function populateSemesterSelects() {
const semesterOptions = '' +
semesters.map(semester => ``).join('');
ruleSemesterSelect.innerHTML = semesterOptions;
matrixSemesterSelect.innerHTML = semesterOptions;
syncSelects(ruleSemesterSelect, matrixSemesterSelect);
}
function populateSpecialtySelects() {
matrixSpecialtySelect.innerHTML = '' +
specialties.map(speciality => ``).join('');
syncSelects(matrixSpecialtySelect);
}
function options(items, selectedValue) {
const selected = selectedValue == null ? '' : String(selectedValue);
return '' + items.map(item => {
const value = String(item.value);
return ``;
}).join('');
}
function activityOptions() {
return Object.entries(ACTIVITY_LABELS).map(([value, label]) => ({ value, label }));
}
function toTimeSlotOption(slot) {
return { value: slot.id, label: `${slot.orderNumber} пара, ${trimTime(slot.startTime)} - ${trimTime(slot.endTime)}` };
}
function toTeacherOption(teacher) {
return { value: teacher.id, label: teacher.fullName || teacher.username || `ID ${teacher.id}` };
}
function toClassroomOption(classroom) {
const capacity = classroom.capacity ? `, ${classroom.capacity} чел.` : '';
return { value: classroom.id, label: `${classroom.name}${capacity}` };
}
function toLessonTypeOption(lessonType) {
return { value: lessonType.id, label: lessonType.name || lessonType.lessonType || `ID ${lessonType.id}` };
}
function semesterLabel(semester) {
const type = normalizeSemesterType(semester.semesterType);
return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${semester.startDate} — ${semester.endDate})`;
}
function formatSemester(rule) {
const type = normalizeSemesterType(rule.semesterType);
return `${rule.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type || '-'}`;
}
function normalizeSemesterType(type) {
return String(type || '').toLowerCase();
}
function specialityLabel(speciality) {
const code = speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code || speciality.id;
const name = speciality.specialityName || speciality.name || '';
return name ? `${code} — ${name}` : String(code);
}
function trimTime(value) {
if (!value) return '';
return String(value).slice(0, 5);
}
function syncSelects(...selects) {
selects.filter(Boolean).forEach(select => {
select.dispatchEvent(new Event('change', { bubbles: true }));
});
}
}