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 `