Files
magistr/frontend/schedule-overview.js
2026-08-20 00:55:27 +03:00

162 lines
7.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export function renderScheduleOverviewState(container, { kind = 'loading', message = '', onRetry = null } = {}) {
if (!container) return;
const labels = {
loading: message || 'Загружаем ближайшие занятия…',
empty: message || 'В ближайшие дни занятий нет',
error: message || 'Не удалось загрузить ближайшие занятия'
};
container.innerHTML = `
<div class="schedule-overview-state is-${escapeHtml(kind)}" role="${kind === 'error' ? 'alert' : 'status'}">
${kind === 'loading' ? '<span class="schedule-overview-spinner" aria-hidden="true"></span>' : ''}
<span>${escapeHtml(labels[kind] || message)}</span>
${kind === 'error' && onRetry ? '<button class="btn btn-md btn-secondary" type="button" data-overview-retry>Повторить</button>' : ''}
</div>
`;
container.querySelector('[data-overview-retry]')?.addEventListener('click', onRetry);
}
export function renderScheduleOverview(container, lessons, { role = 'student', now = new Date(), onGoToday = null } = {}) {
if (!container) return { todayCount: 0, hasNext: false };
const { todayIso, todayLessons, nextLesson } = buildScheduleOverviewModel(lessons, now);
container.innerHTML = `
<header class="schedule-overview-head">
<div>
<span class="schedule-overview-kicker">Быстрый обзор</span>
<h2>${escapeHtml(formatLongDate(now))}</h2>
</div>
<button class="btn btn-md btn-secondary" type="button" data-overview-today>Открыть текущую неделю</button>
</header>
<div class="schedule-overview-grid">
<article class="schedule-overview-card is-today">
<div class="schedule-overview-card-head">
<div>
<span>Сегодня</span>
<h3>${todayLessons.length ? pluralLessons(todayLessons.length) : 'Свободный день'}</h3>
</div>
<span class="schedule-overview-count">${todayLessons.length}</span>
</div>
<div class="schedule-overview-list">
${todayLessons.length
? todayLessons.map(lesson => renderCompactLesson(lesson, role)).join('')
: '<p class="schedule-overview-empty">На сегодня занятий нет.</p>'}
</div>
</article>
<article class="schedule-overview-card is-next">
<div class="schedule-overview-card-head">
<div>
<span>Следующая пара</span>
<h3>${nextLesson ? escapeHtml(relativeDate(nextLesson.date, todayIso)) : 'Пока ничего не запланировано'}</h3>
</div>
${nextLesson ? `<span class="schedule-overview-badge">${lessonStart(nextLesson) <= now ? 'Идёт сейчас' : escapeHtml(trimTime(nextLesson.startTime))}</span>` : ''}
</div>
${nextLesson ? renderNextLesson(nextLesson, role) : '<p class="schedule-overview-empty">В ближайшие 14 дней занятий нет.</p>'}
</article>
</div>
`;
container.querySelector('[data-overview-today]')?.addEventListener('click', onGoToday || (() => {}));
return { todayCount: todayLessons.length, hasNext: Boolean(nextLesson) };
}
export function buildScheduleOverviewModel(lessons, now = new Date()) {
const ordered = [...(lessons || [])].sort((left, right) => lessonStart(left) - lessonStart(right));
const todayIso = toIsoDate(now);
return {
todayIso,
todayLessons: ordered.filter(lesson => lesson.date === todayIso),
nextLesson: ordered.find(lesson => lessonEnd(lesson) >= now) || null
};
}
function renderCompactLesson(lesson, role) {
return `
<div class="schedule-overview-lesson">
<time>${escapeHtml(trimTime(lesson.startTime))}</time>
<div>
<strong>${escapeHtml(lesson.subjectName || 'Занятие')}</strong>
<small>${escapeHtml(lessonMeta(lesson, role).join(' · ') || 'Подробности уточняются')}</small>
</div>
</div>
`;
}
function renderNextLesson(lesson, role) {
return `
<div class="schedule-overview-next">
<div class="schedule-overview-next-time">
<strong>${escapeHtml(trimTime(lesson.startTime))}</strong>
<span>до ${escapeHtml(trimTime(lesson.endTime))}</span>
</div>
<div>
<h4>${escapeHtml(lesson.subjectName || 'Занятие')}</h4>
<p>${escapeHtml(lessonMeta(lesson, role).join(' · ') || 'Подробности уточняются')}</p>
${lesson.lessonTypeName ? `<span class="schedule-overview-type">${escapeHtml(lesson.lessonTypeName)}</span>` : ''}
</div>
</div>
`;
}
function lessonMeta(lesson, role) {
const groups = Array.isArray(lesson.groupNames) ? lesson.groupNames.join(', ') : '';
const subgroup = Array.isArray(lesson.subgroupNames) ? lesson.subgroupNames.join(', ') : lesson.subgroupName;
return role === 'teacher'
? [groups || subgroup, lesson.classroomName].filter(Boolean)
: [lesson.teacherName, lesson.classroomName, subgroup].filter(Boolean);
}
function lessonStart(lesson) {
return new Date(`${lesson.date}T${normalizeTime(lesson.startTime)}`);
}
function lessonEnd(lesson) {
return new Date(`${lesson.date}T${normalizeTime(lesson.endTime || lesson.startTime)}`);
}
function normalizeTime(value) {
const text = String(value || '00:00:00');
return text.length === 5 ? `${text}:00` : text;
}
function relativeDate(value, todayIso) {
if (value === todayIso) return 'Сегодня';
const today = parseIsoDate(todayIso);
const target = parseIsoDate(value);
const dayDifference = Math.round((target - today) / 86400000);
if (dayDifference === 1) return 'Завтра';
return target.toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long' });
}
function parseIsoDate(value) {
const [year, month, day] = String(value).split('-').map(Number);
return new Date(year, month - 1, day);
}
function toIsoDate(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
function formatLongDate(date) {
const value = date.toLocaleDateString('ru-RU', { weekday: 'long', day: 'numeric', month: 'long' });
return value.charAt(0).toUpperCase() + value.slice(1);
}
function pluralLessons(count) {
const mod10 = count % 10;
const mod100 = count % 100;
const noun = mod10 === 1 && mod100 !== 11 ? 'занятие' : mod10 >= 2 && mod10 <= 4 && (mod100 < 12 || mod100 > 14) ? 'занятия' : 'занятий';
return `${count} ${noun}`;
}
function trimTime(value) {
return String(value || '').slice(0, 5);
}
function escapeHtml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}