import { useEffect, useState } from 'react'; import { readUrlState, writeUrlState } from '../../admin/js/url-state.js'; import { trackUxEvent } from '../../telemetry.js'; import { logoutAndRedirect } from '../shared/auth-client.js'; import { SESSION_PHASES } from '../shared/session-policy.js'; import { useAuthorizedSession } from '../shared/use-authorized-session.js'; import { AsyncCombobox } from '../shared/ui/AsyncCombobox.jsx'; import { AsyncState } from '../shared/ui/AsyncState.jsx'; import { buildScheduleOverviewModel } from '../../schedule-overview.js'; import { ScheduleOverview } from '../shared/ui/ScheduleOverview.jsx'; import { WeekGrid } from './WeekGrid.jsx'; import { addDays, buildWeekStatusMessage, fetchGroupOptions, fetchSchedule, formatWeekLabel, groupOptionDescription, GROUP_STORAGE_KEY, OVERVIEW_DAYS, parseIsoDate, resolveSelectedGroup, startOfWeek, toIsoDate, weekEndIso } from './student-service.js'; const THEME_STORAGE_KEY = 'theme'; export function StudentApp() { const sessionState = useAuthorizedSession(['STUDENT']); useEffect(() => { document.documentElement.dataset.theme = localStorage.getItem(THEME_STORAGE_KEY) || 'light'; }, []); if (sessionState.phase === SESSION_PHASES.loading) { return (
); } if (sessionState.phase !== SESSION_PHASES.ready) { return (
); } return ; } function StudentWorkspace() { const [initialUrlState] = useState(() => readUrlState({ studentGroup: '', studentDate: '' })); const [weekStart, setWeekStart] = useState(() => startOfWeek( initialUrlState.studentDate ? parseIsoDate(initialUrlState.studentDate) : new Date() )); const [group, setGroup] = useState(null); const [groupsState, setGroupsState] = useState({ kind: 'loading' }); const [groupsReload, setGroupsReload] = useState(0); const [scheduleState, setScheduleState] = useState({ kind: 'loading' }); const [overviewState, setOverviewState] = useState({ kind: 'loading' }); const [overviewReload, setOverviewReload] = useState(0); const weekStartIso = toIsoDate(weekStart); const groupId = group ? String(group.id) : ''; useEffect(() => { let active = true; (async () => { setGroupsState({ kind: 'loading' }); const savedGroupId = initialUrlState.studentGroup || localStorage.getItem(GROUP_STORAGE_KEY) || ''; try { const groups = await fetchGroupOptions({ selectedId: savedGroupId }); if (!active) return; if (!groups.length) { setGroupsState({ kind: 'empty' }); setOverviewState({ kind: 'empty', message: 'Нет доступных групп' }); return; } const selected = resolveSelectedGroup(groups, savedGroupId); applyGroup(selected); setGroupsState({ kind: 'ready' }); } catch { if (!active) return; setGroupsState({ kind: 'error' }); setOverviewState({ kind: 'error', message: 'Не удалось загрузить ближайшие занятия' }); } })(); return () => { active = false; }; }, [groupsReload, initialUrlState]); useEffect(() => { writeUrlState({ studentDate: weekStartIso }); }, [weekStartIso]); useEffect(() => { if (!group) return undefined; let active = true; (async () => { setScheduleState({ kind: 'loading' }); try { const lessons = await fetchSchedule({ groupId: String(group.id), startDate: weekStartIso, endDate: weekEndIso(weekStart) }); if (!active) return; setScheduleState({ kind: 'ready', lessons }); trackUxEvent('student_schedule_loaded', { lessons: lessons.length, startDate: weekStartIso, groupId: String(group.id) }); } catch { if (!active) return; setScheduleState({ kind: 'error' }); } })(); return () => { active = false; }; }, [group, weekStart, weekStartIso]); useEffect(() => { if (!group) return undefined; let active = true; (async () => { setOverviewState({ kind: 'loading' }); const now = new Date(); try { const lessons = await fetchSchedule({ groupId: String(group.id), startDate: toIsoDate(now), endDate: toIsoDate(addDays(now, OVERVIEW_DAYS)) }); if (!active) return; setOverviewState({ kind: 'ready', lessons, now }); const overview = buildScheduleOverviewModel(lessons, now); trackUxEvent('student_schedule_overview_loaded', { todayCount: overview.todayLessons.length, hasNext: Boolean(overview.nextLesson), groupId: String(group.id) }); } catch { if (!active) return; setOverviewState({ kind: 'error', message: 'Не удалось загрузить ближайшие занятия' }); } })(); return () => { active = false; }; }, [group, overviewReload]); function applyGroup(nextGroup) { setGroup(nextGroup); localStorage.setItem(GROUP_STORAGE_KEY, String(nextGroup.id)); writeUrlState({ studentGroup: String(nextGroup.id) }); } function handleGroupSelect(nextGroup) { if (!nextGroup) return; applyGroup(nextGroup); trackUxEvent('student_group_selected', { groupId: String(nextGroup.id) }); } function moveWeek(delta) { setWeekStart(current => addDays(current, delta * 7)); } function goToCurrentWeek() { setWeekStart(startOfWeek(new Date())); } function handleDateChange(event) { if (event.target.value) { setWeekStart(startOfWeek(parseIsoDate(event.target.value))); } } function toggleTheme() { const next = document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark'; document.documentElement.dataset.theme = next; localStorage.setItem(THEME_STORAGE_KEY, next); } function handleLogout(event) { event.preventDefault(); void logoutAndRedirect(); } return (

Расписание студента

{formatWeekLabel(weekStart)}
Выйти
{groupsState.kind === 'ready' && group ? ( fetchGroupOptions({ query, selectedId })} onSelect={handleGroupSelect} optionDescription={groupOptionDescription} optionLabel={option => option.name} optionValue={option => option.id} selectedLabel={group.name} selectedValue={groupId} /> ) : ( )}
{overviewState.kind === 'ready' ? ( ) : (
{overviewState.kind === 'empty' ? ( ) : overviewState.kind === 'error' ? ( setOverviewReload(reload => reload + 1)} /> ) : ( )}
)}
{statusMessage(scheduleState, groupsState)}
{groupsState.kind === 'error' ? ( setGroupsReload(reload => reload + 1)} /> ) : scheduleState.kind === 'ready' ? ( ) : scheduleState.kind === 'error' ? ( ) : ( )}
); } function groupSelectPlaceholder(kind) { if (kind === 'empty') return 'Группы не найдены'; if (kind === 'error') return 'Ошибка загрузки групп'; return 'Загрузка групп...'; } function statusMessage(scheduleState, groupsState) { if (groupsState.kind === 'error') return 'Не удалось загрузить группы'; if (groupsState.kind === 'empty') return 'Выберите группу'; if (scheduleState.kind === 'error') return 'Не удалось загрузить расписание'; if (scheduleState.kind === 'ready') return buildWeekStatusMessage(scheduleState.lessons); return 'Загрузка расписания...'; }