feat: улучшить учебные периоды и просмотр расписаний
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user