162 lines
7.3 KiB
JavaScript
162 lines
7.3 KiB
JavaScript
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('&', '&')
|
||
.replaceAll('<', '<')
|
||
.replaceAll('>', '>')
|
||
.replaceAll('"', '"')
|
||
.replaceAll("'", ''');
|
||
}
|