Добавил визуальную матрицу расписания групп
This commit is contained in:
@@ -17,12 +17,18 @@ const PARITY_LABELS = {
|
||||
EVEN: 'чётная'
|
||||
};
|
||||
|
||||
const CELL_PARITY_LAYOUT = [
|
||||
{ parity: 'ODD', label: 'Нечётная' },
|
||||
{ parity: 'EVEN', label: 'Чётная' }
|
||||
];
|
||||
|
||||
const SEMESTER_LABELS = {
|
||||
autumn: 'осенний',
|
||||
spring: 'весенний'
|
||||
};
|
||||
|
||||
const WHOLE_GROUP_SUBGROUP_VALUE = '__whole_group__';
|
||||
const ACADEMIC_HOURS_PER_SLOT = 2;
|
||||
|
||||
const LESSON_TYPE_LIMITS = [
|
||||
{
|
||||
@@ -70,6 +76,15 @@ export async function initSchedule() {
|
||||
const slotAddButton = document.getElementById('schedule-slot-add');
|
||||
const rulesTbody = document.getElementById('schedule-tbody');
|
||||
const refreshRulesButton = document.getElementById('schedule-refresh');
|
||||
const visualToggleButton = document.getElementById('schedule-visual-toggle');
|
||||
const visualCloseButton = document.getElementById('schedule-visual-close');
|
||||
const visualDrawer = document.getElementById('schedule-visual-drawer');
|
||||
const visualBackdrop = document.getElementById('schedule-visual-backdrop');
|
||||
const visualStage = document.getElementById('schedule-visual-stage');
|
||||
const visualSummary = document.getElementById('schedule-visual-summary');
|
||||
const visualGroupFilter = document.getElementById('schedule-visual-group-filter');
|
||||
const visualUseFormButton = document.getElementById('schedule-visual-use-form');
|
||||
const visualSelectAllButton = document.getElementById('schedule-visual-select-all');
|
||||
|
||||
let rules = [];
|
||||
let subjects = [];
|
||||
@@ -80,6 +95,8 @@ export async function initSchedule() {
|
||||
let subgroups = [];
|
||||
let timeSlots = [];
|
||||
let semesters = [];
|
||||
let selectedVisualGroupIds = new Set();
|
||||
let visualGroupFilterTouched = false;
|
||||
|
||||
bindEvents();
|
||||
|
||||
@@ -96,6 +113,23 @@ export async function initSchedule() {
|
||||
refreshRulesButton?.addEventListener('click', loadRules);
|
||||
ruleResetButton?.addEventListener('click', resetRuleForm);
|
||||
slotAddButton?.addEventListener('click', () => renderRuleSlots([...readRuleSlots(false), {}]));
|
||||
visualToggleButton?.addEventListener('click', toggleVisualDrawer);
|
||||
visualCloseButton?.addEventListener('click', closeVisualDrawer);
|
||||
visualBackdrop?.addEventListener('click', closeVisualDrawer);
|
||||
visualGroupFilter?.addEventListener('change', handleVisualGroupFilterChange);
|
||||
visualUseFormButton?.addEventListener('click', () => {
|
||||
visualGroupFilterTouched = false;
|
||||
selectedVisualGroupIds = new Set();
|
||||
renderVisualSchedule();
|
||||
});
|
||||
visualSelectAllButton?.addEventListener('click', () => {
|
||||
selectedVisualGroupIds = new Set(collectVisualGroups().map(group => String(group.id)));
|
||||
visualGroupFilterTouched = true;
|
||||
renderVisualSchedule();
|
||||
});
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') closeVisualDrawer();
|
||||
});
|
||||
|
||||
ruleSlotsContainer.addEventListener('click', (event) => {
|
||||
const removeButton = event.target.closest('.schedule-slot-remove');
|
||||
@@ -166,14 +200,17 @@ export async function initSchedule() {
|
||||
try {
|
||||
rules = await api.get('/api/admin/schedule-rules');
|
||||
renderRules();
|
||||
renderVisualSchedule();
|
||||
} catch (error) {
|
||||
rulesTbody.innerHTML = `<tr><td colspan="7" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||||
setVisualState('Ошибка загрузки правил расписания');
|
||||
}
|
||||
}
|
||||
|
||||
function renderRules() {
|
||||
if (!rules.length) {
|
||||
rulesTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Правила расписания не созданы</td></tr>';
|
||||
renderVisualSchedule();
|
||||
return;
|
||||
}
|
||||
rulesTbody.innerHTML = rules.map(rule => `
|
||||
@@ -192,6 +229,326 @@ export async function initSchedule() {
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function toggleVisualDrawer() {
|
||||
if (!visualDrawer) return;
|
||||
if (visualDrawer.classList.contains('open')) {
|
||||
closeVisualDrawer();
|
||||
} else {
|
||||
openVisualDrawer();
|
||||
}
|
||||
}
|
||||
|
||||
function openVisualDrawer() {
|
||||
if (!visualDrawer) return;
|
||||
visualDrawer.classList.add('open');
|
||||
visualDrawer.setAttribute('aria-hidden', 'false');
|
||||
visualToggleButton?.classList.add('open');
|
||||
visualToggleButton?.setAttribute('aria-expanded', 'true');
|
||||
visualToggleButton?.setAttribute('aria-label', 'Скрыть визуальное расписание');
|
||||
if (visualToggleButton) visualToggleButton.textContent = '›';
|
||||
if (visualBackdrop) visualBackdrop.hidden = false;
|
||||
}
|
||||
|
||||
function closeVisualDrawer() {
|
||||
if (!visualDrawer) return;
|
||||
visualDrawer.classList.remove('open');
|
||||
visualDrawer.setAttribute('aria-hidden', 'true');
|
||||
visualToggleButton?.classList.remove('open');
|
||||
visualToggleButton?.setAttribute('aria-expanded', 'false');
|
||||
visualToggleButton?.setAttribute('aria-label', 'Показать визуальное расписание');
|
||||
if (visualToggleButton) visualToggleButton.textContent = '‹';
|
||||
if (visualBackdrop) visualBackdrop.hidden = true;
|
||||
}
|
||||
|
||||
function renderVisualSchedule() {
|
||||
if (!visualStage || !visualSummary) return;
|
||||
if (!rules.length) {
|
||||
visualSummary.textContent = 'Нет сохранённых правил';
|
||||
setVisualState('Сохраните правила расписания, чтобы увидеть матрицу по группам');
|
||||
return;
|
||||
}
|
||||
|
||||
const allGroups = collectVisualGroups();
|
||||
syncVisualGroupFilter(allGroups);
|
||||
const columns = allGroups.filter(group => selectedVisualGroupIds.has(String(group.id)));
|
||||
const slots = timeSlots.length ? timeSlots.map(visualSlotFromTimeSlot) : collectVisualSlotsFromRules();
|
||||
if (!allGroups.length || !slots.length) {
|
||||
visualSummary.textContent = 'Недостаточно данных для построения';
|
||||
setVisualState('У правил должны быть группы и слоты занятий');
|
||||
return;
|
||||
}
|
||||
if (!columns.length) {
|
||||
visualSummary.textContent = 'Выберите группы для просмотра';
|
||||
setVisualState('Отметьте одну или несколько групп в фильтре');
|
||||
return;
|
||||
}
|
||||
|
||||
const lessonsByCell = buildVisualLessonsByCell(selectedVisualGroupIds);
|
||||
const rows = buildVisualRows(columns, slots, lessonsByCell);
|
||||
if (!rows.length) {
|
||||
visualSummary.textContent = `${columns.length} ${groupCountLabel(columns.length)} · пар не найдено`;
|
||||
setVisualState('Для выбранных групп нет активных пар в сохранённых правилах');
|
||||
return;
|
||||
}
|
||||
|
||||
visualSummary.textContent = `${rules.length} ${ruleCountLabel(rules.length)} · ${columns.length} ${groupCountLabel(columns.length)} · ${rows.length} ${rowCountLabel(rows.length)}`;
|
||||
visualStage.innerHTML = `
|
||||
<div class="schedule-visual-table-wrap">
|
||||
<table class="schedule-visual-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="schedule-visual-day-head">День</th>
|
||||
<th class="schedule-visual-time-head">Время</th>
|
||||
${columns.map(group => `<th>${escapeHtml(group.name)}</th>`).join('')}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows.map(renderVisualRow).join('')}</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function buildVisualRows(columns, slots, lessonsByCell) {
|
||||
const rows = [];
|
||||
DAY_OPTIONS.forEach(day => {
|
||||
const dayRows = slots
|
||||
.map(slot => ({
|
||||
day,
|
||||
slot,
|
||||
cells: columns.map(group => ({
|
||||
group,
|
||||
lessons: lessonsByCell.get(visualCellKey(group.id, day.value, slot.key)) || []
|
||||
}))
|
||||
}))
|
||||
.filter(row => row.cells.some(cell => cell.lessons.length));
|
||||
|
||||
dayRows.forEach((row, index) => {
|
||||
rows.push({
|
||||
...row,
|
||||
dayRowspan: index === 0 ? dayRows.length : 0
|
||||
});
|
||||
});
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function renderVisualRow(row) {
|
||||
return `
|
||||
<tr>
|
||||
${row.dayRowspan ? `<td class="schedule-visual-day-cell" rowspan="${row.dayRowspan}">${escapeHtml(row.day.label)}</td>` : ''}
|
||||
<td class="schedule-visual-time-cell">
|
||||
<strong>${escapeHtml(row.slot.orderLabel)}</strong>
|
||||
<span>${escapeHtml(row.slot.timeLabel)}</span>
|
||||
</td>
|
||||
${row.cells.map(cell => {
|
||||
const emptyClass = cell.lessons.length ? '' : ' schedule-visual-lesson-cell-empty';
|
||||
return `<td class="schedule-visual-lesson-cell${emptyClass}">${renderVisualCellContent(cell.lessons)}</td>`;
|
||||
}).join('')}
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
function buildVisualLessonsByCell(allowedGroupIds) {
|
||||
const grouped = new Map();
|
||||
const activeWeeks = buildActiveWeeksByRuleSlot();
|
||||
rules.forEach(rule => {
|
||||
(rule.slots || []).forEach((slot, index) => {
|
||||
const slotKey = visualRuleSlotKey(slot, index);
|
||||
(rule.groupIds || []).forEach((groupId, groupIndex) => {
|
||||
if (!allowedGroupIds.has(String(groupId))) return;
|
||||
const weeks = activeWeeks.get(activeWeekKey(rule.id, groupId, slotKey)) || [];
|
||||
if (!weeks.length) return;
|
||||
const lesson = {
|
||||
id: `${rule.id}:${slotKey}:${groupId}`,
|
||||
parity: slot.parity || 'BOTH',
|
||||
subjectName: rule.subjectName || 'Без дисциплины',
|
||||
teacherName: slot.teacherName || '',
|
||||
classroomName: slot.classroomName || '',
|
||||
lessonTypeName: slot.lessonTypeName || '',
|
||||
lessonFormat: slot.lessonFormat || '',
|
||||
subgroupNames: slotSubgroupNames(slot),
|
||||
weekText: compactWeekRanges(weeks),
|
||||
groupName: (rule.groupNames || [])[groupIndex] || groupNameById(groupId)
|
||||
};
|
||||
const key = visualCellKey(groupId, slot.dayOfWeek, visualSlotKey(slot));
|
||||
if (!grouped.has(key)) grouped.set(key, []);
|
||||
grouped.get(key).push(lesson);
|
||||
});
|
||||
});
|
||||
});
|
||||
grouped.forEach(items => items.sort(compareVisualLessons));
|
||||
return grouped;
|
||||
}
|
||||
|
||||
function buildActiveWeeksByRuleSlot() {
|
||||
const activeWeeks = new Map();
|
||||
rules.forEach(rule => {
|
||||
const maxWeeks = maxWeeksForRule(rule);
|
||||
(rule.groupIds || []).forEach(groupId => {
|
||||
LESSON_TYPE_LIMITS.forEach(config => {
|
||||
const totalHours = Number(rule[config.hoursField] || 0);
|
||||
if (totalHours <= 0) return;
|
||||
const categorySlots = (rule.slots || [])
|
||||
.map((slot, index) => ({ slot, index }))
|
||||
.filter(item => lessonTypeKeyById(item.slot.lessonTypeId) === config.key)
|
||||
.sort(compareSlotItems);
|
||||
if (!categorySlots.length) return;
|
||||
|
||||
let consumedHours = 0;
|
||||
const startWeek = Math.max(1, Number(rule[config.startWeekField] || 1));
|
||||
for (let week = startWeek; week <= maxWeeks && consumedHours < totalHours; week += 1) {
|
||||
for (const item of categorySlots) {
|
||||
if (consumedHours >= totalHours) break;
|
||||
if (!slotAppliesToWeek(item.slot, week)) continue;
|
||||
const key = activeWeekKey(rule.id, groupId, visualRuleSlotKey(item.slot, item.index));
|
||||
if (!activeWeeks.has(key)) activeWeeks.set(key, []);
|
||||
activeWeeks.get(key).push(week);
|
||||
consumedHours += ACADEMIC_HOURS_PER_SLOT;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return activeWeeks;
|
||||
}
|
||||
|
||||
function renderVisualCellContent(lessons) {
|
||||
if (!lessons.length) return '';
|
||||
const oddLessons = lessons.filter(lesson => lesson.parity === 'BOTH' || lesson.parity === 'ODD');
|
||||
const evenLessons = lessons.filter(lesson => lesson.parity === 'BOTH' || lesson.parity === 'EVEN');
|
||||
const shouldSplit = oddLessons.length > 0
|
||||
&& evenLessons.length > 0
|
||||
&& visualLessonSignature(oddLessons) !== visualLessonSignature(evenLessons);
|
||||
|
||||
if (!shouldSplit) {
|
||||
return `<div class="schedule-visual-unified">${uniqueVisualLessons(lessons).map(renderVisualLesson).join('')}</div>`;
|
||||
}
|
||||
|
||||
return CELL_PARITY_LAYOUT.map(({ parity, label }) => {
|
||||
const parityLessons = parity === 'ODD' ? oddLessons : evenLessons;
|
||||
return `
|
||||
<div class="schedule-visual-half schedule-visual-half-${parity.toLowerCase()}">
|
||||
<span class="schedule-visual-half-label">${escapeHtml(label)}</span>
|
||||
${parityLessons.length ? parityLessons.map(renderVisualLesson).join('') : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function uniqueVisualLessons(lessons) {
|
||||
const unique = new Map();
|
||||
lessons.forEach(lesson => {
|
||||
unique.set(visualLessonSignature([lesson]), lesson);
|
||||
});
|
||||
return Array.from(unique.values()).sort(compareVisualLessons);
|
||||
}
|
||||
|
||||
function renderVisualLesson(lesson) {
|
||||
const meta = [
|
||||
lesson.lessonTypeName,
|
||||
lesson.lessonFormat,
|
||||
lesson.teacherName,
|
||||
lesson.classroomName
|
||||
].filter(Boolean).join(' · ');
|
||||
const subgroupText = lesson.subgroupNames.join(', ');
|
||||
return `
|
||||
<article class="schedule-visual-lesson">
|
||||
<strong>${escapeHtml(lesson.subjectName)}${lesson.weekText ? ` <span>${escapeHtml(lesson.weekText)}</span>` : ''}</strong>
|
||||
${subgroupText ? `<small>${escapeHtml(subgroupText)}</small>` : ''}
|
||||
${meta ? `<small>${escapeHtml(meta)}</small>` : ''}
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function collectVisualGroups() {
|
||||
const byId = new Map();
|
||||
rules.forEach(rule => {
|
||||
(rule.groupIds || []).forEach((id, index) => {
|
||||
if (!byId.has(String(id))) {
|
||||
byId.set(String(id), {
|
||||
id,
|
||||
name: (rule.groupNames || [])[index] || groupNameById(id)
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return Array.from(byId.values()).sort((a, b) => naturalCompare(a.name, b.name));
|
||||
}
|
||||
|
||||
function collectVisualSlotsFromRules() {
|
||||
const byKey = new Map();
|
||||
rules.flatMap(rule => rule.slots || []).forEach(slot => {
|
||||
const key = visualSlotKey(slot);
|
||||
if (!byKey.has(key)) {
|
||||
byKey.set(key, {
|
||||
key,
|
||||
orderNumber: slot.timeSlotOrder,
|
||||
orderLabel: slot.timeSlotOrder ? `${slot.timeSlotOrder} пара` : 'Пара',
|
||||
timeLabel: slot.timeSlotLabel || 'Время не указано'
|
||||
});
|
||||
}
|
||||
});
|
||||
return Array.from(byKey.values()).sort(compareVisualSlots);
|
||||
}
|
||||
|
||||
function visualSlotFromTimeSlot(slot) {
|
||||
return {
|
||||
key: String(slot.id),
|
||||
orderNumber: slot.orderNumber,
|
||||
orderLabel: slot.orderNumber ? `${slot.orderNumber} пара` : 'Пара',
|
||||
timeLabel: slot.startTime || slot.endTime ? `${trimTime(slot.startTime)}-${trimTime(slot.endTime)}` : 'Время не указано'
|
||||
};
|
||||
}
|
||||
|
||||
function setVisualState(message) {
|
||||
if (!visualStage) return;
|
||||
visualStage.innerHTML = `<div class="loading-row schedule-visual-state">${escapeHtml(message)}</div>`;
|
||||
}
|
||||
|
||||
function handleVisualGroupFilterChange(event) {
|
||||
if (!event.target.matches('input[type="checkbox"]')) return;
|
||||
selectedVisualGroupIds = new Set(Array.from(visualGroupFilter.querySelectorAll('input[type="checkbox"]:checked'))
|
||||
.map(input => String(input.value)));
|
||||
visualGroupFilterTouched = true;
|
||||
renderVisualSchedule();
|
||||
}
|
||||
|
||||
function syncVisualGroupFilter(allGroups) {
|
||||
ensureVisualGroupSelection(allGroups);
|
||||
if (!visualGroupFilter) return;
|
||||
if (!allGroups.length) {
|
||||
visualGroupFilter.innerHTML = '<span class="muted">Нет групп в правилах</span>';
|
||||
return;
|
||||
}
|
||||
visualGroupFilter.innerHTML = allGroups.map(group => {
|
||||
const id = String(group.id);
|
||||
return `
|
||||
<label class="schedule-visual-group-chip">
|
||||
<input type="checkbox" value="${escapeHtml(id)}" ${selectedVisualGroupIds.has(id) ? 'checked' : ''}>
|
||||
<span>${escapeHtml(group.name)}</span>
|
||||
</label>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function ensureVisualGroupSelection(allGroups) {
|
||||
const availableIds = new Set(allGroups.map(group => String(group.id)));
|
||||
selectedVisualGroupIds = new Set(Array.from(selectedVisualGroupIds).filter(id => availableIds.has(id)));
|
||||
if (visualGroupFilterTouched) return;
|
||||
|
||||
const formIds = selectedRuleGroupIds()
|
||||
.map(String)
|
||||
.filter(id => availableIds.has(id));
|
||||
if (!visualGroupFilterTouched && formIds.length) {
|
||||
selectedVisualGroupIds = new Set(formIds);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedVisualGroupIds.size && allGroups.length) {
|
||||
selectedVisualGroupIds = new Set([String(allGroups[0].id)]);
|
||||
}
|
||||
}
|
||||
|
||||
function renderRuleSlotsSummary(slots) {
|
||||
if (!slots || !slots.length) return '<span class="muted">Нет слотов</span>';
|
||||
return slots.map(slot => {
|
||||
@@ -712,4 +1069,145 @@ export async function initSchedule() {
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
}
|
||||
|
||||
function visualCellKey(groupId, dayOfWeek, slotKey) {
|
||||
return `${groupId}:${dayOfWeek}:${slotKey}`;
|
||||
}
|
||||
|
||||
function visualSlotKey(slot) {
|
||||
return String(slot.timeSlotId ?? slot.timeSlotOrder ?? slot.timeSlotLabel ?? 'unknown');
|
||||
}
|
||||
|
||||
function visualRuleSlotKey(slot, index) {
|
||||
return String(slot.id ?? `${visualSlotKey(slot)}:${slot.dayOfWeek}:${slot.parity}:${index}`);
|
||||
}
|
||||
|
||||
function activeWeekKey(ruleId, groupId, slotKey) {
|
||||
return `${ruleId}:${groupId}:${slotKey}`;
|
||||
}
|
||||
|
||||
function slotAppliesToWeek(slot, week) {
|
||||
if (!slot.parity || slot.parity === 'BOTH') return true;
|
||||
const weekParity = week % 2 === 0 ? 'EVEN' : 'ODD';
|
||||
return slot.parity === weekParity;
|
||||
}
|
||||
|
||||
function maxWeeksForRule(rule) {
|
||||
const semester = semesters.find(item => String(item.id) === String(rule.semesterId));
|
||||
if (semester?.startDate && semester?.endDate) {
|
||||
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));
|
||||
}
|
||||
return 18;
|
||||
}
|
||||
|
||||
function parseIsoDate(value) {
|
||||
const [year, month, day] = String(value).split('-').map(Number);
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
function compactWeekRanges(weeks) {
|
||||
const uniqueWeeks = [...new Set((weeks || []).map(Number).filter(Boolean))].sort((a, b) => a - b);
|
||||
if (!uniqueWeeks.length) return '';
|
||||
const ranges = [];
|
||||
let start = uniqueWeeks[0];
|
||||
let previous = uniqueWeeks[0];
|
||||
for (let index = 1; index < uniqueWeeks.length; index += 1) {
|
||||
const week = uniqueWeeks[index];
|
||||
if (week === previous + 1) {
|
||||
previous = week;
|
||||
continue;
|
||||
}
|
||||
ranges.push(weekRangeText(start, previous));
|
||||
start = week;
|
||||
previous = week;
|
||||
}
|
||||
ranges.push(weekRangeText(start, previous));
|
||||
return `(${ranges.join(', ')})`;
|
||||
}
|
||||
|
||||
function weekRangeText(start, end) {
|
||||
return start === end
|
||||
? `${start} нед.`
|
||||
: `с ${start} по ${end} нед.`;
|
||||
}
|
||||
|
||||
function compareSlotItems(first, second) {
|
||||
return Number(first.slot.dayOfWeek || 0) - Number(second.slot.dayOfWeek || 0)
|
||||
|| compareVisualSlots(slotDescriptor(first.slot), slotDescriptor(second.slot))
|
||||
|| Number(first.index || 0) - Number(second.index || 0);
|
||||
}
|
||||
|
||||
function slotDescriptor(slot) {
|
||||
return {
|
||||
key: visualSlotKey(slot),
|
||||
orderNumber: slot.timeSlotOrder,
|
||||
timeLabel: slot.timeSlotLabel
|
||||
};
|
||||
}
|
||||
|
||||
function compareVisualSlots(first, second) {
|
||||
const firstOrder = Number(first.orderNumber || first.key);
|
||||
const secondOrder = Number(second.orderNumber || second.key);
|
||||
if (!Number.isNaN(firstOrder) && !Number.isNaN(secondOrder) && firstOrder !== secondOrder) {
|
||||
return firstOrder - secondOrder;
|
||||
}
|
||||
return naturalCompare(first.timeLabel, second.timeLabel) || naturalCompare(first.key, second.key);
|
||||
}
|
||||
|
||||
function compareVisualLessons(first, second) {
|
||||
return naturalCompare(first.subjectName, second.subjectName)
|
||||
|| naturalCompare(first.weekText, second.weekText)
|
||||
|| naturalCompare(first.lessonTypeName, second.lessonTypeName)
|
||||
|| naturalCompare(first.teacherName, second.teacherName);
|
||||
}
|
||||
|
||||
function visualLessonSignature(lessons) {
|
||||
return lessons
|
||||
.map(lesson => [
|
||||
lesson.id,
|
||||
lesson.subjectName,
|
||||
lesson.weekText,
|
||||
lesson.lessonTypeName,
|
||||
lesson.teacherName,
|
||||
lesson.classroomName,
|
||||
lesson.subgroupNames.join(',')
|
||||
].join(':'))
|
||||
.sort((a, b) => naturalCompare(a, b))
|
||||
.join('|');
|
||||
}
|
||||
|
||||
function groupNameById(id) {
|
||||
return groups.find(group => String(group.id) === String(id))?.name || `Группа ${id}`;
|
||||
}
|
||||
|
||||
function naturalCompare(a, b) {
|
||||
return String(a || '').localeCompare(String(b || ''), 'ru', { numeric: true, sensitivity: 'base' });
|
||||
}
|
||||
|
||||
function ruleCountLabel(count) {
|
||||
const tail = count % 100;
|
||||
if (tail >= 11 && tail <= 14) return 'правил';
|
||||
if (count % 10 === 1) return 'правило';
|
||||
if ([2, 3, 4].includes(count % 10)) return 'правила';
|
||||
return 'правил';
|
||||
}
|
||||
|
||||
function groupCountLabel(count) {
|
||||
const tail = count % 100;
|
||||
if (tail >= 11 && tail <= 14) return 'групп';
|
||||
if (count % 10 === 1) return 'группа';
|
||||
if ([2, 3, 4].includes(count % 10)) return 'группы';
|
||||
return 'групп';
|
||||
}
|
||||
|
||||
function rowCountLabel(count) {
|
||||
const tail = count % 100;
|
||||
if (tail >= 11 && tail <= 14) return 'строк';
|
||||
if (count % 10 === 1) return 'строка';
|
||||
if ([2, 3, 4].includes(count % 10)) return 'строки';
|
||||
return 'строк';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user