Добавить дисциплины календарных графиков
This commit is contained in:
@@ -1181,6 +1181,150 @@
|
||||
}
|
||||
}
|
||||
|
||||
.academic-calendar-tabs {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--bg-card-border);
|
||||
}
|
||||
|
||||
.academic-calendar-tab {
|
||||
min-height: 40px;
|
||||
padding: 0.55rem 0.9rem;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: color var(--transition), background var(--transition), border-color var(--transition);
|
||||
}
|
||||
|
||||
.academic-calendar-tab:hover,
|
||||
.academic-calendar-tab:focus-visible {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-hover);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.academic-calendar-tab.active {
|
||||
color: var(--accent-hover);
|
||||
background: var(--bg-card);
|
||||
border-color: var(--bg-card-border);
|
||||
}
|
||||
|
||||
.academic-calendar-tab-panel {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.academic-calendar-tab-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.calendar-subject-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.calendar-subject-semester {
|
||||
min-height: 104px;
|
||||
padding: 0.8rem;
|
||||
border: 1px solid var(--bg-card-border);
|
||||
border-radius: var(--radius-sm);
|
||||
background: color-mix(in srgb, var(--bg-input) 84%, var(--bg-card));
|
||||
}
|
||||
|
||||
.calendar-subject-semester-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.65rem;
|
||||
}
|
||||
|
||||
.calendar-subject-semester-head strong {
|
||||
color: var(--text-primary);
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.calendar-subject-semester-head span,
|
||||
.calendar-subject-empty {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.calendar-subject-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.calendar-subject-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
min-height: 30px;
|
||||
padding: 0.25rem 0.28rem 0.25rem 0.55rem;
|
||||
gap: 0.35rem;
|
||||
border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--bg-card-border));
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--accent) 9%, var(--bg-card));
|
||||
color: var(--text-primary);
|
||||
font-size: 0.84rem;
|
||||
}
|
||||
|
||||
.calendar-subject-chip span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.calendar-subject-remove {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
flex: 0 0 22px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--danger) 12%, transparent);
|
||||
color: var(--danger);
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.calendar-subject-remove:hover,
|
||||
.calendar-subject-remove:focus-visible {
|
||||
background: var(--danger);
|
||||
color: white;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.assignment-subjects {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.assignment-subject-semester {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.assignment-subject-semester strong {
|
||||
color: var(--accent-hover);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.assignment-subject-semester span {
|
||||
color: var(--text-primary);
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.calendar-fill-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(150px, 1fr)) auto;
|
||||
@@ -1715,6 +1859,10 @@
|
||||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||
}
|
||||
|
||||
.calendar-subject-summary {
|
||||
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
||||
}
|
||||
|
||||
.calendar-semesters-grid {
|
||||
grid-template-columns: 1fr;
|
||||
grid-auto-rows: auto;
|
||||
@@ -1769,6 +1917,16 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.academic-calendar-tab {
|
||||
flex: 1 1 100%;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--bg-card-border);
|
||||
}
|
||||
|
||||
.calendar-subject-summary {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.calendar-day-dialog-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -522,24 +522,25 @@ export async function initGroups() {
|
||||
}
|
||||
|
||||
async function loadAssignmentsList(groupId) {
|
||||
manageCalendarTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Загрузка...</td></tr>';
|
||||
manageCalendarTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Загрузка...</td></tr>';
|
||||
try {
|
||||
currentAssignments = await api.get(`/api/groups/${groupId}/calendar-assignments`);
|
||||
renderAssignmentsList();
|
||||
} catch (error) {
|
||||
manageCalendarTbody.innerHTML = `<tr><td colspan="3" class="loading-row">Ошибка: ${escapeHtml(error.message)}</td></tr>`;
|
||||
manageCalendarTbody.innerHTML = `<tr><td colspan="4" class="loading-row">Ошибка: ${escapeHtml(error.message)}</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAssignmentsList() {
|
||||
if (!currentAssignments.length) {
|
||||
manageCalendarTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Календарные графики не назначены</td></tr>';
|
||||
manageCalendarTbody.innerHTML = '<tr><td colspan="4" class="loading-row">Календарные графики не назначены</td></tr>';
|
||||
return;
|
||||
}
|
||||
manageCalendarTbody.innerHTML = currentAssignments.map(assignment => `
|
||||
<tr>
|
||||
<td>${escapeHtml(assignment.academicYearTitle)}</td>
|
||||
<td>${escapeHtml(assignment.calendarTitle)}</td>
|
||||
<td>${renderAssignmentSubjects(assignment.subjects || [])}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-danger btn-delete-assignment" data-id="${assignment.id}">Удалить</button>
|
||||
</td>
|
||||
@@ -547,6 +548,26 @@ export async function initGroups() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderAssignmentSubjects(subjects) {
|
||||
if (!subjects.length) {
|
||||
return '<span class="muted">Не привязаны</span>';
|
||||
}
|
||||
const grouped = subjects.reduce((acc, subject) => {
|
||||
const key = String(subject.semesterNumber || '-');
|
||||
if (!acc.has(key)) acc.set(key, []);
|
||||
acc.get(key).push(subject);
|
||||
return acc;
|
||||
}, new Map());
|
||||
return `<div class="assignment-subjects">${Array.from(grouped.entries())
|
||||
.sort(([a], [b]) => Number(a) - Number(b))
|
||||
.map(([semesterNumber, items]) => `
|
||||
<div class="assignment-subject-semester">
|
||||
<strong>${escapeHtml(semesterNumber)} семестр</strong>
|
||||
<span>${items.map(subject => escapeHtml(subjectLabel(subject))).join(', ')}</span>
|
||||
</div>
|
||||
`).join('')}</div>`;
|
||||
}
|
||||
|
||||
async function saveAssignment(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('manage-calendar-alert');
|
||||
@@ -602,6 +623,11 @@ export async function initGroups() {
|
||||
return name ? `${code} — ${name}` : code;
|
||||
}
|
||||
|
||||
function subjectLabel(subject) {
|
||||
const code = subject.subjectCode ? `${subject.subjectCode} · ` : '';
|
||||
return `${code}${subject.subjectName || subject.subjectId || 'Без названия'}`;
|
||||
}
|
||||
|
||||
function statusLabel(group) {
|
||||
if (group.status === 'ARCHIVED') return 'Архив';
|
||||
return group.studyStateName || (group.active ? 'Активна' : 'Неактивна');
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
<div class="academic-calendar-tabs" role="tablist" aria-label="Разделы календарных учебных графиков">
|
||||
<button type="button" class="academic-calendar-tab active" data-calendar-tab="manage" role="tab" aria-selected="true" aria-controls="academic-calendar-tab-manage">Графики</button>
|
||||
<button type="button" class="academic-calendar-tab" data-calendar-tab="grid" role="tab" aria-selected="false" aria-controls="academic-calendar-tab-grid">Сетки</button>
|
||||
<button type="button" class="academic-calendar-tab" data-calendar-tab="subjects" role="tab" aria-selected="false" aria-controls="academic-calendar-tab-subjects">Дисциплины</button>
|
||||
</div>
|
||||
|
||||
<section class="academic-calendar-tab-panel active" id="academic-calendar-tab-manage" data-calendar-tab-panel="manage" role="tabpanel">
|
||||
<div class="card create-card">
|
||||
<h2 id="academic-year-form-title">Учебный год</h2>
|
||||
<form id="academic-year-form" novalidate>
|
||||
@@ -152,6 +159,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="academic-calendar-tab-panel" id="academic-calendar-tab-grid" data-calendar-tab-panel="grid" role="tabpanel" hidden>
|
||||
<div class="card create-card">
|
||||
<h2>Редактор годового графика</h2>
|
||||
<div class="form-row">
|
||||
@@ -191,6 +201,49 @@
|
||||
<div id="calendar-totals" class="calendar-totals"></div>
|
||||
<div id="calendar-grid" class="academic-calendar-grid"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="academic-calendar-tab-panel" id="academic-calendar-tab-subjects" data-calendar-tab-panel="subjects" role="tabpanel" hidden>
|
||||
<div class="card create-card">
|
||||
<div class="card-header-row">
|
||||
<h2>Дисциплины графика</h2>
|
||||
<button type="button" class="btn btn-md btn-secondary" id="calendar-subject-refresh">Обновить</button>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="calendar-subject-calendar">График</label>
|
||||
<select id="calendar-subject-calendar">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="calendar-subject-semester">Семестр</label>
|
||||
<select id="calendar-subject-semester">
|
||||
<option value="">Выберите график</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="calendar-subject-id">Дисциплина</label>
|
||||
<select id="calendar-subject-id">
|
||||
<option value="">Загрузка...</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="button" class="btn btn-md btn-secondary" id="calendar-subject-add">Добавить</button>
|
||||
<button type="button" class="btn btn-md btn-primary" id="calendar-subject-save">Сохранить</button>
|
||||
</div>
|
||||
<div class="form-alert" id="calendar-subject-alert" role="alert"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Привязки по семестрам</h2>
|
||||
<span id="calendar-subject-count" class="muted"></span>
|
||||
</div>
|
||||
<div id="calendar-subject-summary" class="calendar-subject-summary">
|
||||
<div class="loading-row">Выберите календарный график</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="modal-overlay" id="calendar-day-dialog">
|
||||
<div class="modal-content card calendar-day-dialog-card">
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
|
||||
<!-- Модальное окно назначения Календарного графика -->
|
||||
<div class="modal-overlay" id="modal-manage-calendar">
|
||||
<div class="modal-content card" style="max-width: 800px; width: 90%;">
|
||||
<div class="modal-content card" style="max-width: 960px; width: 90%;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<div>
|
||||
<h2>Календарные графики группы</h2>
|
||||
@@ -232,12 +232,13 @@
|
||||
<tr>
|
||||
<th>Учебный год</th>
|
||||
<th>График</th>
|
||||
<th>Дисциплины</th>
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="manage-calendar-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
<td colspan="4" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
Reference in New Issue
Block a user