import { clearAuthState, fetchWithAuth, logoutSession, restoreSession } from '../auth-session.js'; import { AsyncCombobox } from '../admin/js/async-combobox.js'; import { readUrlState, writeUrlState } from '../admin/js/url-state.js'; import { renderScheduleOverview, renderScheduleOverviewState } from '../schedule-overview.js'; import { trackUxEvent } from '../telemetry.js'; (async () => { const session = await restoreSession(); if (!session || session.role !== 'STUDENT') { window.location.replace('/'); return; } const groupSelect = document.getElementById('group-select'); const grid = document.getElementById('schedule-grid'); const statusLine = document.getElementById('status-line'); const weekLabel = document.getElementById('week-label'); const datePicker = document.getElementById('date-picker'); const scheduleOverview = document.getElementById('schedule-overview'); const pageState = readUrlState({ studentGroup: '', studentDate: '' }); let weekStart = startOfWeek(pageState.studentDate ? parseIsoDate(pageState.studentDate) : new Date()); let groupCombobox = null; document.getElementById('logout-link').addEventListener('click', async (event) => { event.preventDefault(); await logoutSession(); window.location.replace('/'); }); document.getElementById('theme-toggle-local').addEventListener('click', () => { const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; document.documentElement.dataset.theme = next; localStorage.setItem('theme', next); }); document.documentElement.dataset.theme = localStorage.getItem('theme') || 'light'; document.getElementById('prev-week').addEventListener('click', () => moveWeek(-1)); document.getElementById('today-week').addEventListener('click', goToCurrentWeek); document.getElementById('next-week').addEventListener('click', () => moveWeek(1)); datePicker.addEventListener('change', () => { if (datePicker.value) { weekStart = startOfWeek(parseIsoDate(datePicker.value)); loadSchedule(); } }); groupSelect.addEventListener('change', () => { localStorage.setItem('studentGroupId', groupSelect.value); writeUrlState({ studentGroup: groupSelect.value }); loadSchedule(); loadScheduleOverview(); trackUxEvent('student_group_selected', { groupId: groupSelect.value }); }); init(); async function init() { await loadGroups(); await Promise.all([loadSchedule(), loadScheduleOverview()]); } async function loadGroups() { try { const savedGroupId = pageState.studentGroup || localStorage.getItem('studentGroupId') || ''; const groups = await fetchGroupOptions('', savedGroupId); if (!groups.length) { groupSelect.innerHTML = ''; renderScheduleOverviewState(scheduleOverview, { kind: 'empty', message: 'Нет доступных групп' }); return; } const selected = groups.find(group => String(group.id) === String(savedGroupId)) || groups[0]; groupSelect.innerHTML = ``; groupSelect.value = String(selected.id); localStorage.setItem('studentGroupId', groupSelect.value); writeUrlState({ studentGroup: groupSelect.value }); groupCombobox = new AsyncCombobox(groupSelect, { allowEmpty: false, placeholder: 'Найдите группу по названию или специальности', emptyLabel: 'Подходящие группы не найдены', load: fetchGroupOptions, optionDescription: group => [ group.specialtyCode, group.specialtyProfileName, group.course ? `${group.course} курс` : '' ].filter(Boolean).join(' · ') }); } catch (error) { groupSelect.innerHTML = ''; showStatus(error.message, true); renderScheduleOverviewState(scheduleOverview, { kind: 'error', message: error.message, onRetry: init }); } } function fetchGroupOptions(query = '', selectedId = '') { const params = new URLSearchParams({ query, limit: '20' }); if (selectedId) params.set('selectedId', selectedId); return fetchJson(`/api/groups/options?${params}`); } function goToCurrentWeek() { weekStart = startOfWeek(new Date()); loadSchedule(); } async function loadScheduleOverview() { const groupId = groupSelect.value; if (!groupId) { renderScheduleOverviewState(scheduleOverview, { kind: 'empty', message: 'Выберите группу, чтобы увидеть ближайшие занятия' }); return; } renderScheduleOverviewState(scheduleOverview, { kind: 'loading' }); const now = new Date(); const startDate = toIsoDate(now); const endDate = toIsoDate(addDays(now, 14)); try { const lessons = await fetchJson(`/api/schedule?groupId=${encodeURIComponent(groupId)}&startDate=${startDate}&endDate=${endDate}`); const overview = renderScheduleOverview(scheduleOverview, lessons, { role: 'student', now, onGoToday: goToCurrentWeek }); trackUxEvent('student_schedule_overview_loaded', { ...overview, groupId }); } catch (error) { renderScheduleOverviewState(scheduleOverview, { kind: 'error', message: error.message, onRetry: loadScheduleOverview }); } } async function loadSchedule() { const groupId = groupSelect.value; const startDate = toIsoDate(weekStart); const endDate = toIsoDate(addDays(weekStart, 6)); datePicker.value = startDate; weekLabel.textContent = `${formatDate(weekStart)} - ${formatDate(addDays(weekStart, 6))}`; writeUrlState({ studentDate: startDate }); renderEmptyWeek('Загрузка...'); if (!groupId) { showStatus('Выберите группу', false); renderEmptyWeek('Нет выбранной группы'); return; } try { const lessons = await fetchJson(`/api/schedule?groupId=${encodeURIComponent(groupId)}&startDate=${startDate}&endDate=${endDate}`); renderWeek(lessons); showStatus(lessons.length ? `Найдено занятий: ${lessons.length}` : 'На этой неделе занятий нет', false); trackUxEvent('student_schedule_loaded', { lessons: lessons.length, startDate, groupId }); } catch (error) { renderEmptyWeek('Ошибка загрузки'); showStatus(error.message, true); } } function renderWeek(lessons) { const lessonsByDate = new Map(); lessons.forEach(lesson => { if (!lessonsByDate.has(lesson.date)) lessonsByDate.set(lesson.date, []); lessonsByDate.get(lesson.date).push(lesson); }); grid.innerHTML = daysOfWeek().map(date => { const key = toIsoDate(date); const dayLessons = lessonsByDate.get(key) || []; return renderDay(date, dayLessons); }).join(''); } function renderEmptyWeek(message) { grid.innerHTML = daysOfWeek().map(date => renderDay(date, [], message)).join(''); } function renderDay(date, lessons, emptyMessage = 'Занятий нет') { return `
${escapeHtml(dayName(date))}
${escapeHtml(formatDate(date))}
${lessons.length ? lessons.map(renderLesson).join('') : `
${escapeHtml(emptyMessage)}
`}
`; } function renderLesson(lesson) { const groups = Array.isArray(lesson.groupNames) ? lesson.groupNames.join(', ') : ''; const subgroups = Array.isArray(lesson.subgroupNames) && lesson.subgroupNames.length ? lesson.subgroupNames.join(', ') : lesson.subgroupName; return `
${escapeHtml(trimTime(lesson.startTime))} - ${escapeHtml(trimTime(lesson.endTime))}
${escapeHtml(lesson.subjectName)}
${escapeHtml(lesson.lessonTypeName)} ${subgroups ? `${escapeHtml(subgroups)}` : ''} ${escapeHtml(lesson.lessonFormat)} ${escapeHtml(lesson.teacherName)} ${escapeHtml(lesson.classroomName)} ${groups ? `${escapeHtml(groups)}` : ''}
`; } function moveWeek(delta) { weekStart = addDays(weekStart, delta * 7); loadSchedule(); } function daysOfWeek() { return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index)); } async function fetchJson(url) { const response = await fetchWithAuth(url); const data = await response.json().catch(() => null); if (response.status === 401) { clearAuthState(); window.location.replace('/'); throw new Error('Требуется повторный вход в систему'); } if (!response.ok) { throw new Error(data?.message || `Ошибка HTTP: ${response.status}`); } return data; } function showStatus(message, isError) { statusLine.textContent = message; statusLine.classList.toggle('error', isError); } function startOfWeek(date) { const copy = new Date(date.getFullYear(), date.getMonth(), date.getDate()); const day = copy.getDay() || 7; copy.setDate(copy.getDate() - day + 1); return copy; } function parseIsoDate(value) { const [year, month, day] = String(value || '').split('-').map(Number); return year && month && day ? new Date(year, month - 1, day) : new Date(); } function addDays(date, days) { const copy = new Date(date); copy.setDate(copy.getDate() + days); return copy; } function toIsoDate(date) { return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } function formatDate(date) { return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' }); } function dayName(date) { const value = date.toLocaleDateString('ru-RU', { weekday: 'long' }); return value.charAt(0).toUpperCase() + value.slice(1); } function trimTime(value) { return String(value || '').slice(0, 5); } function escapeHtml(value) { return String(value ?? '') .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } })();