Добавить дисциплины календарных графиков
This commit is contained in:
@@ -70,6 +70,16 @@ export async function initAcademicCalendar() {
|
||||
const calendarDayDialogTimeScopeSelect = document.getElementById('calendar-day-dialog-time-scope');
|
||||
const calendarDayDialogTimeSaveButton = document.getElementById('calendar-day-dialog-time-save');
|
||||
const calendarDayDialogTimeClearButton = document.getElementById('calendar-day-dialog-time-clear');
|
||||
const calendarTabButtons = Array.from(document.querySelectorAll('[data-calendar-tab]'));
|
||||
const calendarTabPanels = Array.from(document.querySelectorAll('[data-calendar-tab-panel]'));
|
||||
const subjectCalendarSelect = document.getElementById('calendar-subject-calendar');
|
||||
const subjectSemesterSelect = document.getElementById('calendar-subject-semester');
|
||||
const subjectSelect = document.getElementById('calendar-subject-id');
|
||||
const subjectAddButton = document.getElementById('calendar-subject-add');
|
||||
const subjectSaveButton = document.getElementById('calendar-subject-save');
|
||||
const subjectRefreshButton = document.getElementById('calendar-subject-refresh');
|
||||
const subjectSummary = document.getElementById('calendar-subject-summary');
|
||||
const subjectCount = document.getElementById('calendar-subject-count');
|
||||
|
||||
let academicYears = [];
|
||||
let semesters = [];
|
||||
@@ -78,6 +88,8 @@ export async function initAcademicCalendar() {
|
||||
let studyForms = [];
|
||||
let activityTypes = [];
|
||||
let calendars = [];
|
||||
let subjects = [];
|
||||
let calendarSubjectRows = [];
|
||||
let timeSlotScopes = [];
|
||||
let timeSlotDateAssignments = [];
|
||||
let activeCalendarDay = null;
|
||||
@@ -95,6 +107,7 @@ export async function initAcademicCalendar() {
|
||||
await Promise.all([
|
||||
loadBaseLists(),
|
||||
loadYears(),
|
||||
loadSubjects(),
|
||||
loadStudyForms(),
|
||||
loadActivityTypes(),
|
||||
loadTimeSlotScopes(),
|
||||
@@ -106,6 +119,9 @@ export async function initAcademicCalendar() {
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
calendarTabButtons.forEach(button => {
|
||||
button.addEventListener('click', () => activateCalendarTab(button.dataset.calendarTab));
|
||||
});
|
||||
academicYearsRefreshButton?.addEventListener('click', loadYears);
|
||||
calendarsRefreshButton?.addEventListener('click', loadCalendars);
|
||||
academicYearResetButton?.addEventListener('click', resetAcademicYearForm);
|
||||
@@ -115,6 +131,11 @@ export async function initAcademicCalendar() {
|
||||
editorLoadButton?.addEventListener('click', loadCalendarGrid);
|
||||
editorSaveButton?.addEventListener('click', saveCalendarGrid);
|
||||
fillApplyButton?.addEventListener('click', applyCalendarFill);
|
||||
subjectCalendarSelect?.addEventListener('change', loadCalendarSubjects);
|
||||
subjectAddButton?.addEventListener('click', addCalendarSubject);
|
||||
subjectSaveButton?.addEventListener('click', saveCalendarSubjects);
|
||||
subjectRefreshButton?.addEventListener('click', loadCalendarSubjects);
|
||||
subjectSummary?.addEventListener('click', handleSubjectSummaryClick);
|
||||
|
||||
calendarGrid?.addEventListener('change', (event) => {
|
||||
if (event.target.matches('.calendar-day-activity')) {
|
||||
@@ -361,6 +382,11 @@ export async function initAcademicCalendar() {
|
||||
populateStudyFormSelect();
|
||||
}
|
||||
|
||||
async function loadSubjects() {
|
||||
subjects = await api.get('/api/subjects');
|
||||
populateSubjectSelect();
|
||||
}
|
||||
|
||||
async function loadActivityTypes() {
|
||||
activityTypes = await api.get('/api/admin/calendar/activity-types');
|
||||
populateActivitySelects();
|
||||
@@ -380,6 +406,7 @@ export async function initAcademicCalendar() {
|
||||
calendars = await api.get('/api/admin/academic-calendars');
|
||||
renderCalendars();
|
||||
populateEditorCalendarSelect();
|
||||
populateSubjectCalendarSelect();
|
||||
} catch (error) {
|
||||
calendarsTbody.innerHTML = `<tr><td colspan="7" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
throw error;
|
||||
@@ -402,6 +429,7 @@ export async function initAcademicCalendar() {
|
||||
<td>
|
||||
<button class="btn btn-sm btn-secondary btn-edit-calendar" data-id="${calendar.id}">Изменить</button>
|
||||
<button class="btn btn-sm btn-secondary btn-open-calendar-grid" data-id="${calendar.id}">Сетка</button>
|
||||
<button class="btn btn-sm btn-secondary btn-open-calendar-subjects" data-id="${calendar.id}">Дисциплины</button>
|
||||
<button class="btn btn-sm btn-danger btn-delete-calendar" data-id="${calendar.id}">Удалить</button>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -440,9 +468,11 @@ export async function initAcademicCalendar() {
|
||||
async function handleCalendarTableClick(event) {
|
||||
const editButton = event.target.closest('.btn-edit-calendar');
|
||||
const gridButton = event.target.closest('.btn-open-calendar-grid');
|
||||
const subjectsButton = event.target.closest('.btn-open-calendar-subjects');
|
||||
const deleteButton = event.target.closest('.btn-delete-calendar');
|
||||
|
||||
if (editButton) {
|
||||
activateCalendarTab('manage');
|
||||
const calendar = calendars.find(item => item.id == editButton.dataset.id);
|
||||
if (!calendar) return;
|
||||
calendarFormTitle.textContent = 'Редактирование календарного графика';
|
||||
@@ -460,13 +490,23 @@ export async function initAcademicCalendar() {
|
||||
}
|
||||
|
||||
if (gridButton) {
|
||||
activateCalendarTab('grid');
|
||||
editorCalendarSelect.value = gridButton.dataset.id;
|
||||
syncSelects(editorCalendarSelect);
|
||||
refreshSelectVisuals(editorCalendarSelect);
|
||||
await loadCalendarGrid();
|
||||
calendarGrid.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (subjectsButton) {
|
||||
activateCalendarTab('subjects');
|
||||
subjectCalendarSelect.value = subjectsButton.dataset.id;
|
||||
refreshSelectVisuals(subjectCalendarSelect);
|
||||
await loadCalendarSubjects();
|
||||
subjectSummary.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (deleteButton) {
|
||||
if (!confirm('Удалить календарный график?')) return;
|
||||
try {
|
||||
@@ -489,6 +529,149 @@ export async function initAcademicCalendar() {
|
||||
hideAlert('academic-calendar-alert');
|
||||
}
|
||||
|
||||
function activateCalendarTab(tabName) {
|
||||
const target = tabName || 'manage';
|
||||
calendarTabButtons.forEach(button => {
|
||||
const active = button.dataset.calendarTab === target;
|
||||
button.classList.toggle('active', active);
|
||||
button.setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
});
|
||||
calendarTabPanels.forEach(panel => {
|
||||
const active = panel.dataset.calendarTabPanel === target;
|
||||
panel.classList.toggle('active', active);
|
||||
panel.hidden = !active;
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCalendarSubjects() {
|
||||
hideAlert('calendar-subject-alert');
|
||||
const calendar = selectedSubjectCalendar();
|
||||
populateSubjectSemesterSelect();
|
||||
calendarSubjectRows = [];
|
||||
if (!calendar) {
|
||||
renderCalendarSubjects();
|
||||
return;
|
||||
}
|
||||
subjectSummary.innerHTML = '<div class="loading-row">Загрузка...</div>';
|
||||
try {
|
||||
const rows = await api.get(`/api/admin/academic-calendars/${calendar.id}/subjects`);
|
||||
calendarSubjectRows = sortCalendarSubjectRows(rows || []);
|
||||
renderCalendarSubjects();
|
||||
} catch (error) {
|
||||
subjectSummary.innerHTML = `<div class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</div>`;
|
||||
showAlert('calendar-subject-alert', error.message || 'Ошибка загрузки дисциплин графика', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function addCalendarSubject() {
|
||||
hideAlert('calendar-subject-alert');
|
||||
const calendar = selectedSubjectCalendar();
|
||||
const semesterNumber = Number(subjectSemesterSelect.value);
|
||||
const subjectId = Number(subjectSelect.value);
|
||||
if (!calendar || !semesterNumber || !subjectId) {
|
||||
showAlert('calendar-subject-alert', 'Выберите график, семестр и дисциплину', 'error');
|
||||
return;
|
||||
}
|
||||
if (calendarSubjectRows.some(row =>
|
||||
Number(row.semesterNumber) === semesterNumber && Number(row.subjectId) === subjectId
|
||||
)) {
|
||||
showAlert('calendar-subject-alert', 'Дисциплина уже добавлена в этот семестр', 'error');
|
||||
return;
|
||||
}
|
||||
const subject = subjects.find(item => Number(item.id) === subjectId);
|
||||
if (!subject) {
|
||||
showAlert('calendar-subject-alert', 'Дисциплина не найдена в справочнике', 'error');
|
||||
return;
|
||||
}
|
||||
calendarSubjectRows = sortCalendarSubjectRows([
|
||||
...calendarSubjectRows,
|
||||
{
|
||||
id: null,
|
||||
calendarId: calendar.id,
|
||||
semesterNumber,
|
||||
subjectId,
|
||||
subjectName: subject.name,
|
||||
subjectCode: subject.code,
|
||||
departmentId: subject.departmentId
|
||||
}
|
||||
]);
|
||||
renderCalendarSubjects();
|
||||
showAlert('calendar-subject-alert', 'Дисциплина добавлена. Сохраните привязки.', 'success');
|
||||
}
|
||||
|
||||
async function saveCalendarSubjects() {
|
||||
hideAlert('calendar-subject-alert');
|
||||
const calendar = selectedSubjectCalendar();
|
||||
if (!calendar) {
|
||||
showAlert('calendar-subject-alert', 'Выберите календарный график', 'error');
|
||||
return;
|
||||
}
|
||||
const payload = calendarSubjectRows.map(row => ({
|
||||
semesterNumber: Number(row.semesterNumber),
|
||||
subjectId: Number(row.subjectId)
|
||||
}));
|
||||
try {
|
||||
const saved = await api.put(`/api/admin/academic-calendars/${calendar.id}/subjects`, payload);
|
||||
calendarSubjectRows = sortCalendarSubjectRows(saved || []);
|
||||
renderCalendarSubjects();
|
||||
showAlert('calendar-subject-alert', 'Привязки дисциплин сохранены', 'success');
|
||||
} catch (error) {
|
||||
showAlert('calendar-subject-alert', error.message || 'Ошибка сохранения дисциплин графика', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function handleSubjectSummaryClick(event) {
|
||||
const removeButton = event.target.closest('.calendar-subject-remove');
|
||||
if (!removeButton) return;
|
||||
const semesterNumber = Number(removeButton.dataset.semester);
|
||||
const subjectId = Number(removeButton.dataset.subject);
|
||||
calendarSubjectRows = calendarSubjectRows.filter(row =>
|
||||
Number(row.semesterNumber) !== semesterNumber || Number(row.subjectId) !== subjectId
|
||||
);
|
||||
renderCalendarSubjects();
|
||||
showAlert('calendar-subject-alert', 'Дисциплина убрана. Сохраните привязки.', 'success');
|
||||
}
|
||||
|
||||
function renderCalendarSubjects() {
|
||||
const calendar = selectedSubjectCalendar();
|
||||
if (!calendar) {
|
||||
subjectCount.textContent = '';
|
||||
subjectSummary.innerHTML = '<div class="loading-row">Выберите календарный график</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const semesterCount = calendarSemesterCount(calendar);
|
||||
subjectCount.textContent = `${calendarSubjectRows.length} дисциплин`;
|
||||
subjectSummary.innerHTML = Array.from({ length: semesterCount }, (_, index) => {
|
||||
const semesterNumber = index + 1;
|
||||
const semesterRows = calendarSubjectRows.filter(row => Number(row.semesterNumber) === semesterNumber);
|
||||
return `
|
||||
<section class="calendar-subject-semester">
|
||||
<div class="calendar-subject-semester-head">
|
||||
<strong>${escapeHtml(semesterOptionLabel(semesterNumber))}</strong>
|
||||
<span>${semesterRows.length} дисциплин</span>
|
||||
</div>
|
||||
${semesterRows.length
|
||||
? `<div class="calendar-subject-chips">${semesterRows.map(renderSubjectChip).join('')}</div>`
|
||||
: '<div class="calendar-subject-empty">Нет дисциплин</div>'}
|
||||
</section>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderSubjectChip(row) {
|
||||
return `
|
||||
<span class="calendar-subject-chip">
|
||||
<span>${escapeHtml(subjectRowLabel(row))}</span>
|
||||
<button type="button"
|
||||
class="calendar-subject-remove"
|
||||
data-semester="${escapeHtml(String(row.semesterNumber))}"
|
||||
data-subject="${escapeHtml(String(row.subjectId))}"
|
||||
aria-label="Убрать ${escapeHtml(subjectRowLabel(row))}">×</button>
|
||||
</span>
|
||||
`;
|
||||
}
|
||||
|
||||
async function loadCalendarGrid() {
|
||||
hideAlert('calendar-editor-alert');
|
||||
const calendar = selectedEditorCalendar();
|
||||
@@ -568,8 +751,8 @@ export async function initAcademicCalendar() {
|
||||
selectedCalendarDayKey = null;
|
||||
selectedCalendarDays = [];
|
||||
const grouped = groupBy(rows, row => row.courseNumber);
|
||||
calendarGrid.innerHTML = Array.from(grouped.entries()).map(([course, courseRows], index) => `
|
||||
<details class="calendar-course-block" ${index === 0 ? 'open' : ''}>
|
||||
calendarGrid.innerHTML = Array.from(grouped.entries()).map(([course, courseRows]) => `
|
||||
<details class="calendar-course-block">
|
||||
<summary class="calendar-course-header">
|
||||
<span class="calendar-course-title">
|
||||
<span class="calendar-course-chevron" aria-hidden="true"></span>
|
||||
@@ -1104,6 +1287,21 @@ export async function initAcademicCalendar() {
|
||||
syncSelects(calendarStudyFormSelect);
|
||||
}
|
||||
|
||||
function populateSubjectSelect() {
|
||||
if (!subjectSelect) return;
|
||||
const current = subjectSelect.value;
|
||||
const sortedSubjects = [...subjects].sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'ru'));
|
||||
subjectSelect.innerHTML = sortedSubjects.length
|
||||
? '<option value="">Выберите дисциплину</option>' + sortedSubjects.map(subject =>
|
||||
`<option value="${subject.id}">${escapeHtml(subjectLabel(subject))}</option>`
|
||||
).join('')
|
||||
: '<option value="">Нет дисциплин</option>';
|
||||
if (current && sortedSubjects.some(subject => String(subject.id) === String(current))) {
|
||||
subjectSelect.value = current;
|
||||
}
|
||||
syncSelects(subjectSelect);
|
||||
}
|
||||
|
||||
function populateActivitySelects() {
|
||||
const activityOptions = '<option value="">Выберите код</option>' +
|
||||
activityTypes.map(type => `<option value="${type.id}">${escapeHtml(type.code)} - ${escapeHtml(type.name)}</option>`).join('');
|
||||
@@ -1121,6 +1319,41 @@ export async function initAcademicCalendar() {
|
||||
syncSelects(editorCalendarSelect);
|
||||
}
|
||||
|
||||
function populateSubjectCalendarSelect() {
|
||||
if (!subjectCalendarSelect) return;
|
||||
const current = subjectCalendarSelect.value;
|
||||
subjectCalendarSelect.innerHTML = calendars.length
|
||||
? '<option value="">Выберите график</option>' + calendars.map(calendar =>
|
||||
`<option value="${calendar.id}">${escapeHtml(calendar.title)}</option>`
|
||||
).join('')
|
||||
: '<option value="">Нет графиков</option>';
|
||||
if (current && calendars.some(calendar => String(calendar.id) === String(current))) {
|
||||
subjectCalendarSelect.value = current;
|
||||
}
|
||||
populateSubjectSemesterSelect();
|
||||
syncSelects(subjectCalendarSelect);
|
||||
}
|
||||
|
||||
function populateSubjectSemesterSelect() {
|
||||
if (!subjectSemesterSelect) return;
|
||||
const calendar = selectedSubjectCalendar();
|
||||
const current = subjectSemesterSelect.value;
|
||||
if (!calendar) {
|
||||
subjectSemesterSelect.innerHTML = '<option value="">Выберите график</option>';
|
||||
syncSelects(subjectSemesterSelect);
|
||||
return;
|
||||
}
|
||||
const semesterCount = calendarSemesterCount(calendar);
|
||||
subjectSemesterSelect.innerHTML = Array.from({ length: semesterCount }, (_, index) => {
|
||||
const semesterNumber = index + 1;
|
||||
return `<option value="${semesterNumber}">${escapeHtml(semesterOptionLabel(semesterNumber))}</option>`;
|
||||
}).join('');
|
||||
if (current && Number(current) >= 1 && Number(current) <= semesterCount) {
|
||||
subjectSemesterSelect.value = current;
|
||||
}
|
||||
syncSelects(subjectSemesterSelect);
|
||||
}
|
||||
|
||||
function populateFillCourseSelect() {
|
||||
const courses = new Set(Array.from(calendarGrid.querySelectorAll('.calendar-day')).map(day => day.dataset.course));
|
||||
fillCourseSelect.innerHTML = '<option value="">Все курсы</option>' +
|
||||
@@ -1145,6 +1378,38 @@ export async function initAcademicCalendar() {
|
||||
return calendars.find(calendar => String(calendar.id) === String(editorCalendarSelect.value));
|
||||
}
|
||||
|
||||
function selectedSubjectCalendar() {
|
||||
return calendars.find(calendar => String(calendar.id) === String(subjectCalendarSelect?.value));
|
||||
}
|
||||
|
||||
function calendarSemesterCount(calendar) {
|
||||
return Math.max(1, Number(calendar?.courseCount || 0) * 2);
|
||||
}
|
||||
|
||||
function semesterOptionLabel(semesterNumber) {
|
||||
const course = Math.ceil(Number(semesterNumber) / 2);
|
||||
const type = Number(semesterNumber) % 2 === 1 ? 'осенний' : 'весенний';
|
||||
return `${semesterNumber} семестр · ${course} курс, ${type}`;
|
||||
}
|
||||
|
||||
function sortCalendarSubjectRows(rows) {
|
||||
return [...rows].sort((a, b) => {
|
||||
const semesterDiff = Number(a.semesterNumber) - Number(b.semesterNumber);
|
||||
if (semesterDiff !== 0) return semesterDiff;
|
||||
return subjectRowLabel(a).localeCompare(subjectRowLabel(b), 'ru');
|
||||
});
|
||||
}
|
||||
|
||||
function subjectLabel(subject) {
|
||||
const code = subject.code ? `${subject.code} · ` : '';
|
||||
return `${code}${subject.name || 'Без названия'}`;
|
||||
}
|
||||
|
||||
function subjectRowLabel(row) {
|
||||
const code = row.subjectCode ? `${row.subjectCode} · ` : '';
|
||||
return `${code}${row.subjectName || row.subjectId || 'Без названия'}`;
|
||||
}
|
||||
|
||||
function theoryActivityId() {
|
||||
return activityTypes.find(type => type.code === 'Т')?.id || activityTypes[0]?.id || '';
|
||||
}
|
||||
@@ -1265,7 +1530,16 @@ export async function initAcademicCalendar() {
|
||||
|
||||
function syncSelects(...selects) {
|
||||
selects.filter(Boolean).forEach(select => {
|
||||
refreshSelectVisuals(select);
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
function refreshSelectVisuals(...selects) {
|
||||
selects.filter(Boolean).forEach(select => {
|
||||
if (select.customSelectInstance) {
|
||||
select.customSelectInstance.rebuildMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user