комит
This commit is contained in:
180
frontend/admin/js/date-input.js
Normal file
180
frontend/admin/js/date-input.js
Normal file
@@ -0,0 +1,180 @@
|
||||
const DISPLAY_DATE_PATTERN = /^(\d{2})\.(\d{2})\.(\d{4})$/;
|
||||
const ISO_DATE_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
||||
const boundDateInputs = new WeakSet();
|
||||
|
||||
export function formatDateInputValue(value) {
|
||||
const text = String(value || '');
|
||||
const isoMatch = text.match(ISO_DATE_PATTERN);
|
||||
if (isoMatch) {
|
||||
return `${isoMatch[3]}.${isoMatch[2]}.${isoMatch[1]}`;
|
||||
}
|
||||
|
||||
const digits = text.replace(/\D/g, '').slice(0, 8);
|
||||
if (digits.length <= 2) return digits;
|
||||
if (digits.length <= 4) return `${digits.slice(0, 2)}.${digits.slice(2)}`;
|
||||
return `${digits.slice(0, 2)}.${digits.slice(2, 4)}.${digits.slice(4)}`;
|
||||
}
|
||||
|
||||
export function dateInputValueToIso(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) return '';
|
||||
|
||||
const match = text.match(DISPLAY_DATE_PATTERN);
|
||||
if (!match) {
|
||||
throw new TypeError('Введите дату полностью в формате ДД.ММ.ГГГГ (год — 4 цифры)');
|
||||
}
|
||||
|
||||
const [, dayText, monthText, yearText] = match;
|
||||
const day = Number(dayText);
|
||||
const month = Number(monthText);
|
||||
const year = Number(yearText);
|
||||
if (year < 1 || month < 1 || month > 12 || day < 1 || day > daysInMonth(month, year)) {
|
||||
throw new TypeError('Введите существующую дату в формате ДД.ММ.ГГГГ');
|
||||
}
|
||||
|
||||
return `${yearText}-${monthText}-${dayText}`;
|
||||
}
|
||||
|
||||
export function isoDateToInputValue(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (!text) return '';
|
||||
|
||||
const match = text.match(ISO_DATE_PATTERN);
|
||||
if (!match) {
|
||||
throw new TypeError('Ожидалась дата в формате ГГГГ-ММ-ДД');
|
||||
}
|
||||
|
||||
const [, year, month, day] = match;
|
||||
dateInputValueToIso(`${day}.${month}.${year}`);
|
||||
return `${day}.${month}.${year}`;
|
||||
}
|
||||
|
||||
export function bindDateInputMask(input) {
|
||||
if (!input || boundDateInputs.has(input)) return;
|
||||
boundDateInputs.add(input);
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
const rawValue = input.value;
|
||||
const caret = input.selectionStart ?? rawValue.length;
|
||||
const digitsBeforeCaret = rawValue.slice(0, caret).replace(/\D/g, '').length;
|
||||
const caretWasAtEnd = caret === rawValue.length;
|
||||
const formattedValue = formatDateInputValue(rawValue);
|
||||
|
||||
input.value = formattedValue;
|
||||
clearDateInputError(input);
|
||||
const nextCaret = caretPositionAfterDigits(formattedValue, digitsBeforeCaret, caretWasAtEnd);
|
||||
input.setSelectionRange?.(nextCaret, nextCaret);
|
||||
});
|
||||
|
||||
input.addEventListener('keydown', event => {
|
||||
if (event.key === 'Backspace'
|
||||
&& input.selectionStart === input.selectionEnd
|
||||
&& input.selectionStart > 0
|
||||
&& input.value[input.selectionStart - 1] === '.') {
|
||||
event.preventDefault();
|
||||
const nextCaret = input.selectionStart - 1;
|
||||
input.setSelectionRange(nextCaret, nextCaret);
|
||||
}
|
||||
|
||||
if (event.key === 'Delete'
|
||||
&& input.selectionStart === input.selectionEnd
|
||||
&& input.value[input.selectionStart] === '.') {
|
||||
event.preventDefault();
|
||||
const nextCaret = input.selectionStart + 1;
|
||||
input.setSelectionRange(nextCaret, nextCaret);
|
||||
}
|
||||
});
|
||||
|
||||
input.addEventListener('blur', () => {
|
||||
if (!input.value) {
|
||||
clearDateInputError(input);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const isoValue = dateInputValueToIso(input.value);
|
||||
input.value = isoDateToInputValue(isoValue);
|
||||
clearDateInputError(input);
|
||||
} catch (error) {
|
||||
setDateInputError(input, error.message);
|
||||
}
|
||||
});
|
||||
|
||||
input.form?.addEventListener('reset', () => clearDateInputError(input));
|
||||
}
|
||||
|
||||
export function readDateInputIso(input, label = 'Дата') {
|
||||
try {
|
||||
const value = dateInputValueToIso(input?.value);
|
||||
clearDateInputError(input);
|
||||
return value;
|
||||
} catch (error) {
|
||||
const message = `${label}: ${lowercaseFirst(error.message)}`;
|
||||
setDateInputError(input, message);
|
||||
input?.focus();
|
||||
throw new Error(message);
|
||||
}
|
||||
}
|
||||
|
||||
export function setDateInputIso(input, value) {
|
||||
if (!input) return;
|
||||
input.value = isoDateToInputValue(value);
|
||||
clearDateInputError(input);
|
||||
}
|
||||
|
||||
function daysInMonth(month, year) {
|
||||
const monthLengths = [
|
||||
31,
|
||||
isLeapYear(year) ? 29 : 28,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
31,
|
||||
30,
|
||||
31,
|
||||
30,
|
||||
31
|
||||
];
|
||||
return monthLengths[month - 1] || 0;
|
||||
}
|
||||
|
||||
function isLeapYear(year) {
|
||||
return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
||||
}
|
||||
|
||||
function caretPositionAfterDigits(value, digitCount, preferEnd) {
|
||||
if (digitCount <= 0) return 0;
|
||||
|
||||
let seenDigits = 0;
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
if (!/\d/.test(value[index])) continue;
|
||||
seenDigits += 1;
|
||||
if (seenDigits === digitCount) {
|
||||
let position = index + 1;
|
||||
if (preferEnd) {
|
||||
while (value[position] === '.') position += 1;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
}
|
||||
return value.length;
|
||||
}
|
||||
|
||||
function setDateInputError(input, message) {
|
||||
if (!input) return;
|
||||
input.setCustomValidity?.(message);
|
||||
input.setAttribute?.('aria-invalid', 'true');
|
||||
}
|
||||
|
||||
function clearDateInputError(input) {
|
||||
if (!input) return;
|
||||
input.setCustomValidity?.('');
|
||||
input.removeAttribute?.('aria-invalid');
|
||||
}
|
||||
|
||||
function lowercaseFirst(value) {
|
||||
const text = String(value || '');
|
||||
return text ? text[0].toLowerCase() + text.slice(1) : text;
|
||||
}
|
||||
39
frontend/admin/js/views/academic-calendar-grid.js
Normal file
39
frontend/admin/js/views/academic-calendar-grid.js
Normal file
@@ -0,0 +1,39 @@
|
||||
const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
export function academicCalendarWeekNumber(academicYearStartIso, dateIso) {
|
||||
const academicYearStart = parseIsoDate(academicYearStartIso);
|
||||
const date = parseIsoDate(dateIso);
|
||||
const daysFromYearStart = (date.timestamp - academicYearStart.timestamp) / MILLISECONDS_PER_DAY;
|
||||
|
||||
if (!Number.isInteger(daysFromYearStart) || daysFromYearStart < 0) {
|
||||
throw new RangeError('Дата ячейки не может быть раньше начала учебного года');
|
||||
}
|
||||
|
||||
const daysBeforeYearStartInFirstWeek = academicYearStart.dayOfWeek - 1;
|
||||
return Math.floor((daysBeforeYearStartInFirstWeek + daysFromYearStart) / 7) + 1;
|
||||
}
|
||||
|
||||
function parseIsoDate(value) {
|
||||
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value));
|
||||
if (!match) {
|
||||
throw new TypeError('Ожидалась дата в формате ГГГГ-ММ-ДД');
|
||||
}
|
||||
|
||||
const [, yearText, monthText, dayText] = match;
|
||||
const year = Number(yearText);
|
||||
const month = Number(monthText);
|
||||
const day = Number(dayText);
|
||||
const timestamp = Date.UTC(year, month - 1, day);
|
||||
const date = new Date(timestamp);
|
||||
if (date.getUTCFullYear() !== year
|
||||
|| date.getUTCMonth() !== month - 1
|
||||
|| date.getUTCDate() !== day) {
|
||||
throw new TypeError('Ожидалась дата в формате ГГГГ-ММ-ДД');
|
||||
}
|
||||
|
||||
const jsDayOfWeek = date.getUTCDay();
|
||||
return {
|
||||
timestamp,
|
||||
dayOfWeek: jsDayOfWeek === 0 ? 7 : jsDayOfWeek
|
||||
};
|
||||
}
|
||||
21
frontend/admin/js/views/academic-calendar-title.js
Normal file
21
frontend/admin/js/views/academic-calendar-title.js
Normal file
@@ -0,0 +1,21 @@
|
||||
const TITLE_SEPARATOR = ' — ';
|
||||
|
||||
function normalizeTitlePart(value) {
|
||||
return String(value ?? '').trim();
|
||||
}
|
||||
|
||||
export function buildAcademicCalendarTitle({
|
||||
specialtyCode,
|
||||
specialtyProfileName,
|
||||
studyFormName,
|
||||
academicYearTitle
|
||||
} = {}) {
|
||||
const parts = [
|
||||
specialtyCode,
|
||||
specialtyProfileName,
|
||||
studyFormName,
|
||||
academicYearTitle
|
||||
].map(normalizeTitlePart);
|
||||
|
||||
return parts.every(Boolean) ? parts.join(TITLE_SEPARATOR) : '';
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import { api } from '../api.js';
|
||||
import { bindDateInputMask, readDateInputIso, setDateInputIso } from '../date-input.js';
|
||||
import { escapeHtml, formatLocalDate, showAlert, hideAlert } from '../utils.js';
|
||||
import { academicCalendarWeekNumber } from './academic-calendar-grid.js';
|
||||
import { buildAcademicCalendarTitle } from './academic-calendar-title.js';
|
||||
|
||||
const DAY_OPTIONS = [
|
||||
{ value: 1, label: 'Понедельник', shortLabel: 'Пн' },
|
||||
@@ -16,6 +19,41 @@ const SEMESTER_LABELS = {
|
||||
spring: 'весенний'
|
||||
};
|
||||
|
||||
const CALENDAR_TOOLTIP_OFFSET = 14;
|
||||
const CALENDAR_TOOLTIP_VIEWPORT_PADDING = 12;
|
||||
|
||||
export function calculateCalendarTooltipPosition({
|
||||
pointerX,
|
||||
pointerY,
|
||||
tooltipWidth,
|
||||
tooltipHeight,
|
||||
viewportWidth,
|
||||
viewportHeight
|
||||
}) {
|
||||
const maxLeft = Math.max(
|
||||
CALENDAR_TOOLTIP_VIEWPORT_PADDING,
|
||||
viewportWidth - tooltipWidth - CALENDAR_TOOLTIP_VIEWPORT_PADDING
|
||||
);
|
||||
const maxTop = Math.max(
|
||||
CALENDAR_TOOLTIP_VIEWPORT_PADDING,
|
||||
viewportHeight - tooltipHeight - CALENDAR_TOOLTIP_VIEWPORT_PADDING
|
||||
);
|
||||
let left = pointerX + CALENDAR_TOOLTIP_OFFSET;
|
||||
let top = pointerY + CALENDAR_TOOLTIP_OFFSET;
|
||||
|
||||
if (left > maxLeft) {
|
||||
left = pointerX - tooltipWidth - CALENDAR_TOOLTIP_OFFSET;
|
||||
}
|
||||
if (top > maxTop) {
|
||||
top = pointerY - tooltipHeight - CALENDAR_TOOLTIP_OFFSET;
|
||||
}
|
||||
|
||||
return {
|
||||
left: Math.round(Math.max(CALENDAR_TOOLTIP_VIEWPORT_PADDING, Math.min(left, maxLeft))),
|
||||
top: Math.round(Math.max(CALENDAR_TOOLTIP_VIEWPORT_PADDING, Math.min(top, maxTop)))
|
||||
};
|
||||
}
|
||||
|
||||
export async function initAcademicCalendar() {
|
||||
const academicYearForm = document.getElementById('academic-year-form');
|
||||
const academicYearFormTitle = document.getElementById('academic-year-form-title');
|
||||
@@ -39,7 +77,6 @@ export async function initAcademicCalendar() {
|
||||
const calendarForm = document.getElementById('academic-calendar-form');
|
||||
const calendarFormTitle = document.getElementById('academic-calendar-form-title');
|
||||
const calendarIdInput = document.getElementById('academic-calendar-id');
|
||||
const calendarTitleInput = document.getElementById('academic-calendar-title');
|
||||
const calendarYearSelect = document.getElementById('academic-calendar-year');
|
||||
const calendarSpecialtySelect = document.getElementById('academic-calendar-specialty');
|
||||
const calendarProfileSelect = document.getElementById('academic-calendar-profile');
|
||||
@@ -100,6 +137,7 @@ export async function initAcademicCalendar() {
|
||||
let suppressCalendarDayClick = false;
|
||||
let isSyncingCalendarDayDialog = false;
|
||||
let calendarDayTooltip = null;
|
||||
let calendarDayTooltipPositionAnimation = null;
|
||||
|
||||
bindEvents();
|
||||
|
||||
@@ -119,6 +157,15 @@ export async function initAcademicCalendar() {
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
[
|
||||
academicYearStartInput,
|
||||
academicYearEndInput,
|
||||
semesterStartInput,
|
||||
semesterEndInput,
|
||||
fillStartInput,
|
||||
fillEndInput
|
||||
].forEach(bindDateInputMask);
|
||||
|
||||
calendarTabButtons.forEach(button => {
|
||||
button.addEventListener('click', () => activateCalendarTab(button.dataset.calendarTab));
|
||||
});
|
||||
@@ -271,12 +318,12 @@ export async function initAcademicCalendar() {
|
||||
async function saveAcademicYear(event) {
|
||||
event.preventDefault();
|
||||
hideAlert('academic-year-alert');
|
||||
const payload = {
|
||||
title: academicYearTitleInput.value.trim(),
|
||||
startDate: academicYearStartInput.value,
|
||||
endDate: academicYearEndInput.value
|
||||
};
|
||||
try {
|
||||
const payload = {
|
||||
title: academicYearTitleInput.value.trim(),
|
||||
startDate: readDateInputIso(academicYearStartInput, 'Начало учебного года'),
|
||||
endDate: readDateInputIso(academicYearEndInput, 'Окончание учебного года')
|
||||
};
|
||||
if (!payload.title || !payload.startDate || !payload.endDate) throw new Error('Заполните название и даты учебного года');
|
||||
const id = academicYearIdInput.value;
|
||||
await (id
|
||||
@@ -302,8 +349,8 @@ export async function initAcademicCalendar() {
|
||||
academicYearFormTitle.textContent = 'Редактирование учебного года';
|
||||
academicYearIdInput.value = year.id;
|
||||
academicYearTitleInput.value = year.title || '';
|
||||
academicYearStartInput.value = year.startDate || '';
|
||||
academicYearEndInput.value = year.endDate || '';
|
||||
setDateInputIso(academicYearStartInput, year.startDate);
|
||||
setDateInputIso(academicYearEndInput, year.endDate);
|
||||
hideAlert('academic-year-alert');
|
||||
academicYearForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
return;
|
||||
@@ -316,8 +363,8 @@ export async function initAcademicCalendar() {
|
||||
semesterIdInput.value = semester.id;
|
||||
semesterYearSelect.value = semester.academicYearId || '';
|
||||
semesterTypeSelect.value = normalizeSemesterType(semester.semesterType);
|
||||
semesterStartInput.value = semester.startDate || '';
|
||||
semesterEndInput.value = semester.endDate || '';
|
||||
setDateInputIso(semesterStartInput, semester.startDate);
|
||||
setDateInputIso(semesterEndInput, semester.endDate);
|
||||
syncSelects(semesterYearSelect, semesterTypeSelect);
|
||||
hideAlert('semester-alert');
|
||||
semesterForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
@@ -348,12 +395,12 @@ export async function initAcademicCalendar() {
|
||||
event.preventDefault();
|
||||
hideAlert('semester-alert');
|
||||
const yearId = semesterYearSelect.value;
|
||||
const payload = {
|
||||
semesterType: semesterTypeSelect.value,
|
||||
startDate: semesterStartInput.value,
|
||||
endDate: semesterEndInput.value
|
||||
};
|
||||
try {
|
||||
const payload = {
|
||||
semesterType: semesterTypeSelect.value,
|
||||
startDate: readDateInputIso(semesterStartInput, 'Начало семестра'),
|
||||
endDate: readDateInputIso(semesterEndInput, 'Окончание семестра')
|
||||
};
|
||||
if (!yearId || !payload.semesterType || !payload.startDate || !payload.endDate) {
|
||||
throw new Error('Заполните учебный год, тип и даты семестра');
|
||||
}
|
||||
@@ -420,7 +467,7 @@ export async function initAcademicCalendar() {
|
||||
}
|
||||
calendarsTbody.innerHTML = calendars.map(calendar => `
|
||||
<tr>
|
||||
<td>${escapeHtml(calendar.title)}</td>
|
||||
<td>${escapeHtml(buildAcademicCalendarTitle(calendar) || '-')}</td>
|
||||
<td>${escapeHtml(calendar.academicYearTitle || '-')}</td>
|
||||
<td>${escapeHtml(`${calendar.specialtyCode || ''} ${calendar.specialtyName || ''}`.trim() || '-')}</td>
|
||||
<td>${escapeHtml(calendar.specialtyProfileName || '-')}</td>
|
||||
@@ -441,7 +488,7 @@ export async function initAcademicCalendar() {
|
||||
hideAlert('academic-calendar-alert');
|
||||
const payload = {
|
||||
id: calendarIdInput.value ? Number(calendarIdInput.value) : null,
|
||||
title: calendarTitleInput.value.trim(),
|
||||
title: buildCalendarTitleFromForm(),
|
||||
academicYearId: Number(calendarYearSelect.value),
|
||||
specialtyId: Number(calendarSpecialtySelect.value),
|
||||
specialtyProfileId: Number(calendarProfileSelect.value),
|
||||
@@ -457,7 +504,8 @@ export async function initAcademicCalendar() {
|
||||
const saved = id
|
||||
? await api.put('/api/admin/academic-calendars/' + id, payload)
|
||||
: await api.post('/api/admin/academic-calendars', payload);
|
||||
showAlert('academic-calendar-alert', `График "${escapeHtml(saved.title)}" сохранён`, 'success');
|
||||
const savedTitle = buildAcademicCalendarTitle(saved) || payload.title;
|
||||
showAlert('academic-calendar-alert', `График "${escapeHtml(savedTitle)}" сохранён`, 'success');
|
||||
resetCalendarForm();
|
||||
await loadCalendars();
|
||||
} catch (error) {
|
||||
@@ -477,7 +525,6 @@ export async function initAcademicCalendar() {
|
||||
if (!calendar) return;
|
||||
calendarFormTitle.textContent = 'Редактирование календарного графика';
|
||||
calendarIdInput.value = calendar.id;
|
||||
calendarTitleInput.value = calendar.title || '';
|
||||
calendarYearSelect.value = calendar.academicYearId || '';
|
||||
calendarSpecialtySelect.value = calendar.specialtyId || '';
|
||||
populateProfileSelect(calendarProfileSelect, calendarSpecialtySelect.value, calendar.specialtyProfileId);
|
||||
@@ -721,12 +768,19 @@ export async function initAcademicCalendar() {
|
||||
function applyCalendarFill() {
|
||||
hideAlert('calendar-editor-alert');
|
||||
const activityId = fillActivitySelect.value;
|
||||
const start = fillStartInput.value;
|
||||
const end = fillEndInput.value;
|
||||
if (!activityId || !start || !end) {
|
||||
if (!activityId || !fillStartInput.value || !fillEndInput.value) {
|
||||
showAlert('calendar-editor-alert', 'Выберите даты и код активности для заполнения', 'error');
|
||||
return;
|
||||
}
|
||||
let start;
|
||||
let end;
|
||||
try {
|
||||
start = readDateInputIso(fillStartInput, 'Дата начала диапазона');
|
||||
end = readDateInputIso(fillEndInput, 'Дата окончания диапазона');
|
||||
} catch (error) {
|
||||
showAlert('calendar-editor-alert', error.message, 'error');
|
||||
return;
|
||||
}
|
||||
if (end < start) {
|
||||
showAlert('calendar-editor-alert', 'Дата окончания не может быть раньше даты начала', 'error');
|
||||
return;
|
||||
@@ -1051,12 +1105,33 @@ export async function initAcademicCalendar() {
|
||||
const tooltip = ensureCalendarDayTooltip();
|
||||
tooltip.innerHTML = renderCalendarDayTooltip(day);
|
||||
tooltip.classList.add('visible');
|
||||
positionCalendarDayTooltip(tooltip, day, event);
|
||||
}
|
||||
|
||||
function hideCalendarDayTooltip() {
|
||||
calendarDayTooltip?.classList.remove('visible');
|
||||
}
|
||||
|
||||
function positionCalendarDayTooltip(tooltip, day, event) {
|
||||
const dayRect = day.getBoundingClientRect();
|
||||
const pointerX = Number.isFinite(event?.clientX) ? event.clientX : dayRect.right;
|
||||
const pointerY = Number.isFinite(event?.clientY) ? event.clientY : dayRect.bottom;
|
||||
const tooltipRect = tooltip.getBoundingClientRect();
|
||||
const { left, top } = calculateCalendarTooltipPosition({
|
||||
pointerX,
|
||||
pointerY,
|
||||
tooltipWidth: tooltipRect.width,
|
||||
tooltipHeight: tooltipRect.height,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight
|
||||
});
|
||||
calendarDayTooltipPositionAnimation?.cancel();
|
||||
calendarDayTooltipPositionAnimation = tooltip.animate(
|
||||
[{ left: `${left}px`, top: `${top}px` }],
|
||||
{ duration: 0, fill: 'forwards' }
|
||||
);
|
||||
}
|
||||
|
||||
function ensureCalendarDayTooltip() {
|
||||
if (calendarDayTooltip) return calendarDayTooltip;
|
||||
calendarDayTooltip = document.createElement('div');
|
||||
@@ -1156,13 +1231,13 @@ export async function initAcademicCalendar() {
|
||||
const rows = [];
|
||||
const dates = enumerateDates(calendar.academicYearStartDate, calendar.academicYearEndDate);
|
||||
for (let course = 1; course <= calendar.courseCount; course += 1) {
|
||||
dates.forEach((date, index) => {
|
||||
dates.forEach(date => {
|
||||
rows.push({
|
||||
id: null,
|
||||
calendarId: calendar.id,
|
||||
courseNumber: course,
|
||||
date: date.iso,
|
||||
weekNumber: Math.floor(index / 7) + 1,
|
||||
weekNumber: academicCalendarWeekNumber(calendar.academicYearStartDate, date.iso),
|
||||
dayOfWeek: date.dayOfWeek,
|
||||
activityTypeId: theoryActivityId(),
|
||||
activityCode: 'Т'
|
||||
@@ -1285,7 +1360,9 @@ export async function initAcademicCalendar() {
|
||||
function populateEditorCalendarSelect() {
|
||||
const current = editorCalendarSelect.value;
|
||||
editorCalendarSelect.innerHTML = '<option value="">Выберите график</option>' +
|
||||
calendars.map(calendar => `<option value="${calendar.id}">${escapeHtml(calendar.title)}</option>`).join('');
|
||||
calendars.map(calendar =>
|
||||
`<option value="${calendar.id}">${escapeHtml(buildAcademicCalendarTitle(calendar) || '-')}</option>`
|
||||
).join('');
|
||||
if (current && calendars.some(calendar => String(calendar.id) === String(current))) {
|
||||
editorCalendarSelect.value = current;
|
||||
}
|
||||
@@ -1297,7 +1374,7 @@ export async function initAcademicCalendar() {
|
||||
const current = subjectCalendarSelect.value;
|
||||
subjectCalendarSelect.innerHTML = calendars.length
|
||||
? '<option value="">Выберите график</option>' + calendars.map(calendar =>
|
||||
`<option value="${calendar.id}">${escapeHtml(calendar.title)}</option>`
|
||||
`<option value="${calendar.id}">${escapeHtml(buildAcademicCalendarTitle(calendar) || '-')}</option>`
|
||||
).join('')
|
||||
: '<option value="">Нет графиков</option>';
|
||||
if (current && calendars.some(calendar => String(calendar.id) === String(current))) {
|
||||
@@ -1425,11 +1502,40 @@ export async function initAcademicCalendar() {
|
||||
}
|
||||
|
||||
function specialityLabel(speciality) {
|
||||
const code = speciality.specialityCode || speciality.specialtyCode || speciality.specialty_code || speciality.id;
|
||||
const code = specialityCode(speciality) || speciality.id;
|
||||
const name = speciality.specialityName || speciality.name || '';
|
||||
return name ? `${code} - ${name}` : String(code);
|
||||
}
|
||||
|
||||
function specialityCode(speciality) {
|
||||
return speciality?.specialityCode
|
||||
|| speciality?.specialtyCode
|
||||
|| speciality?.speciality_code
|
||||
|| speciality?.specialty_code
|
||||
|| '';
|
||||
}
|
||||
|
||||
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 || '-';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
import { buildAcademicCalendarTitle } from './academic-calendar-title.js';
|
||||
|
||||
async function fetchEducationForms() {
|
||||
try {
|
||||
@@ -384,7 +385,7 @@ export async function initGroups() {
|
||||
: [];
|
||||
manageCalendarSelect.innerHTML = matching.length
|
||||
? '<option value="">Выберите график</option>' + matching.map(calendar =>
|
||||
`<option value="${calendar.id}">${escapeHtml(calendar.title)}</option>`
|
||||
`<option value="${calendar.id}">${escapeHtml(buildAcademicCalendarTitle(calendar) || '-')}</option>`
|
||||
).join('')
|
||||
: '<option value="">Нет подходящих графиков</option>';
|
||||
syncSelects(manageCalendarSelect);
|
||||
@@ -767,7 +768,7 @@ export async function initGroups() {
|
||||
manageCalendarTbody.innerHTML = currentAssignments.map(assignment => `
|
||||
<tr>
|
||||
<td>${escapeHtml(assignment.academicYearTitle)}</td>
|
||||
<td>${escapeHtml(assignment.calendarTitle)}</td>
|
||||
<td>${escapeHtml(assignmentCalendarTitle(assignment))}</td>
|
||||
<td>${renderAssignmentSubjects(assignment.subjects || [])}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-danger btn-delete-assignment" data-id="${assignment.id}">Удалить</button>
|
||||
@@ -776,6 +777,13 @@ export async function initGroups() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function assignmentCalendarTitle(assignment) {
|
||||
const calendar = calendars.find(item =>
|
||||
String(item.id) === String(assignment.calendarId)
|
||||
);
|
||||
return buildAcademicCalendarTitle(calendar) || assignment.calendarTitle || '-';
|
||||
}
|
||||
|
||||
function renderAssignmentSubjects(subjects) {
|
||||
if (!subjects.length) {
|
||||
return '<span class="muted">Не привязаны</span>';
|
||||
|
||||
Reference in New Issue
Block a user