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

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

@@ -0,0 +1,161 @@
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;');
}