import { api } from '../api.js'; import { escapeHtml, showAlert, hideAlert } from '../utils.js'; const ROLE_DEPARTMENT = 'DEPARTMENT'; const PARITY_LABELS = { BOTH: 'каждая', ODD: 'нечётная', EVEN: 'чётная' }; export async function initScheduleView() { const role = localStorage.getItem('role'); const userDepartmentId = localStorage.getItem('departmentId'); const startInput = document.getElementById('schedule-view-start'); const endInput = document.getElementById('schedule-view-end'); const loadButton = document.getElementById('schedule-view-load'); const departmentField = document.getElementById('schedule-view-department-field'); const departmentSelect = document.getElementById('schedule-view-department'); const tables = document.getElementById('schedule-view-tables'); const countLabel = document.getElementById('schedule-view-count'); setDefaultDates(startInput, endInput); loadButton?.addEventListener('click', loadData); try { await loadDictionaries(role, userDepartmentId); if (role === ROLE_DEPARTMENT) { departmentField.hidden = true; departmentSelect.value = userDepartmentId || ''; } await loadData(); } catch (error) { setTableState(error.message || 'Ошибка загрузки справочников'); } async function loadData() { hideAlert('schedule-view-alert'); setTableState('Загрузка...'); countLabel.textContent = 'Загрузка...'; const params = new URLSearchParams({ startDate: startInput.value, endDate: endInput.value }); addOptional(params, 'groupId', valueOf('schedule-view-group')); addOptional(params, 'teacherId', valueOf('schedule-view-teacher')); addOptional(params, 'classroomId', valueOf('schedule-view-classroom')); addOptional(params, 'departmentId', role === ROLE_DEPARTMENT ? userDepartmentId : departmentSelect.value); addOptional(params, 'subjectId', valueOf('schedule-view-subject')); addOptional(params, 'lessonTypeId', valueOf('schedule-view-lesson-type')); addOptional(params, 'parity', valueOf('schedule-view-parity')); try { const lessons = await api.get(`/api/schedule/search?${params}`); renderLessons(lessons); } catch (error) { const message = error.message || 'Ошибка загрузки расписания'; showAlert('schedule-view-alert', message, 'error'); setTableState(message); countLabel.textContent = '0 занятий'; } } function renderLessons(lessons) { countLabel.textContent = lessonCountLabel(lessons.length); if (!lessons.length) { setTableState('Занятия не найдены'); return; } const slots = collectSlots(lessons); const lessonsByDateSlot = groupLessonsByDateSlot(lessons); const weeks = groupDatesByWeek(lessons.map(lesson => lesson.date)); tables.innerHTML = weeks .map(week => renderWeekTable(week, slots, lessonsByDateSlot)) .join(''); } function renderWeekTable(week, slots, lessonsByDateSlot) { const rows = week.dates.map(date => ` ${escapeHtml(dayTitle(date, lessonsByDateSlot))} ${escapeHtml(formatDate(date))} ${slots.map(slot => renderSlotCell(date, slot, lessonsByDateSlot)).join('')} `).join(''); return `

${escapeHtml(week.label)}

${escapeHtml(weekCaption(week.dates))}
${slots.map(renderSlotHeader).join('')} ${rows}
Время Дата
`; } function renderSlotHeader(slot) { return ` ${escapeHtml(slot.orderLabel)} ${escapeHtml(slot.timeLabel)} ${slot.id ? `ID ${escapeHtml(slot.id)}` : ''} `; } function renderSlotCell(date, slot, lessonsByDateSlot) { const lessons = lessonsByDateSlot.get(dateSlotKey(date, slot.key)) || []; return ` ${lessons.length ? lessons.map(renderLessonCard).join('') : 'Свободно'} `; } function renderLessonCard(lesson) { const subgroupText = (lesson.subgroupNames || []).join(', '); const meta = [ lesson.lessonTypeName, lesson.lessonFormat, PARITY_LABELS[lesson.parity] || lesson.parity ].filter(Boolean).join(' · '); return `
${escapeHtml(lesson.subjectName || 'Без дисциплины')}
${escapeHtml((lesson.groupNames || []).join(', ') || 'Группа не указана')}
${subgroupText ? `
${escapeHtml(subgroupText)}
` : ''}
${escapeHtml(lesson.teacherName || 'Преподаватель не указан')}
${escapeHtml(lesson.classroomName || 'Аудитория не указана')}
${meta ? `
${escapeHtml(meta)}
` : ''} ${lesson.scheduleRuleSlotId ? `
ID слота: ${escapeHtml(lesson.scheduleRuleSlotId)}
` : ''}
`; } function setTableState(message) { if (!tables) return; tables.innerHTML = `
${escapeHtml(message)}
`; } } async function loadDictionaries(role, userDepartmentId) { const [groups, teachers, classrooms, departments, subjects, lessonTypes] = await Promise.all([ api.get('/api/groups'), api.get('/api/users/teachers'), api.get('/api/classrooms'), api.get('/api/departments'), api.get('/api/subjects'), api.get('/api/lesson-types') ]); fillSelect('schedule-view-group', groups, item => item.id, item => item.name, 'Все группы'); fillSelect('schedule-view-teacher', teachers, item => item.id, item => item.fullName || item.username, 'Все преподаватели'); fillSelect('schedule-view-classroom', classrooms, item => item.id, item => roomLabel(item), 'Все аудитории'); fillSelect('schedule-view-department', departments, item => item.id, item => item.departmentName, 'Все кафедры'); fillSelect('schedule-view-subject', subjects, item => item.id, item => item.name, 'Все дисциплины'); fillSelect('schedule-view-lesson-type', lessonTypes, item => item.id, item => item.name, 'Все типы'); if (role === ROLE_DEPARTMENT && userDepartmentId) { document.getElementById('schedule-view-department').value = userDepartmentId; } } function setDefaultDates(startInput, endInput) { const today = new Date(); const nextWeek = new Date(); nextWeek.setDate(today.getDate() + 7); startInput.value = toIsoDate(today); endInput.value = toIsoDate(nextWeek); } function fillSelect(id, items, valueFn, labelFn, emptyLabel) { const select = document.getElementById(id); if (!select) return; select.innerHTML = `` + (items || []).map(item => ``).join(''); } function addOptional(params, key, value) { if (value) params.set(key, value); } function valueOf(id) { return document.getElementById(id)?.value || ''; } function roomLabel(room) { return [room.name, room.building, room.floor ? `${room.floor} этаж` : null].filter(Boolean).join(' · '); } function formatSlot(lesson) { const order = lesson.timeSlotOrder ? `${lesson.timeSlotOrder} пара` : 'Пара'; const time = lesson.startTime && lesson.endTime ? ` (${trimTime(lesson.startTime)}-${trimTime(lesson.endTime)})` : ''; return `${order}${time}`; } function collectSlots(lessons) { const slots = new Map(); lessons.forEach(lesson => { const key = lessonSlotKey(lesson); const existing = slots.get(key); const next = { key, id: lesson.timeSlotId, orderNumber: lesson.timeSlotOrder, startTime: lesson.startTime, endTime: lesson.endTime, orderLabel: lesson.timeSlotOrder ? `${lesson.timeSlotOrder} пара` : 'Пара', timeLabel: lesson.startTime || lesson.endTime ? `${trimTime(lesson.startTime)}-${trimTime(lesson.endTime)}` : 'Время не указано' }; if (!existing || existing.timeLabel === 'Время не указано') { slots.set(key, next); } }); return Array.from(slots.values()).sort(compareSlots); } function groupLessonsByDateSlot(lessons) { const grouped = new Map(); lessons.forEach(lesson => { if (!lesson.date) return; const key = dateSlotKey(lesson.date, lessonSlotKey(lesson)); if (!grouped.has(key)) grouped.set(key, []); grouped.get(key).push(lesson); }); grouped.forEach(items => { items.sort((a, b) => naturalCompare(a.subjectName, b.subjectName) || naturalCompare(a.teacherName, b.teacherName) || naturalCompare((a.groupNames || []).join(','), (b.groupNames || []).join(','))); }); return grouped; } function groupDatesByWeek(dates) { const groups = new Map(); Array.from(new Set(dates.filter(Boolean))) .sort((a, b) => naturalCompare(a, b)) .forEach(date => { const key = weekStart(date); if (!groups.has(key)) groups.set(key, []); groups.get(key).push(date); }); return Array.from(groups.entries()).map(([startDate, weekDates]) => ({ startDate, dates: weekDates, label: `Неделя ${formatDate(startDate)} - ${formatDate(addDaysToIso(startDate, 6))}` })); } function dayTitle(date, lessonsByDateSlot) { for (const [key, lessons] of lessonsByDateSlot.entries()) { if (key.startsWith(`${date}:`) && lessons[0]?.dayName) { return lessons[0].dayName; } } return dayName(date); } function weekCaption(dates) { const first = dates[0]; const last = dates[dates.length - 1]; return `${formatDate(first)} - ${formatDate(last)}`; } function lessonSlotKey(lesson) { const value = lesson.timeSlotOrder ?? lesson.timeSlotId ?? `${lesson.startTime || ''}-${lesson.endTime || ''}`; return value ? String(value) : 'unknown'; } function dateSlotKey(date, slotKey) { return `${date}:${slotKey}`; } function weekStart(value) { const date = parseIsoDate(value); const day = date.getDay() || 7; date.setDate(date.getDate() + 1 - day); return toIsoDate(date); } function addDaysToIso(value, days) { const date = parseIsoDate(value); date.setDate(date.getDate() + days); return toIsoDate(date); } function parseIsoDate(value) { const [year, month, day] = String(value).split('-').map(Number); return new Date(year, month - 1, day); } function dayName(value) { return parseIsoDate(value).toLocaleDateString('ru-RU', { weekday: 'long' }); } function formatDate(value) { if (!value) return '-'; const [year, month, day] = String(value).split('-'); return `${day}.${month}.${year}`; } function trimTime(value) { return String(value || '').slice(0, 5); } function toIsoDate(date) { return [ date.getFullYear(), String(date.getMonth() + 1).padStart(2, '0'), String(date.getDate()).padStart(2, '0') ].join('-'); } function lessonCountLabel(count) { const tail = count % 100; if (tail >= 11 && tail <= 14) return `${count} занятий`; if (count % 10 === 1) return `${count} занятие`; if ([2, 3, 4].includes(count % 10)) return `${count} занятия`; return `${count} занятий`; } function compareSlots(a, b) { const first = Number(a.orderNumber || a.key); const second = Number(b.orderNumber || b.key); if (!Number.isNaN(first) && !Number.isNaN(second) && first !== second) { return first - second; } return naturalCompare(a.startTime, b.startTime) || naturalCompare(a.key, b.key); } function naturalCompare(a, b) { return String(a || '').localeCompare(String(b || ''), 'ru', { numeric: true, sensitivity: 'base' }); }