Много разных изменений

This commit is contained in:
Zuev
2026-08-20 00:55:27 +03:00
parent 3599d850f6
commit a1c4ee4298
78 changed files with 4698 additions and 428 deletions

View File

@@ -4,6 +4,10 @@ import {
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();
@@ -16,7 +20,10 @@ import {
const statusLine = document.getElementById('status-line');
const weekLabel = document.getElementById('week-label');
const datePicker = document.getElementById('date-picker');
let weekStart = startOfWeek(new Date());
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();
@@ -32,46 +39,96 @@ import {
document.documentElement.dataset.theme = localStorage.getItem('theme') || 'light';
document.getElementById('prev-week').addEventListener('click', () => moveWeek(-1));
document.getElementById('today-week').addEventListener('click', () => {
weekStart = startOfWeek(new Date());
loadSchedule();
});
document.getElementById('today-week').addEventListener('click', goToCurrentWeek);
document.getElementById('next-week').addEventListener('click', () => moveWeek(1));
datePicker.addEventListener('change', () => {
if (datePicker.value) {
weekStart = startOfWeek(new Date(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 loadSchedule();
await Promise.all([loadSchedule(), loadScheduleOverview()]);
}
async function loadGroups() {
try {
const groups = await fetchJson('/api/groups');
const savedGroupId = pageState.studentGroup || localStorage.getItem('studentGroupId') || '';
const groups = await fetchGroupOptions('', savedGroupId);
if (!groups.length) {
groupSelect.innerHTML = '<option value="">Группы не найдены</option>';
renderScheduleOverviewState(scheduleOverview, { kind: 'empty', message: 'Нет доступных групп' });
return;
}
const savedGroupId = localStorage.getItem('studentGroupId');
groupSelect.innerHTML = groups.map(group => (
`<option value="${escapeHtml(group.id)}">${escapeHtml(group.name)}</option>`
)).join('');
groupSelect.value = savedGroupId && groups.some(group => String(group.id) === savedGroupId)
? savedGroupId
: String(groups[0].id);
const selected = groups.find(group => String(group.id) === String(savedGroupId)) || groups[0];
groupSelect.innerHTML = `<option value="${escapeHtml(selected.id)}" selected>${escapeHtml(selected.name)}</option>`;
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 = '<option value="">Ошибка загрузки групп</option>';
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
});
}
}
@@ -81,6 +138,7 @@ import {
const endDate = toIsoDate(addDays(weekStart, 6));
datePicker.value = startDate;
weekLabel.textContent = `${formatDate(weekStart)} - ${formatDate(addDays(weekStart, 6))}`;
writeUrlState({ studentDate: startDate });
renderEmptyWeek('Загрузка...');
if (!groupId) {
@@ -93,6 +151,7 @@ import {
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);
@@ -187,6 +246,11 @@ import {
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);
@@ -219,4 +283,3 @@ import {
.replaceAll("'", '&#039;');
}
})();

View File

@@ -5,6 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Расписание студента</title>
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="../schedule-overview.css">
</head>
<body>
<main class="shell">
@@ -43,6 +44,7 @@
<input class="date-input" id="date-picker" type="date" aria-label="Дата недели">
</section>
<section class="schedule-overview" id="schedule-overview" aria-live="polite"></section>
<div class="status" id="status-line"></div>
<section class="schedule" id="schedule-grid" aria-live="polite"></section>
</main>