3 Задача

This commit is contained in:
dipatrik10
2026-08-11 01:55:02 +03:00
parent 5276729a69
commit 519864b962
42 changed files with 4083 additions and 21 deletions

View File

@@ -124,6 +124,7 @@ export async function initSchedule() {
let timeSlots = [];
let academicYears = [];
let semesters = [];
let approvedTeacherPreferences = [];
let selectedVisualYearId = '';
let selectedVisualSemesterId = '';
let selectedVisualGroupIds = new Set();
@@ -137,7 +138,7 @@ export async function initSchedule() {
bindEvents();
try {
await Promise.all([loadBaseLists(), loadTimeSlots(), loadYears()]);
await Promise.all([loadBaseLists(), loadTimeSlots(), loadYears(), loadApprovedTeacherPreferences()]);
renderRuleGroups([]);
renderRuleSlots([{}]);
await loadRules();
@@ -185,6 +186,7 @@ export async function initSchedule() {
renderVisualSchedule();
});
ruleSemesterSelect?.addEventListener('change', () => {
updatePreferenceHints();
if (visualPeriodTouched) return;
selectDefaultVisualPeriod();
renderVisualSchedule();
@@ -252,6 +254,7 @@ export async function initSchedule() {
if (event.target.closest('.slot-subgroup-checkboxes')) {
updateSlotSubgroupText(event.target.closest('.schedule-slot-row'));
}
updatePreferenceHints();
});
ruleForm.addEventListener('submit', saveRule);
rulesTbody.addEventListener('click', handleRuleTableClick);
@@ -276,6 +279,11 @@ export async function initSchedule() {
selectDefaultVisualPeriod();
}
async function loadApprovedTeacherPreferences() {
approvedTeacherPreferences = await api.get('/api/teacher-preferences?status=APPROVED');
updatePreferenceHints();
}
async function loadRules() {
rulesTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Загрузка...</td></tr>';
try {
@@ -1383,8 +1391,55 @@ export async function initSchedule() {
${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-md ${actionClass} schedule-slot-remove" data-index="${index}">${actionLabel}</button>
<div class="schedule-preference-hint" data-preference-hint></div>
</div>
`).join('');
updatePreferenceHints();
}
function updatePreferenceHints() {
const semesterId = Number(ruleSemesterSelect?.value);
Array.from(ruleSlotsContainer?.querySelectorAll('.schedule-slot-row') || []).forEach(row => {
const hint = row.querySelector('[data-preference-hint]');
if (!hint) return;
const teacherId = Number(row.querySelector('.slot-teacher')?.value);
const dayOfWeek = Number(row.querySelector('.slot-day')?.value);
const timeSlotId = Number(row.querySelector('.slot-time')?.value);
if (!semesterId || !teacherId) {
hint.innerHTML = '';
return;
}
const teacherItems = approvedTeacherPreferences.filter(item =>
Number(item.semesterId) === semesterId && Number(item.teacherId) === teacherId
);
const exact = teacherItems.filter(item =>
Number(item.dayOfWeek) === dayOfWeek && Number(item.timeSlotId) === timeSlotId
);
const dateCount = teacherItems.filter(item => item.preferenceType === 'HARD_UNAVAILABLE' && item.preferenceDate).length;
const flags = teacherItems.filter(item => ['CONSECUTIVE', 'NO_GAPS'].includes(item.preferenceType));
const pills = [
...exact.map(item => ({
text: preferenceHintLabel(item.preferenceType),
kind: item.preferenceType === 'HARD_UNAVAILABLE' ? 'hard'
: item.preferenceType === 'SOFT_PREFERRED' ? 'preferred' : 'unwanted'
})),
...(dateCount ? [{ text: `Недоступных дат: ${dateCount}`, kind: 'dates' }] : []),
...flags.map(item => ({ text: preferenceHintLabel(item.preferenceType), kind: 'compact' }))
];
hint.innerHTML = pills.length
? `<span class="schedule-preference-caption">Пожелания преподавателя</span>${pills.map(pill => `<span class="schedule-preference-pill ${pill.kind}">${escapeHtml(pill.text)}</span>`).join('')}`
: '<span class="schedule-preference-caption muted">Согласованных пожеланий для слота нет</span>';
});
}
function preferenceHintLabel(type) {
return {
HARD_UNAVAILABLE: 'Строгая недоступность',
SOFT_PREFERRED: 'Предпочтительно',
SOFT_UNWANTED: 'Нежелательно',
CONSECUTIVE: 'Пары подряд',
NO_GAPS: 'Без окон'
}[type] || type;
}
function readRuleSlots(validate) {