исправил настройку временных слотов

This commit is contained in:
Zuev
2026-05-02 01:34:21 +03:00
parent 3cb311f469
commit 44cfe8ab6e
10 changed files with 390 additions and 154 deletions

View File

@@ -2,10 +2,17 @@
export class CustomSelect {
constructor(originalSelect) {
if (originalSelect.classList.contains('custom-select-initialized')) return;
if (originalSelect.classList.contains('custom-select-initialized')) {
if (originalSelect.customSelectInstance) {
return originalSelect.customSelectInstance;
}
originalSelect.classList.remove('custom-select-initialized');
}
this.originalSelect = originalSelect;
this.originalSelect.classList.add('custom-select-initialized');
this.originalSelect.customSelectInstance = this;
this.layerParent = this.originalSelect.closest('.settings-card, .card');
// Hide original but keep it accessible for form submissions and JS
this.originalSelect.style.display = 'none';
@@ -163,6 +170,7 @@ export class CustomSelect {
this.closeAll(); // Close other open dropdowns
if (!isOpen) {
this.layerParent?.classList.add('select-layer-active');
this.wrapper.classList.add('open');
// Scroll selected item into view
const selectedItem = this.menu.querySelector('.selected');
@@ -178,10 +186,14 @@ export class CustomSelect {
document.querySelectorAll('.custom-select-wrapper.open').forEach(wrapper => {
wrapper.classList.remove('open');
});
document.querySelectorAll('.select-layer-active').forEach(layer => {
layer.classList.remove('select-layer-active');
});
}
close() {
this.wrapper.classList.remove('open');
this.layerParent?.classList.remove('select-layer-active');
}
handleItemClick(e, index) {
@@ -198,12 +210,28 @@ export class CustomSelect {
// Global initializer
export function initAllCustomDropdowns(root = document) {
const selects = root.querySelectorAll('select:not(.custom-select-initialized)');
const selects = root.querySelectorAll('select:not(.custom-select-initialized):not([data-native-select="true"])');
selects.forEach(select => {
new CustomSelect(select);
});
}
export function refreshCustomDropdowns(...selects) {
selects.filter(Boolean).forEach(select => {
if (select.dataset.nativeSelect === 'true') {
return;
}
if (select.customSelectInstance) {
select.customSelectInstance.rebuildMenu();
return;
}
if (select.classList.contains('custom-select-initialized')) {
select.classList.remove('custom-select-initialized');
}
new CustomSelect(select);
});
}
// Observe DOM for automatically picking up new select elements
export function startDropdownAutoObserver() {
const observer = new MutationObserver((mutations) => {

View File

@@ -67,6 +67,9 @@ export async function initAcademicCalendar() {
const calendarDayDialogDay = document.getElementById('calendar-day-dialog-day');
const calendarDayDialogActivitySummary = document.getElementById('calendar-day-dialog-activity-summary');
const calendarDayDialogActivitySelect = document.getElementById('calendar-day-dialog-activity');
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');
let academicYears = [];
let semesters = [];
@@ -75,12 +78,21 @@ export async function initAcademicCalendar() {
let studyForms = [];
let activityTypes = [];
let calendars = [];
let timeSlotScopes = [];
let timeSlotDateAssignments = [];
let activeCalendarDay = null;
bindEvents();
try {
await Promise.all([loadBaseLists(), loadYears(), loadStudyForms(), loadActivityTypes()]);
await Promise.all([
loadBaseLists(),
loadYears(),
loadStudyForms(),
loadActivityTypes(),
loadTimeSlotScopes(),
loadTimeSlotDateAssignments()
]);
await loadCalendars();
} catch (error) {
showAlert('academic-calendar-alert', error.message || 'Ошибка загрузки календарных графиков', 'error');
@@ -126,6 +138,8 @@ export async function initAcademicCalendar() {
updateCalendarDayDialogSummary(activeCalendarDay);
renderCalendarTotals();
});
calendarDayDialogTimeSaveButton?.addEventListener('click', saveTimeSlotDateAssignment);
calendarDayDialogTimeClearButton?.addEventListener('click', clearTimeSlotDateAssignment);
academicYearForm?.addEventListener('submit', saveAcademicYear);
semesterForm?.addEventListener('submit', saveSemester);
@@ -309,6 +323,14 @@ export async function initAcademicCalendar() {
populateActivitySelects();
}
async function loadTimeSlotScopes() {
timeSlotScopes = await api.get('/api/admin/time-slots/scopes');
}
async function loadTimeSlotDateAssignments() {
timeSlotDateAssignments = await api.get('/api/admin/time-slots/date-assignments');
}
async function loadCalendars() {
calendarsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Загрузка...</td></tr>';
try {
@@ -432,6 +454,7 @@ export async function initAcademicCalendar() {
return;
}
try {
await loadTimeSlotDateAssignments();
const rows = await api.get(`/api/admin/academic-calendars/${calendar.id}/grid`);
const gridRows = rows.length ? rows : buildDefaultGrid(calendar);
renderCalendarGrid(gridRows);
@@ -573,12 +596,15 @@ export async function initAcademicCalendar() {
data-date="${row.date}"
data-week="${row.weekNumber}"
data-day="${row.dayOfWeek}"
data-time-assignment-id="${timeAssignmentForDate(row.date)?.id || ''}"
data-time-scope-id="${timeAssignmentForDate(row.date)?.scopeId || ''}"
role="button"
tabindex="0"
aria-label="${escapeHtml(`${formatShortDate(row.date)} ${dayName}, ${row.weekNumber} неделя, код ${code}`)}"
title="${escapeHtml(`${formatLongDate(row.date)}, ${dayName}, ${row.weekNumber} неделя, ${code} - ${activity.name || ''}`)}"
style="--activity-color:${escapeHtml(color)}">
<strong class="calendar-day-code">${escapeHtml(code)}</strong>
${timeAssignmentForDate(row.date) ? '<span class="calendar-day-time-marker">вр.</span>' : ''}
<select class="calendar-day-activity custom-select-initialized" aria-hidden="true" tabindex="-1">${options(activityTypes.map(toActivityOption), selected)}</select>
</td>
`;
@@ -594,7 +620,9 @@ export async function initAcademicCalendar() {
activityTypes.map(toActivityOption),
day.querySelector('.calendar-day-activity').value
);
populateTimeScopeDialogSelect(day.dataset.date);
syncSelects(calendarDayDialogActivitySelect);
syncSelects(calendarDayDialogTimeScopeSelect);
updateCalendarDayDialogSummary(day);
calendarDayDialog.classList.add('open');
}
@@ -619,6 +647,77 @@ export async function initAcademicCalendar() {
`;
}
async function saveTimeSlotDateAssignment() {
if (!activeCalendarDay) return;
hideAlert('calendar-day-dialog-time-alert');
const scopeId = calendarDayDialogTimeScopeSelect.value;
if (!scopeId) {
await clearTimeSlotDateAssignment();
return;
}
try {
await api.post('/api/admin/time-slots/date-assignments', {
date: activeCalendarDay.dataset.date,
scopeId: Number(scopeId)
});
await loadTimeSlotDateAssignments();
updateTimeAssignmentVisuals(activeCalendarDay.dataset.date);
populateTimeScopeDialogSelect(activeCalendarDay.dataset.date);
syncSelects(calendarDayDialogTimeScopeSelect);
showAlert('calendar-day-dialog-time-alert', 'Ручная сетка применена к дате', 'success');
} catch (error) {
showAlert('calendar-day-dialog-time-alert', error.message || 'Ошибка применения временной сетки', 'error');
}
}
async function clearTimeSlotDateAssignment() {
if (!activeCalendarDay) return;
hideAlert('calendar-day-dialog-time-alert');
const assignment = timeAssignmentForDate(activeCalendarDay.dataset.date);
if (!assignment) {
calendarDayDialogTimeScopeSelect.value = '';
syncSelects(calendarDayDialogTimeScopeSelect);
showAlert('calendar-day-dialog-time-alert', 'Для даты нет ручной сетки', 'success');
return;
}
try {
await api.delete('/api/admin/time-slots/date-assignments/' + assignment.id);
await loadTimeSlotDateAssignments();
updateTimeAssignmentVisuals(activeCalendarDay.dataset.date);
populateTimeScopeDialogSelect(activeCalendarDay.dataset.date);
syncSelects(calendarDayDialogTimeScopeSelect);
showAlert('calendar-day-dialog-time-alert', 'Ручная сетка убрана', 'success');
} catch (error) {
showAlert('calendar-day-dialog-time-alert', error.message || 'Ошибка удаления ручной сетки', 'error');
}
}
function populateTimeScopeDialogSelect(date) {
const assignment = timeAssignmentForDate(date);
const manualScopes = timeSlotScopes.filter(scope => scope.applyMode === 'MANUAL');
const automaticLabel = automaticTimeScopeLabel(date);
calendarDayDialogTimeScopeSelect.innerHTML = `<option value="">${escapeHtml(automaticLabel)}</option>` +
manualScopes.map(scope => `<option value="${scope.id}">${escapeHtml(scope.name)}</option>`).join('');
calendarDayDialogTimeScopeSelect.value = assignment?.scopeId || '';
calendarDayDialogTimeSaveButton.disabled = manualScopes.length === 0 && !assignment;
calendarDayDialogTimeClearButton.disabled = !assignment;
}
function updateTimeAssignmentVisuals(date) {
const assignment = timeAssignmentForDate(date);
calendarGrid.querySelectorAll(`.calendar-day[data-date="${date}"]`).forEach(day => {
day.dataset.timeAssignmentId = assignment?.id || '';
day.dataset.timeScopeId = assignment?.scopeId || '';
const marker = day.querySelector('.calendar-day-time-marker');
if (assignment && !marker) {
day.insertAdjacentHTML('beforeend', '<span class="calendar-day-time-marker">вр.</span>');
}
if (!assignment && marker) {
marker.remove();
}
});
}
function updateCalendarDayVisual(day) {
if (!day) return;
const select = day.querySelector('.calendar-day-activity');
@@ -784,6 +883,20 @@ export async function initAcademicCalendar() {
return activityTypes.find(type => String(type.id) === String(id));
}
function timeAssignmentForDate(date) {
return timeSlotDateAssignments.find(item => item.date === date);
}
function automaticTimeScopeLabel(date) {
const jsDate = new Date(`${date}T00:00:00`);
const jsDay = jsDate.getDay();
const isoDay = jsDay === 0 ? 7 : jsDay;
const weekdayScope = timeSlotScopes.find(scope => scope.applyMode === 'WEEKDAY' && Number(scope.dayOfWeek) === isoDay);
const defaultScope = timeSlotScopes.find(scope => scope.applyMode === 'DEFAULT');
const scopeName = weekdayScope?.name || defaultScope?.name || 'базовая сетка';
return `По правилу даты (${scopeName})`;
}
function semesterLabel(semester) {
const type = normalizeSemesterType(semester.semesterType);
return `${semester.academicYearTitle || '-'}, ${SEMESTER_LABELS[type] || type} (${semester.startDate} - ${semester.endDate})`;