#5 8 9 10 12 13 17 - готовы. #15 - тест

This commit is contained in:
Zuev
2026-07-27 02:23:26 +03:00
parent 3e8722a26b
commit a78a93f2f0
21 changed files with 413 additions and 102 deletions

View File

@@ -448,26 +448,25 @@ export async function initAcademicCalendar() {
}
async function loadCalendars() {
calendarsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Загрузка...</td></tr>';
calendarsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Загрузка...</td></tr>';
try {
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>`;
calendarsTbody.innerHTML = `<tr><td colspan="6" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
throw error;
}
}
function renderCalendars() {
if (!calendars.length) {
calendarsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Календарные графики не созданы</td></tr>';
calendarsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Календарные графики не созданы</td></tr>';
return;
}
calendarsTbody.innerHTML = calendars.map(calendar => `
<tr>
<td>${escapeHtml(buildAcademicCalendarTitle(calendar) || '-')}</td>
<td>${escapeHtml(calendar.academicYearTitle || '-')}</td>
<td>${escapeHtml(`${calendar.specialtyCode || ''} ${calendar.specialtyName || ''}`.trim() || '-')}</td>
<td>${escapeHtml(calendar.specialtyProfileName || '-')}</td>
@@ -488,7 +487,6 @@ export async function initAcademicCalendar() {
hideAlert('academic-calendar-alert');
const payload = {
id: calendarIdInput.value ? Number(calendarIdInput.value) : null,
title: buildCalendarTitleFromForm(),
academicYearId: Number(calendarYearSelect.value),
specialtyId: Number(calendarSpecialtySelect.value),
specialtyProfileId: Number(calendarProfileSelect.value),
@@ -496,7 +494,7 @@ export async function initAcademicCalendar() {
courseCount: Number(calendarCourseCountInput.value)
};
try {
if (!payload.title || !payload.academicYearId || !payload.specialtyId
if (!payload.academicYearId || !payload.specialtyId
|| !payload.specialtyProfileId || !payload.studyFormId || !payload.courseCount) {
throw new Error('Заполните все поля календарного графика');
}
@@ -504,7 +502,7 @@ export async function initAcademicCalendar() {
const saved = id
? await api.put('/api/admin/academic-calendars/' + id, payload)
: await api.post('/api/admin/academic-calendars', payload);
const savedTitle = buildAcademicCalendarTitle(saved) || payload.title;
const savedTitle = buildAcademicCalendarTitle(saved);
showAlert('academic-calendar-alert', `График "${escapeHtml(savedTitle)}" сохранён`, 'success');
resetCalendarForm();
await loadCalendars();
@@ -1126,9 +1124,13 @@ export async function initAcademicCalendar() {
viewportHeight: window.innerHeight
});
calendarDayTooltipPositionAnimation?.cancel();
const positionedTransform = `translate3d(${left}px, ${top}px, 0) scale(1)`;
calendarDayTooltipPositionAnimation = tooltip.animate(
[{ left: `${left}px`, top: `${top}px` }],
{ duration: 0, fill: 'forwards' }
[
{ transform: positionedTransform },
{ transform: positionedTransform }
],
{ duration: 1, fill: 'forwards' }
);
}
@@ -1515,27 +1517,6 @@ export async function initAcademicCalendar() {
|| '';
}
function buildCalendarTitleFromForm() {
const specialty = specialties.find(item =>
String(item.id) === String(calendarSpecialtySelect.value)
);
const profile = (profilesBySpecialty.get(String(calendarSpecialtySelect.value)) || [])
.find(item => String(item.id) === String(calendarProfileSelect.value));
const studyForm = studyForms.find(item =>
String(item.id) === String(calendarStudyFormSelect.value)
);
const academicYear = academicYears.find(item =>
String(item.id) === String(calendarYearSelect.value)
);
return buildAcademicCalendarTitle({
specialtyCode: specialityCode(specialty),
specialtyProfileName: profile?.name,
studyFormName: studyForm?.name,
academicYearTitle: academicYear?.title
});
}
function dayLabel(value) {
return DAY_OPTIONS.find(day => String(day.value) === String(value))?.label || '-';
}

View File

@@ -37,6 +37,7 @@ const CELL_PARITY_LAYOUT = [
];
const MOBILE_QUERY = '(max-width: 760px)';
const DEFAULT_SEMESTER_WEEKS = 22;
const SCHEDULE_REFRESH_PROMPT = 'Нажмите «Показать», чтобы обновить расписание';
export async function initScheduleView() {
const session = getSession();
@@ -89,7 +90,7 @@ export async function initScheduleView() {
targetSelect?.addEventListener('change', () => {
clearInactiveEntityValues();
updateEntityFieldVisibility();
resetResults('Нажмите «Показать», чтобы обновить расписание');
resetResults(SCHEDULE_REFRESH_PROMPT);
});
loadButton?.addEventListener('click', loadData);
previousPeriodButton?.addEventListener('click', () => movePeriod(-1));
@@ -100,16 +101,21 @@ export async function initScheduleView() {
nextPeriodButton?.addEventListener('click', () => movePeriod(1));
dateInput?.addEventListener('change', () => {
updatePeriodLabel();
resetResults('Нажмите «Показать», чтобы обновить расписание');
resetResults(SCHEDULE_REFRESH_PROMPT);
});
semesterSelect?.addEventListener('change', () => {
synchronizeDateWithSemester();
updatePeriodLabel();
resetResults('Нажмите «Показать», чтобы обновить расписание');
resetResults(SCHEDULE_REFRESH_PROMPT);
});
resultPreviousButton?.addEventListener('click', () => moveResult(-1));
resultNextButton?.addEventListener('click', () => moveResult(1));
tables?.addEventListener('click', event => {
const reloadButton = event.target.closest('[data-schedule-view-reload]');
if (reloadButton) {
loadData();
return;
}
const button = event.target.closest('[data-schedule-override-edit]');
if (!button) return;
const lesson = state.lessons.find(item =>
@@ -743,9 +749,20 @@ export async function initScheduleView() {
`;
}
function setTableState(message) {
function setTableState(message, showReloadButton = false) {
if (!tables) return;
tables.innerHTML = `<div class="loading-row schedule-view-state">${escapeHtml(message)}</div>`;
tables.innerHTML = `
<div class="loading-row schedule-view-state${showReloadButton ? ' schedule-view-state-actionable' : ''}">
<span>${escapeHtml(message)}</span>
${showReloadButton ? `
<button type="button"
class="btn btn-md btn-primary schedule-view-state-reload"
data-schedule-view-reload>
Показать расписание
</button>
` : ''}
</div>
`;
}
function resetResults(message) {
@@ -757,7 +774,7 @@ export async function initScheduleView() {
renderResultNavigation();
tableTitle.textContent = 'Расписание';
tableCaption.textContent = message;
setTableState(message);
setTableState(message, message === SCHEDULE_REFRESH_PROMPT);
countLabel.textContent = '0 занятий';
dayTabs.hidden = true;
dayTabs.innerHTML = '';

View File

@@ -30,6 +30,10 @@ const SEMESTER_LABELS = {
const WHOLE_GROUP_SUBGROUP_VALUE = '__whole_group__';
const ACADEMIC_HOURS_PER_SLOT = 2;
export function scheduleSlotActionLabel(slotCount) {
return slotCount === 1 ? 'Очистить' : 'Удалить';
}
const LESSON_TYPE_LIMITS = [
{
key: 'lecture',
@@ -1364,7 +1368,11 @@ export async function initSchedule() {
}
function renderRuleSlots(slots) {
ruleSlotsContainer.innerHTML = (slots || [{}]).map((slot, index) => `
const normalizedSlots = slots?.length ? slots : [{}];
const isSingleSlot = normalizedSlots.length === 1;
const actionLabel = scheduleSlotActionLabel(normalizedSlots.length);
const actionClass = isSingleSlot ? 'btn-ghost' : 'btn-danger';
ruleSlotsContainer.innerHTML = normalizedSlots.map((slot, index) => `
<div class="schedule-slot-row ${isLaboratorySlot(slot) ? 'has-subgroup' : ''}" data-index="${index}">
<div class="form-group"><label>День</label><select class="slot-day" required>${options(DAY_OPTIONS, slot.dayOfWeek)}</select></div>
<div class="form-group"><label>Чётность</label><select class="slot-parity" required>${options(Object.entries(PARITY_LABELS).map(([value, label]) => ({ value, label })), slot.parity || 'BOTH')}</select></div>
@@ -1374,7 +1382,7 @@ export async function initSchedule() {
<div class="form-group"><label>Тип</label><select class="slot-lesson-type" required>${options(orderedLessonTypes().map(toLessonTypeOption), slot.lessonTypeId)}</select></div>
${isLaboratorySlot(slot) ? renderSubgroupField(slot, index) : ''}
<div class="form-group"><label>Формат</label><select class="slot-format" required>${options([{ value: 'Очно', label: 'Очно' }, { value: 'Онлайн', label: 'Онлайн' }], slot.lessonFormat || 'Очно')}</select></div>
<button type="button" class="btn btn-sm btn-danger schedule-slot-remove" data-index="${index}">Удалить</button>
<button type="button" class="btn btn-md ${actionClass} schedule-slot-remove" data-index="${index}">${actionLabel}</button>
</div>
`).join('');
}