feat: улучшить учебные периоды и просмотр расписаний

This commit is contained in:
Zuev
2026-08-15 00:20:00 +03:00
parent 855d191517
commit 52d516dd8f
22 changed files with 1107 additions and 250 deletions

View File

@@ -421,6 +421,11 @@ export async function initScheduleView() {
function renderDesktopSchedule(section, range) {
const slots = collectSlots(section.lessons);
const semester = selectedSemester();
if (semester) {
renderDesktopSemesterSchedule(section, slots, semester);
return;
}
const lessonsByDateSlot = groupLessonsByDateSlot(section.lessons);
const blocks = buildCombinedWeekBlocks(range.startDate, range.endDate, section.lessons);
if (!blocks.length || !slots.length) {
@@ -432,6 +437,50 @@ export async function initScheduleView() {
.join('');
}
function renderDesktopSemesterSchedule(section, slots, semester) {
if (!slots.length) {
setTableState('Занятия не найдены');
return;
}
const lessonMatrix = buildSemesterLessonMatrix(section.lessons);
const dayHeaders = DAY_ORDER.map(day => `
<th class="room-day-cell">
<strong>${escapeHtml(dayShort(day))}</strong>
<span>${escapeHtml(DAY_FULL_LABELS[day] || '')}</span>
</th>
`).join('');
const rows = slots.map(slot => `
<tr>
<td class="room-time-cell">
<strong>${escapeHtml(slot.orderLabel)}</strong>
<span>${escapeHtml(slot.timeLabel)}</span>
${slot.id ? `<small>ID ${escapeHtml(slot.id)}</small>` : ''}
</td>
${DAY_ORDER.map(day => renderSemesterSlotCell(day, slot, lessonMatrix)).join('')}
</tr>
`).join('');
tables.innerHTML = `
<section class="schedule-view-week-panel schedule-view-semester-panel">
<div class="room-week-heading">
<h3>${escapeHtml(section.title)}</h3>
<span>${escapeHtml(semesterScheduleCaption(semester))}</span>
</div>
<div class="workload-grid-container schedule-view-grid-container">
<table class="workload-table room-week-table schedule-view-matrix-table">
<thead>
<tr>
<th class="room-time-heading">Время</th>
${dayHeaders}
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
</div>
</section>
`;
}
function renderMobileDayTabs() {
dayTabs.hidden = false;
dayTabs.innerHTML = DAY_ORDER.map(day => `
@@ -451,6 +500,11 @@ export async function initScheduleView() {
function renderMobileSchedule(section, range) {
const slots = collectSlots(section.lessons);
const semester = selectedSemester();
if (semester) {
renderMobileSemesterSchedule(section, slots, semester);
return;
}
const lessonsByDateSlot = groupLessonsByDateSlot(section.lessons);
const blocks = buildCombinedWeekBlocks(range.startDate, range.endDate, section.lessons);
if (!blocks.length || !slots.length) {
@@ -460,6 +514,34 @@ export async function initScheduleView() {
tables.innerHTML = blocks.map(block => renderMobileDayBlock(block, slots, lessonsByDateSlot)).join('');
}
function renderMobileSemesterSchedule(section, slots, semester) {
if (!slots.length) {
setTableState('Занятия не найдены');
return;
}
const lessonMatrix = buildSemesterLessonMatrix(section.lessons);
const rows = slots.map(slot => `
<article class="schedule-view-mobile-slot">
<div class="schedule-view-mobile-slot-time">
<strong>${escapeHtml(slot.orderLabel)}</strong>
<span>${escapeHtml(slot.timeLabel)}</span>
</div>
<div class="schedule-view-mobile-slot-content">
${renderSlotContent(slotStatesForSemester(state.mobileDay, slot, lessonMatrix))}
</div>
</article>
`).join('');
tables.innerHTML = `
<section class="schedule-view-mobile-day schedule-view-mobile-semester">
<div class="schedule-view-mobile-day-heading">
<strong>${escapeHtml(DAY_FULL_LABELS[state.mobileDay] || dayShort(state.mobileDay))}</strong>
<span>${escapeHtml(semesterScheduleCaption(semester))}</span>
</div>
${rows}
</section>
`;
}
function renderMobileDayBlock(block, slots, lessonsByDateSlot) {
const dayDates = block.datesByDay[state.mobileDay] || {};
const rows = slots.map(slot => {
@@ -537,6 +619,12 @@ export async function initScheduleView() {
return `<td class="room-combined-cell${singleCellClass}">${renderSlotContent(states)}</td>`;
}
function renderSemesterSlotCell(day, slot, lessonMatrix) {
const states = slotStatesForSemester(day, slot, lessonMatrix);
const singleCellClass = slotStatesEqual(states[0].state, states[1].state) ? ' room-single-cell' : '';
return `<td class="room-combined-cell${singleCellClass}">${renderSlotContent(states)}</td>`;
}
function slotStatesForDates(datesByParity, slot, lessonsByDateSlot) {
return CELL_PARITY_LAYOUT.map(({ parity, label }) => ({
parity,
@@ -545,6 +633,54 @@ export async function initScheduleView() {
}));
}
function slotStatesForSemester(day, slot, lessonMatrix) {
return CELL_PARITY_LAYOUT.map(({ parity, label }) => {
const lessons = lessonMatrix.get(`${day}:${slot.key}:${parity}`) || [];
return {
parity,
label,
state: lessons.length ? { kind: 'busy', lessons } : { kind: 'free', lessons: [] }
};
});
}
function buildSemesterLessonMatrix(lessons) {
const weekRanges = new Map();
lessons.forEach(lesson => {
const signature = lessonContentSignature(lesson);
const weekNumber = Number(lesson.weekNumber);
if (!Number.isFinite(weekNumber)) return;
const range = weekRanges.get(signature) || { startWeek: weekNumber, endWeek: weekNumber };
range.startWeek = Math.min(range.startWeek, weekNumber);
range.endWeek = Math.max(range.endWeek, weekNumber);
weekRanges.set(signature, range);
});
const matrix = new Map();
lessons.forEach(lesson => {
const day = Number(lesson.dayOfWeek) || isoDay(lesson.date);
const parity = ['ODD', 'EVEN'].includes(lesson.parity)
? lesson.parity
: (Number(lesson.weekNumber) % 2 === 0 ? 'EVEN' : 'ODD');
const signature = lessonContentSignature(lesson);
const key = `${day}:${lessonSlotKey(lesson)}:${parity}`;
if (!matrix.has(key)) matrix.set(key, new Map());
if (!matrix.get(key).has(signature)) {
const range = weekRanges.get(signature);
matrix.get(key).set(signature, {
...lesson,
displayStartWeek: range?.startWeek,
displayEndWeek: range?.endWeek
});
}
});
return new Map(Array.from(matrix.entries()).map(([key, values]) => [
key,
Array.from(values.values()).sort((a, b) => naturalCompare(a.subjectName, b.subjectName))
]));
}
function renderSlotContent(states) {
if (slotStatesEqual(states[0].state, states[1].state)) {
return renderUnifiedSlot(states[0].state);
@@ -640,6 +776,9 @@ export async function initScheduleView() {
}
function getLessonWeeksText(lesson) {
if (lesson?.displayStartWeek != null && lesson?.displayEndWeek != null) {
return formatLessonWeeksText(lesson.displayStartWeek, lesson.displayEndWeek);
}
if (!lesson || lesson.weekNumber == null || lesson.lessonTypeAcademicHours == null) {
return '';
}
@@ -667,42 +806,7 @@ export async function initScheduleView() {
if (startWeek <= 0) startWeek = 1;
if (endWeek < startWeek) endWeek = startWeek;
return formatLessonWeeksText(startWeek, endWeek, parity, semesterWeeksForLesson(lesson));
}
function formatLessonWeeksText(startWeek, endWeek, parity, semesterWeeks) {
const semesterEndWeek = lastApplicableSemesterWeek(semesterWeeks, parity);
const semesterStartWeek = firstApplicableSemesterWeek(parity);
if (semesterEndWeek && startWeek <= semesterStartWeek && endWeek >= semesterEndWeek) return '';
if (semesterEndWeek && endWeek >= semesterEndWeek) return `(с ${startWeek} нед.)`;
return startWeek === endWeek
? `(${startWeek} нед.)`
: `(с ${startWeek} по ${endWeek} нед.)`;
}
function semesterWeeksForLesson(lesson) {
const semester = (state.dictionaries.semesters || []).find(item => dateInRange(lesson.date, item.startDate, item.endDate));
if (!semester?.startDate || !semester?.endDate) return DEFAULT_SEMESTER_WEEKS;
const start = parseIsoDate(semester.startDate);
const end = parseIsoDate(semester.endDate);
const days = Math.max(1, Math.round((end - start) / 86400000) + 1);
return Math.max(1, Math.ceil(days / 7));
}
function dateInRange(date, startDate, endDate) {
return Boolean(date && startDate && endDate && date >= startDate && date <= endDate);
}
function firstApplicableSemesterWeek(parity) {
return parity === 'EVEN' ? 2 : 1;
}
function lastApplicableSemesterWeek(maxWeeks, parity) {
const totalWeeks = Number(maxWeeks || 0);
if (!totalWeeks) return 0;
if (parity === 'ODD' && totalWeeks % 2 === 0) return totalWeeks - 1;
if (parity === 'EVEN' && totalWeeks % 2 !== 0) return totalWeeks - 1;
return totalWeeks;
return formatLessonWeeksText(startWeek, endWeek);
}
function renderLessonCard(lesson) {
@@ -801,8 +905,8 @@ export async function initScheduleView() {
function selectCurrentSemester() {
if (!semesterSelect) return;
const currentSemester = findSemesterForDate(state.dictionaries.semesters, dateInput.value);
semesterSelect.value = currentSemester ? String(currentSemester.id) : '';
const defaultSemester = findDefaultSemester(state.dictionaries.semesters, dateInput.value);
semesterSelect.value = defaultSemester ? String(defaultSemester.id) : '';
synchronizeDateWithSemester();
updatePeriodLabel();
}
@@ -1107,11 +1211,42 @@ export function findSemesterForDate(semesters, date) {
return (semesters || []).find(semester => dateWithinSemester(date, semester)) || null;
}
export function findDefaultSemester(semesters, date) {
const availableSemesters = semesters || [];
return findSemesterForDate(availableSemesters, date) || availableSemesters[0] || null;
}
export function scheduleRangeForSemester(selectedDate, semester) {
const anchorDate = semester && !dateWithinSemester(selectedDate, semester)
? semester.startDate
: selectedDate;
return twoWeekRange(anchorDate);
if (semester?.startDate && semester?.endDate) {
return {
startDate: semester.startDate,
endDate: semester.endDate
};
}
return twoWeekRange(selectedDate);
}
export function formatLessonWeeksText(startWeek, endWeek) {
const start = Number(startWeek);
const end = Number(endWeek);
if (!Number.isFinite(start) || !Number.isFinite(end)) return '';
return start === end
? `(${start} нед.)`
: `(с ${start} по ${end} нед.)`;
}
function semesterScheduleCaption(semester) {
const weeks = semesterWeekCount(semester);
const weekLabel = weeks === 1 ? '1 неделя' : `недели 1–${weeks}`;
return `Весь семестр · ${weekLabel}`;
}
function semesterWeekCount(semester) {
if (!semester?.startDate || !semester?.endDate) return DEFAULT_SEMESTER_WEEKS;
const start = parseIsoDate(semester.startDate);
const end = parseIsoDate(semester.endDate);
const days = Math.max(1, Math.round((end - start) / 86400000) + 1);
return Math.max(1, Math.ceil(days / 7));
}
function dateWithinSemester(date, semester) {