Добавить дисциплины календарных графиков

This commit is contained in:
2026-06-01 23:27:37 +03:00
parent 4b05d4287c
commit 43d72c5235
17 changed files with 887 additions and 21 deletions

View File

@@ -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;
}

View File

@@ -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();
}
});
}
}

View File

@@ -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 ? 'Активна' : 'Неактивна');

View File

@@ -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">

View File

@@ -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>